实例构造器和类(引用类型)
构造器是将类型的实例初始化为良好状态的特殊方法。构造器方法在“方法定义元数据表”中始终叫做.ctor(constructor的简称)。创建引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实力构造器来设置对象的初始状态。
构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。没有被构造器构造器显式重写的所有字段都保证获得0或者null值。
和其他方法不同,实力构造器永远不能被继承。也就是说,类只有类自己定义的实例构造器。由于永远不能被继承实力构造器,所以实例构造器不能使用以下修饰符:virtual,new,override,sealed和abstract。如果类没有显示定义任何构造器,c#编译器将定义一个默认无参构造器。在它的实现中,只是简单地调用了基类的无参构造器。
如果类的修饰符为abstract,那么编译器生成的默认构造器的可访问性就为protected;否则,构造器会被赋予public可访问性。如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译器会报错。如果类的修饰符为static(sealed和abstract),编译器根本不会再类的定义中生成默认构造器。
一个类型可以定义多个实参构造器。每个构造器都必须有不同的签名,而且每个都可以有不同的可访问性。为了使代码“可验证”,类的实例构造器在访问从基类继承的任何字段志强,必须先调用基类的构造器。如果派生类的构造器没有显式调用一个基类构造器,c#编译器会自动生成对默认的基类构造器的调用。最终,system.object的公共无参构造器会得到调用。该构造器什么都不做,会直接返回。
极少数时候可以在不调用实例构造器的前提下创建类型的实例。一个典型的例子是object的memberwiseClone方法。该方法的作用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),然后将源对象的字节数据复制到新对象中。另外,用运行时序列化器反序列化对象时,通常也不需要调用构造器。反序列化代码使用System.Runtime.Serialization.FormatterServices类型的GetUninitializeObject或者GetSafeUninitializeObject方法为对象分配内存,期间不会调用一个构造器。
重要提示:不要在构造器中调用虚方法。原因是加入被实例化的类型重写了虚方法,就会执行派生类型对虚方法的实现。但在这个时候,尚未完成对继承层次结构中所有字段的初始化(被实例化的类型的构造器还没有运行),所以,调用虚方法会导致无法预测的行为。归根结底,这是由于调用虚方法时,知道运行时之前都不会选择执行该方法的实际类型。
构造器也可以继承,可以把公共部分的构造器写成基方法。
class SomeType { private int a; private string b; private double c; //该构造器将所有字段都设为默认值,其他所有构造器都显式调用该构造器 public SomeType() { a = 1; b = "@"; c = 1.1; } //该构造器将所有字段都设为默认值,然后修改a的值 public SomeType(int x):this() { a = x; } }
实例构造器和结构(值类型)
值类型(struct)构造器的工作方式与引用类型(classs)的构造器截然不同。clr总是允许创建值类型的实例,并且没有办法阻止值类型的实例化。所以,值类型其实并不需要定义构造器,c#编译器根本不会为值类型内联(嵌入)嵌入默认的无参构造器。(如果为值类型创建无参构造函数,编译器会报错)
类型构造器
除了实例构造器,clr还还和支持类型构造器(type constructor),也成为静态构造器、类构造器或者类型初始化器。类型构造器可应用于接口(虽然C#编译器不允许)、引用类型和值类型。实例构造器的作用是设置类型的实例的初始状态。对应地,类型构造器的作用是设置类型的初始状态。类型默认没有定义类型构造器。如果定义,也只能定义一个。此外,类型构造器永远没有参数。以下代码演示了如何在c#中为引用类型和值类型定义一个类型构造器。
class SomeRefType{ static SomeRefType(){ // SomeRefType被首次访问时,执行这里的代码 } } struct SomeValType{ static SomeValType (){ // SomeValType被首次访问时,执行这里的代码 } }
(虽然能在值类型中定义类型构造器,但永远不都要真的那么做,因为clr有时不会调用值类型的静态类型构造器。)
定义类型构造器类似于定义无参实例构造器,区别在于必须标记为为static。此外,类型构造器总是私有:c#自动把它们标记为private。(而且你无法修改)之所以必须私有,是为了防止任何由开发人员写的代码调用它,对它的调用总是有clr负责。
类型构造器的调用比较麻烦。jit编译器在编译一个方法时,会查看代码中都引用了哪些类型。任何一个类型定义了类型构造器,jit编译器都会检查针对当前AppDomain,是否已经执行了这个类型构造器。如果构造器从未执行,jit编译器会在它生成的本机代码中添加对类型构造器的调用。如果类型构造器已经执行,jit编译器就不添加对它的调用。
现在,当方法被jit编译完毕之后,线程开始执行它,最终会执行到调用类型构造器的代码。事实上,多个线程可能同时执行相同的方法。clr希望确保每个appdomain中,一个类型构造器只执行一次。为了确保这一点,在调用类型构造器时,调用线程要获取一个互斥线程同步锁。这样一来,如果多个线程视图同时调用某个类型的静态构造器,只有一个线程可以获得锁,其他线程线程会被阻塞,而且已经执行过,就不会再次被执行。
由于clr保证一个类型构造器在每个appdomain中只执行一次,而且这种执行是线程安全的,所以非常适合在类型构造器中初始化类型需要的任何单实例(singleton)对象
那个线程中的两个类型构造器包含相互引用的代码可能出现问题。假定classA的类型构造器包含了引用classB的代码,classB的类型构造器包含了引用ClassA的代码。在这种情况下,clr仍然保证每个类型构造器的代码只被执行一次,但是,完全有可能classA的类型构造器还没执行完毕的前提下,就开始执行classB的类型构造器。因此,应尽量避免写造成这种情况的代码。事实上,由于clr负责类型构造器的调用,所以任何代码都不应该要求以特定的顺序调用类型构造器。
最后,如果类型构造器抛出未处理的异常,clr会认为类型不可用。视图访问该类型的任何字段或方法都会抛出异常。
类型构造器中的代码只能访问类型的静态字段,并且它的常规用途就是初始化这些字段。和实例字段一样,c#提供了一个简单的语法来初始化类型的静态字段
internal sealed class SomeType{ private static Int32 s_x=5; }
操作符重载方法
有的语言允许类型定义操作符应该如何操作类型的实例。例如,许多类型(比如system.string)都重载了相等(==)和不等(!=)操作符。clr对操作符重载一无所知,他甚至不知道什么是操作符。是编程语言定义了每个操作符的含义,以及当这些特殊符号出现时,应该生成什么样的代码。
例如在c#中,向基元(类型的)数字应用+符号,编译器生成将两个数加到一起的代码。将+符号应用于string对象,c#编译器生成将两个字符串连接到一起的代码。虽然clr对操作符一无所知,但它确实规定了语言应如何公开操作符重载,以便由另一种语言的代码使用。每个编程语言都要自行决定是否支持操作符重载,至于clr,操作符重载只是方法而已。
clr规范要求操作符重载方法必须是public和static方法。另外,c#要求操作符重载方法至少有一个参数的类型与当前定义这个方法的类型相同。之所以要进行这样的限制,是为了使c#编译器能在合理的时间内找到要绑定的操作符方法。
一下c#代码展示了在一个类中定义的操作符重载方法
public sealed class Complex{ public static Complex operator +( Complex c1, Complex c2){ …… } }
转换操作符方法
有时需要将对象从一种类型转换为另一种类型(例如将byte转换为int32)。当源类型和目标类型都是编译器识别的基元类型时,编译器自己就知道如何生成转换对象所需的代码。
如果源类型或目标类型不是基元基元额类型,编译器会生成代码,要求clr执行转换(强制转型)。这种情况下,clr只是检查源对象的类型和目前类型(或者从目标类型派生的其他类型)是不是相同。但有时需要将对象从一种类型转换成全然不同的其他类型。为了进行这样的转换,类型应该定义只有一个参数的公共构造器,该参数要求是源类型的实例。还应该定义无参的公共实例方法ToXxx(类似于你熟悉的tostring方法),每个方法都将定义类型的实例转换成Xxx类型。
调用这些构造器和方法,使用任何编程语言的开发人员都能将Int32或者Single对象转换成Rational对象,反之亦然。这些转换能给编程带来很多方便。设计。类型时,应认真考虑类型需要迟滞的转换构造器和方法。
public sealed class Rational { //由一个int32构造一个rational public Rational (Int32 num) { } //将一个Rational转换成一个INT32 public Int32 ToInt32() { return 1; } //由一个int32隐式构造并返回一个rational public static implicit operator Rational(Int32 num) { return new Rational(num); } //由一个rational显式返回一个INT32 public static explicit operator Int32(Rational r){ return r.ToInt32(); } }
对于转换操作符,编译器既可生成代码来隐式调用转换操作符方法,也可只有在源代码进行了显式转型时才生成代码来调用转换操作符方法。在c#中,implicit关键字告诉编译器为了生成代码来调用方法,不需要在源代码中进行显式转型。相反,explicit关键字告诉编译器只有在发现了显式转型,才调用方法。
在implicit或explicit关键字之后,要指定operator关键字告诉编译器该方法是一个转换操作符。在operator之后,指定对象要转换成什么类型。在圆括号内,则指定要从什么类型转换。
像前面那样为Rational类型定义了转换操作符之后,就可以像下面这样的调用
Rational r1=5 ;//int32隐式转型为rational int32 x=(int32) r1;
在幕后,c#编译器检测到代码中的转型,并内部生成IL代码来调用Rational类型定义的转换操作符方法。将对象从一种类型转换成另一种类型的方法总是叫做op_Implicit或者op_Explicit。
为了真正理解操作符重载方法和转换操作符方法,强烈建议将system.decimal类型作为典型来研究。
扩展方法
扩展方法允许定义一个静态方法,并用实例方法的语法来调用。
public static class StringBuilderExtensions{ public static Int32 IndexOf(this StringBuilder sb,Char value){ for(int index =0;index<sb.Length;index ++) if(sb[index]==value) return index; return -1; } }
现在,当编译器看到以下代码
Int32 index=sb.IndexOf(‘x’);
就首先检查StringBuilder类火灾它的任何基类是否提供了获取单个Char参数、名为IndexOf的一个实例方法。如果是,就生成il代码来调用它。如果没找到匹配的实例方法,就继续检查是否有任何静态类定义了名为IndexOf的静态方法,方法的第一个参数的类型和当前用于调用方法的那个表达式的类型匹配,而且该类型必须用this关键字表示。
规则和原则
关于扩展方法,有一些附加的规则和原则需要注意。
1 c#只支持扩展方法,不支持扩展属性、扩展事件、扩展操作符等。
2 扩展方法(第一个参数前面有this的方法)必须在非泛型的静态类中声明。然后,类名没有限制,可以随便叫什么名字。当然,扩展方法至少要有一个参数,而且只有第一个参数能用this关键字标记。
3 c#编译器在静态类中查找扩展方法时,要求静态类本身必须具有文件作用域。如果静态类嵌套在另一个类中,c#编译器显示以下消息:扩展方法必须在顶级静态类中定义
4 由于静态类可以取任何名字,所以c#编译器要花一定时间来寻找扩展方法,它必须坚持稳健作用域中的所有静态类,并扫描他们的所有静态方法来查找一个匹配。为增强性能,并避免找到非你所愿的扩展方法,c#编译器要求“导入”扩展方法。也就是using xxxxxxx;
5 使用这个功能须谨慎,一个原因是并非所有程序员都熟悉它。例如,用一个扩展方法扩展一个类型时,同时也扩展了派生类型。所以,不要讲SYSTEM.OBJECT作为扩展方法的第一个参数,否则这个方法在所有表达式类型上都能调用。
用扩展方法扩展各种类型
由于扩展方法实际是对一个静态方法的调用,所以clr不会生成代码对调用方法的表达式的值进行null检查。
还要注意,可以为接口类型定义扩展方法,如下
public static void ShowItems<t>(this IEnumerable<T> collection){ foreach (var item in collection) console.writeline(item); }
任何表达式,只要他最终的类型实现了IEnumerable<T>接口,就能调用上述扩展方法。
重要提示:扩展方法时Microsoft的linq(language integrated query,语言集成查询)计数的基础。
还可以为委托类型定义扩展方法
public static void InvokeAndCatch<TException>(this Action<Object> d, Object o)where TException:Exception { try{ d(o)} catch(TException){} }
可选参数和命名参数
规则和原则
1 有默认值的参数必须放在没有默认值的所有参数之后。换言之,一旦定义了有默认值的参数,它右边的所有参数也必须有默认值。
2 默认值必须是编译时能确定的常量值。那么,哪些参数能设置默认值?这些参数的类型可以是c#认定的基元类型,还包括枚举类型,以及能设为null的任何引用类型。
3 不要重命名参数变量,否则任何调用者以参数名的方式传递实参,他们的代码也必须修改。
4 如果参数用ref或out关键字进行了标识,就不能设置默认值。因为没法办法为这些参数传递有意义的默认值。
在c#中,一旦为参数分配 默认值,编译器就会在内部向该参数应用定制特性system.runtime.interopServices.OptionalAttribute.该特性会在最终胜出的文件的元数据中持久性地存储下来。之后,一旦编译器发现某个方法调用缺失了部分实参,就可以确定省略的是可选的实参,并从元数据中提取默认值,将值自动签入调用中。
c#能根据初始化表达式的类型推断方法中的局部变量的类型,所以可以使用var进行隐式类型标记
以传引用的方式向方法传递参数
clr默认所有方法参数都传值。传递引用类型的对象时,对象引用(或者说指向对象的指针)被传给方法。注意引用(或指针)本身是传值的,意味着方法能修改对象,而调用者能看到这些修改。对于值类型的实例,传给方法的是实例的一个副本,意味着方法将获取它专用的一个值类型实例副本,调用者中的实例不受影响。
重要提示:在方法中,必须知道传递的每个参数是引用类型还是值类型,处理参数的代码显著有别。
clr允许以传引用而非传值的方式传递参数。c#用关键字out或ref支持这个功能。两个关键字都告诉c#编译器生成元数据来指明该参数是传引用的。编译器将生成代码来传递参数的地址,而非传递参数本身。
clr不区分out和ref,意味着无论用哪个关键字,都会生成相同的IL代码。另外,元数据也几乎完全一致,只有一个bit除外,它用于记录声明方法方法时指定的是out还是ref。但c#编译器是将这两个关键字区别对待的,而且这个区别决定了由哪个方法负责初始化所引用的对象。如果方法的参数用out来标记,表明不指望调用者在调用方法之前初始化好对象。被调用的方法不能读取参数的值,而且在返回前必须向这个值写入。相反,如果方法的参数用ref来标记,调用者就必须在调用该方法前初始化参数的值,被调用的方法可以读取值以及向值写入。
为大的值类型使用out,可提升代码的执行效率,因为它避免了在进行方法调用时复制值类型实例的字段。
向方法传递可变数量的参数
方法有时需要获取可变数量的参数。例如,system.string类型的一些方法允许连接任意数量的字符串,还有一些方法允许指定一组要统一格式化的字符串。
为了接受可变数量的参数,方法要像下面这样声明:
static int Add(params int[] values)
或者使用lits进行传参
参数和返回类型的设计规范
声明方法的参数类型时,应尽量指定最弱类型,宁愿要接口也不要基类。例如,如果要写方法来处理一组数据项,最好是用接口(比如IEnumerable<T>)声明参数,而不要用强类型数据(比如List<T>)或更全的接口类型(比如ICollection<T>或IList<T>):原因是使用弱类型更灵活,适合更广泛的情形。
当然,如果方法需要的是列表(而非任何可枚举的对象),就应该将参数类型声明为IList<T>。但仍然要避免将参数类型声明为List<T>。
注意,这里的例子讨论的是集合,使用接口体系结构来设计的。讨论使用基类体系结构设计的类时,概念同样使用。例如,要实现对流中的字节进行处理的方法,可定义以下方法:
public void ProcessBytes(Stream someStream) { …… } public void ProcessBytes(FileStream someStream) { …… }
第一个方法能处理任何流,包括fileStream,netWorkStream等。第二个只能处理fileStream。
相反,一般最好是将方法的返回类型声明为最强的类型(防止受限于特定类型)。例如方法最好返回FileStream而不是Stream对象。