CLR类型设计之参数传递

写到这篇文章的时候,笔者回忆起来以前的开发过程中,并没有注意参数的传递是以值传递还是引用传递的,也是第一次了解到可变参数params,常用的不一定就代表理解,可能只是会用。接下来我们就一起回忆一下关于参数传递中得一些方法技巧。

可选参数和命名参数

           在设计方法的参数时,可为部分或全部参数分配默认值,调用这些方法的代码,然后调用这些方法的代码可以选择不提供部分实参,使用其默认值,此外,调用方法时可通过指定参数名称来传递实参。下面的例子为一个有一个形参两个默认值参数的方法:方法第三次调用S:"张三",就是一个指定参数参数名称传参的例子

注意事项:

1.可为方法时,构造器方法和有参属性的参数指定默认值,还可以为属于委托定义一部分的参数指定默认值,调用该委托的变量时,可以省略实参使用默认值。

2.默认值必须是编译时就确定的常量值。

3.如果参数用ref和out关键字进行了标识,就不能设置默认值。

4.命名实参只能出现在实参列表的尾部。

关于可选参数和命名参数是比较简单的一部分,但是却也很实用。

隐式类型的局部变量

         这种设置参数的方法比较少见,但是多在底层代码中见到,我回想平时所写代码,大部分参数类型都会有所指定,在接下来的文章中我们会讨论参数类型弱化,所以我们很多时候可以传比较基类的参数,比如objeck,T等类型接收参数,但是有的时候我们希望可以定义一些比较公共的,以让方法最大程度复用,C#能根据初始化表达式的类型推断方法中的局部变量类型

上图是我写了个ShowvalueType方法,接收泛型T,不了解泛型的可以百度一下,我用var定义了四种类型的变量,分别是string,int,DateTime,IListI,然后运行结果如下图,方法准确的识别了每一种数据类型,并且正确的返回了其类型。

 以传引用的方式向方法传递参数

这里是整个参数的重点,也是本文最复杂的难点。ref和out参数从IL代码上是没有区别的,但实际上在C#的使用中是存在很大的区别的,我们还是沿着CLR中得堆栈概念去分析两者的区别。我们先来回忆一下值类型和引用类型在堆栈上的存放地址,关于更具体的内容我就不去讲了。

但是我们大体可以知道,在C#中参数传递应该是算有四种情况的。参数传递方式有按值传递和按引用传递两种,而C#支持的类型呢,也分为两种:值类型和引用类型。所以排列组合一下,就有四种情况了:值类型按值传递、引用类型按值传递、值类型按引用传递和引用类型按引用传递。

值类型参数的按值传递

上例代码就是一个参数传递值类型的例子,我们可以看出sum的值 并没有改变还是10,我们知道值类型都是存储在栈上。那么实际过程中发生了什么呢?实际上的过程是这样,sum在栈中声明并且赋值为10,i也同时在栈中声明并且赋值为10,接下来执行++i操作,i的值会变成11,但是sum的值不会跟着改变,还是10。

        引用类型参数按值传递:

  public sealed  class Student
    {
         public int Age { get; set; }
     }

我们先定义一个Student类,然后在定义一个接收引用类型的方法

 1   static  void Main(string[] args)
 2         {
 3             Student stu = new Student { Age = 20 };
 4             Add(stu);
 5             Console.WriteLine(stu.Age);
 6
 7         }
 8         public static void Add(Student i) {
 9             i.Age++;
10         }

运行结果是21,说明引用类型参数传递是内存地址,因此在方法内对变量的任何改变都会影响到原数据。

  值类型按引用传递

如果把要把值类型当作引用类型传递,必须使用ref或out关键字来传递变量。示例中使用的是引用类型所以不用ref关键字,这个时候我们回头在看第一个sum的例子,我们将其改写为,最后的输出结果就是11,如果使用了ref或者out关键字,则调用时,变量名称前也必须加上ref或out,这就是ref的神奇之处,那么ref在参数传递中到底起了什么作用呢,我们可以理解为ref 仅仅是一个地址。在文章前面我们提到过, 每个变量都有其堆栈,不同的变量不能共用一个堆栈地址。所以形参和实参在栈上的地址是不一样的。ref所起的作用我们可以简单的理解为他会记住实参的地址,当形参的值改变后,他会把值映射回 之前的实参地址。

 1     static  void Main(string[] args)
 2         {
 3             int sum = 10;
 4             Add(ref sum);
 5             Console.WriteLine(sum);
 6
 7         }
 8         public static void Add(ref int i) {
 9             i++;
10         }

 引用类型按引用传递

         还是看刚才第二个例子,但是这回我们在改写一下这个例子,类型不变

  public sealed  class Student
    {
         public int Age { get; set; }
     }
 1  static  void Main(string[] args)
 2         {
 3             Student stu = new Student { Age = 20 };
 4             Add(ref stu);
 5             Console.WriteLine(stu.Age);
 6
 7         }
 8         public static void Add(ref Student i) {
 9             i.Age++;
10         }

输出结果是不变的,只是说明了引用类型也可以这样传递,关于ref和out的却别

1.ref侧重于输入参数,out侧重于输出参数

2.ref的参数值必须初始化,out的参数可以不必初始化

3.编译器会按照不同标准来验证代码是否正确

向方法传递可变量的参数

          方法有时候需要获取可变数量的参数,为了接受可变量,方法要像下面这种形式声明

 1     public static Int32 Add(params int[] values) {
 2             int sum = 0;
 3             if (values != null)
 4             {
 5                 for (int i = 0; i < values.Length; i++)
 6                 {
 7                     sum += values[i];
 8                 }
 9             }
10             return sum;
11         }

params关键字只能应用于方法签名中的最后一个参数,并且一个方法签名中只能有一个params关键字,那么为什么params关键字声明,会导致参数变成可变的呢?按照上例代码,参数应该是一个数组才对,其实是C#为我们处理了params关键字,我们可以直接这样调用

//显示15
            Console.WriteLine(Add(1,2,3,4,5));
            //显示0
            Console.WriteLine(Add(null));
            //显示0
            Console.WriteLine(Add());

我已经直接把调用结果输出的值显示出来了,如果不需要传参,那么直接传null会比传空更高性能一些。

注意事项:

调用参数可变的方法对性能会有所影响,数组对象必须在堆上分配,数组元素必须初始化,而且数组的内存最终需要垃圾回收,要减少对性能的影响,可以考虑多定义个不同参数的重载版本,减少使用params关键字

参数返回类型的设计规范

        参数尽可能的去使用弱类型而不是强类型,而返回值则尽可能的使用强类型不用弱类型,设计规范大致就可以归结为这么一句话。

         声明方法的类型参数时,应尽量指定最弱的类型,宁愿要接口,也不要基类,例如,如果要写方法处理一组数据项,最好使用接口(IEnumerable<T>)声明参数,而不用强类型List<T>或者更强的接口类型。下例就是一个参数,第一个方法可以处理任何文件流,第二个只能处理Filestream文件流

       //好的
        public void ProcessBytes(Stream somestram) { }
        //不好的
        public void ProcessBytes(FileStream somestram) { }

而返回值则是越强越好,第一个返回值就告诉你返回的就是一个Filestream文件流,而第二个则返回一个Stream文件流,类型要弱了一些

        //好的
        public FileStream ProcessBytes() {}
        //不好的
        public Stream ProcessBytes() { }

总结

            今天梳理了参数传递中的一些情况,其中参数按值和按引用类型传递是重点,在今后的程序设计中更好的设计传参类型和返回值类型,也是可以让方法变得更好的一种技巧,内容比较多,可能有说明不对的地方,如果有请在评论说明

时间: 2024-11-04 22:56:32

CLR类型设计之参数传递的相关文章

CLR类型设计之方法与构造器

C#语言中最常用到的就是方法的语法,如果在控制台应用程序中,一定要有一个Main()方法作为程序入口,本文讨论的不是方法实现也不是为什么要写方法,而是来讨论下如何构造器和扩展方法. 无论学习那门语言都要学习函数体,C#,JAVA,PHP,都会涉及到函数体,而C#的函数体成员并不少,方法和构造器就是函数体成员之一,函数体成员还包括但不限于:方法,属性,构造器,终结器,运算符及索引器. 方法就是某个类相关的函数,也可以返回简单的基元类型或者什么也不反回,方法可以定义其公开性,如果使用static修饰

CLR类型设计之泛型(一)

在讨论泛型之前,我们先讨论一下在没有泛型的世界里,如果我们想要创建一个独立于被包含类型的类和方法,我们需要定义objece类型,但是使用object就要面对装箱和拆箱的操作,装箱和拆箱会很损耗性能,我们接下来会用一个示例来说明使用泛型和使用非泛型对值操作时的性能差距.但是如果使用泛型,也是同样的效果,不需要装箱和拆箱的同时泛型还保证了类型安全 言归正传,.Net自2.0以后就开始支持泛型,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型,此外,CLR还允许创建泛型接口和泛型委托.

CLR类型设计之属性

在之前的随笔中,我们探讨了参数,字段,方法,我们在开始属性之前回顾一下,之前的探讨实际上串联起来就是OOP编程的思想,在接下来的文章中,我们还会讨论接口(就是行为),举个例子:我们如果要做一个学生档案,我们需要先抽象出来有那些对象实体,比如有一个学生类,里面有学生id,姓名,年龄,班级等字段. 但是这不能满足我们的需求,我们要做学生档案管理,需要知道学生的每科成绩,所以我们还需要一个成绩类,里面定义了学生的学生Id,科目,科目分数,下面是两个类的代码示例 public sealed class

[译]类型设计准则

本文由 CYJB 译自 Type Design Guidelines(.NET Framework 4.5). 对 CLR 来说,只存在两种类型——引用类型和值类型.但是为了讨论框架设计,我们将类型细分为更多的逻辑组,每组有其特定的设计准则. 类是通用的引用类型,框架中的大部分类型都是类.类因其支持面向对象的大部分特性和普遍适应性而大受欢迎.基类和抽象类是与扩展性相关的特殊逻辑组. 接口是可以由引用类型和值类型实现的类型.它们可以作为引用类型和值类型的层次结构的根,或者模拟多重继承(CLR 本身

类库开发的设计准则 读书笔记 2类型设计准则

MSDN链接:http://msdn.microsoft.com/zh-cn/library/vstudio/ms229036(v=vs.100).aspx 系列文章列表: 1名称准则:http://www.cnblogs.com/liu-meng/p/4181984.html 2类型设计准则:http://www.cnblogs.com/liu-meng/p/4182737.html 类型与命名空间: 使用命名空间将类型组织到相关功能区域的的层次结构中 避免使用较深的命名空间层次结构 避免使用

异常跟踪之CLR 类型到 EDM 类型的映射不明确

异常信息: "指定的架构无效.错误: CLR 类型到 EDM 类型的映射不明确,因为多个 CLR 类型与 EDM 类型“Person”匹配. 以前找到的是 CLR 类型“A.Person”, 新找到的则是 CLR 类型“B.Person”. 这类异常信息在代码里面出现过几次,每次的解决方案都让人匪夷所思.不知道为什么出现,也不知道为什么被解决了. 查阅了一些国外的资料,链接: Don't use classes with the same name - EF uses only class na

.NET的堆和栈02,值类型和引用类型参数传递以及内存分配

在" .NET的堆和栈01,基本概念.值类型内存分配"中,了解了"堆"和"栈"的基本概念,以及值类型的内存分配.我们知道:当执行一个方法的时候,值类型实例会在"栈"上分配内存,而引用类型实例会在"堆"上分配内存,当方法执行完毕,"栈"上的实例由操作系统自动释放,"堆"上的实例由.NET Framework的GC进行回收.而本篇的重点要放在:值类型和引用类型参数的传递,

记一次类型设计的求索历程

多年以前,在我的刚接触编程语言时,我遇到了一个超出能力范围的类型设计问题.这个问题困扰我多年,让我寝食难安.原因并不是因为这个问题有多复杂,恰恰相反,让我纠结的是,这个问题看起来很简单,而我却找不到一个优秀的解决方法. 俗话说踏破铁鞋无觅处,得来全不费工夫.苦苦求索而不得的多年之后,我从一次系统设计过程中得到了启发,我终于想出了一个激动人心的解决方案.本文除了吹吹牛逼告诉你们我从几年前开始考虑代码的细节外,我还想和你们分享一下我的激动人心的优秀解决方案的产生过程. 问题所在 现在已有一个汽车类,

CLR via C#深解笔记二 - 类型设计

类型基础 所有类型都从System.Object派生 CLR要求所有对象都用new 操作符来创建. Employee e = new Employee("Constructor Parameters"); 以下是 new 操作符所做的事情: #1, 计算类型及所有基类型(一直到System.Object, 虽然它没有定义自己的实例字段)中定义的所有实例字段需要的字节数. 堆上的每个对象还需要一些额外(overhead 开销成员)的成员 -- 即“类型对象指针”(type object