类型的各种成员
类型可以定义以下种类的成员
1 常量
常量是指出数据值恒定不变的符号。这种符号使代码更易阅读和维护。常量总与类型管理,不与类型的实例管理。常量逻辑上总是静态成员。
2 字段
字段表示只读或可读、可写的数据值。字段可以是静态的,这种字段被认为是类型状态的一部分。字段也可以是实例(非静态),这种字段被认为是对象状态的一部分。强烈建议将字段声明为私有,防止类型或对象的状态被类型外部的代码破坏
3 实例构造器
实例构造器是将新对象的实例字段初始化为良好初始状态的特殊方法。
4 类型构造器
类型构造器是将类型的静态字段初始化为良好初始状态的特殊方法
5 方法
方法时更改或查询类型或对象状态的函数。作用域类型称为静态方法,作用于对象称为实例方法。方法通常要读写类型或对象的字段
![](/qrcode.jpg)
6 操作符重载
操作符重载实际是方法,定义了当操作符作用域对象时,应该如何操作该对象。
7 转换操作符
转换操作符是定义如何隐式或显式将对象从一种类型转型为另一种类型的方法。和操作符重载方法一样,并不是所有编程语言都支持转换操作符。
8 属性
属性允许用简单的、字段风格的语法设置或查询类型或对象的逻辑状态,同时保证状态不被破坏。作用域类型称为静态属性,作用域对象称为实例属性。属性可以无参(非常普遍),也可以有多个参数(比较少见,集合类用得多)
9 事件
静态事件允许类型向一个或多个静态或实例方法发送通知。实例(非静态)事件允许对象向一个或多个静态或实例方法发送通知。引发事件通常是为了响应提供事件的类型或对象的状态的改变。事件包含两个方法,允许静态或实例方法登记或注销对该事件的关注。除了这两个方法,事件通常还用一个委托字段来维护已登记的方法集。
10 类型
类型可定义其他嵌套类型。通常用这个办法将大的、复杂的类型分解成更小的构建单元以简化实现。
类型的可见性
默认的可见性为internal,如果不加修饰词,就意味着可见性为internal。
友元程序集可以使不同项目团队定义的程序集间互相访问internal的类型。
成员的可访问性
定义类型的成员(包括嵌套类型)时,可指定成员的可访问性。在代码中引用成员时,成员的可访问性指出引用是否合法。clr自己定义了一组可访问性修饰符,但每种编程语言在向成员应用可访问性时,都选择了自己的一组术语以及相应的语法。
下表总结了6个用于成员的可访问性修饰符。从第一行到最后一行,按照从限制最大到限制最小的顺序排列。
静态类
有一些永远不需要实例化的类,例如Console,Math,Environment和ThreadPool。这些类只有static成员。事实上,这种类唯一的作用就是组合一组相关的成员。例如,Math类就定义了一组执行数学运算的方法。在C#中,要用static关键字定义不可实例化的类。
c#编译器对静态类进行了如下限制。
1 静态类必须直接从基类system.object派生,从其他任何基类派生都没有意义。继承只适用于对象,而你不能创建静态类的实例。
2 静态类不能实现任何借口,这是因为只有使用类的实例时,才可调用类的接口方法。
3 静态类只能定义静态成员(字段、方法、属性和事件),任何实例成员都会导致编译器报错。
4 静态类不能作为字段、方法参数或局部变量使用,因为它们都代表引用了实例的变量,而这是不允许的。编译器检测到任何这样的用法都会报错。
使用关键字static定义类,将导致C#编译器将该类标记为abstract和sealed。另外,编译器不再类型中生成实例构造器方法(.ctor)。
分部类、结构和接口
partial关键字告诉C#编译器:类、结构或接口的定义源代码可能要分散到一个或多个源代码文件中。这种分散的原因有三
1 源代码控制
假定类型定义包含大量源代码,一个程序员把他从源代码控制系统签出已进行修改。没有其他程序员能同时修改这个类型,除非之后执行合并。使用partial关键字可将类型的代码分散到多个源代码文件中,每个文件都可单独签出,多个程序员能同时编辑类型。
2 在同一个文件将类或结构分解成不同的逻辑单元
3 代码拆分
组件、多态和版本控制
组件软件编程是OOP发展到极致的成功,下面列举组件的一些特点。
1 组件(.NET Framework称为程序集)有已发布的意思
2 组件有自己的标识(名称、版本、语言文化和公钥)
3 组件永远维持自己的标识(程序集中的代码永远不会静态链接到另一个程序集中,.NET总是使用动态链接)
4 组件清楚指明它所依赖的组件(引用元数据表)
5 组件应编档它的类和成员。C#语言通过源代码内的XML文档和编译器的/doc命令行开关提供这个功能。
6 组件必须制定它需要的安全权限。CLR的代码访问安全性(code access security)机制提供这个功能。
7 组件要发布在任何“维护版本”中都不会改变的接口(对象模型)。
将一个组件(程序集)中定义的类型作为另一个组件(程序集)中的一个类型的基类使用时,变回发生版本控制问题。
c#提供了5个能影响组件版本控制的关键字,可将它们应用于类型以及类型的成员。这些关键字直接对应clr用于支持组件版本控制的功能。
clr如何调用虚方法、属性和事件
本方法代表在类型或类型的实例上执行某些操作的代码。在类型上执行操作,称为静态方法;在类型的实例上执行操作,称为非静态方法。所有方法都有名称、签名和返回类型(可为void)。clr允许类型定义多个同名方法,只要每个方法都有一组不同的参数或者一个不同的返回乐行。所以,完全能定义两个同名、同参数的方法,只要两者返回类型类型不同。但除了IL汇编语言,大多数语言(包括c#)在判断方法的唯一性时,除了方法名之外,都只以参数为准,方法返回类型会被忽略。
一下employee类定义了3种不同的方法
Internal class Employee{ //非虚实例方法 public int32 GetYearsEmployed(){………} //非虚实例方法 public virtual string GetProgressReport(){………} //非虚实例方法 public static Employee Lookup(string name){………} }
编译上述代码,编译器会在程序集的方法定义表中写入3个记录向,每个记录项都用一组标志(flag)指明方法时实例方法、虚方法还是静态方法。
写代码调用这些方法,生成调用代码的编译器会检查方法定义的标志(flag),判断应如何生成il代码来正确调用方法。clr提供两个方法调用指令。
Call
改il指令可调用静态方法、实例方法和虚方法。用call指令调用静态方法,必须指定方法的定义类型。用call指令调用实例方法或虚方法,必须指定引用了对象的变量。call 指令鉴定该变量不为null。换言之,变量本身的类型指明了方法的定义类型。如果变量的类型没有定义该方法,就检查基类型来查找匹配方法。call指令经常用于以非虚方式调用虚方法。
callvirt
该il指令可调用实例方法或虚方法,不能调用静态方法。与call命令的区别是,callvirtual会先检查调用变量是否为空,所以执行速度比call稍慢。
编译器有时用call而不是callvirt调用虚方法,比如有时候子类使用base.tostring(),这是c#编译器生成call指令来确保以非虚方式调用基类的tostring方法。如果以虚方式调用tostring,调用会递归执行,直至线程栈溢出,这显然不是预期的。
无论用call还是callvirt调用实例方法或虚方法,这些方法通常接收隐藏this实参作为方法第一个参数。this实参引用要操作的对象。
设计类型时应尽量减少虚方法数量。首先,调用虚方法的速度比调用非虚方法慢。其次,jit编译器不能内嵌(inline)虚方法,这进一步影响性能。第三。虚方法使组件版本控制变得更脆弱。第四,定义基类型时,经常要提供一组重载的简便方法。如果希望这些方法时多态的,最好的办法就是使最复杂的方法称为虚方法,使所有重载的简便方法称为非虚方法。
合理使用类型的可见性和成员的可访问性
C#编译器在内的许多编译器都默认生成非密封类,只允许开饭人员使用关键字sealed将类显式标记为密封。我认为现在的编译器使用了错误的默认设定。密封类之所以比非密封类更好,有以下三个方面的原因
1 版本控制
如果类最初密封,将来可在不破坏兼容性的前提下更改为非密封。但如果最初非密封,将来就不可能更改为密封,因为这将中断派生类。
2 性能
调用虚方法在性能上不及调用非虚方法,因为clr必须在运行时查找对象的类型,判断要调用的方法由哪个类型定义。但是,如果jit编译器看到使用密封类型的虚方法调用,就可采用非虚方式调用虚方法。
3 安全性和可预测性
类必须保护自己的状态,不允许被破坏。当类处于非密封状态时,只要它的任何数据字段或者在内部对这些字段进行处理的方法时可以访问的,而不是私有的,派生类就能访问和更改基类的状态。
以下是我自己定义类时遵循的原则。
1 定义类时,除非确定要将其作为基类,并允许派生类对它进行特化,否则总是显式地指定为sealed类。如前所述,这与c#以及其他许多编译器的默认方式相反。另外,我默认将类指定为internal类,除非我希望在程序集外部公开这个类。如果我真的要定义一个可由其他人继承的类,同时不希望允许特化,那么我会重写并密封继承的所有虚方法(sealed override)。
2 类的内部,我总是毫不犹豫地将数据字段定义为private。幸好,c#默认就将字段标记为private。
3 oop有一条古老的格言,大意是当事情变得过于复杂时,就搞更多的类型出来。 当算法的实现开始变得复杂时,我会定义一些辅助类型来封装独立的功能。
常量
常量是值从不变化的符号。定义常量符号时,它的值必须能在编译时确定。确定后,编译器将常量值保存到程序集的元数据中。这意味着只能定义编译器识别的基元类型的常量。·····
由于常量值从不变化,所以常量总是被视为类型定义的一部分。换而言之,常量总是被视为静态成员,而不是实例成员。定义常量将导致创建元数据。
代码引用常量符号时,编译器在定义常量的程序集的元数据中查找该符号,提取常量的值,将值签入生成的il代码中。由于常量的值直接嵌入代码,所以在运行时不需要为常量分配任何内存。除此之外,不能获取常量的地址,也不能以传引用的方式传递常量。这些限制意味着常量不能很好地支持跨程序集的版本控制。因此,只有确定一个符号的值从不变化才应定义常量。如果希望在运行时从一个程序集中提取另一个程序集的值,那么不应该使用常量,而应该使用readonly字段。
字段
字段是一种数据成员,其中容纳了一个值类型的实例或者一个引用类型的引用。下表总结了可应用于字段的修饰符。
如上表所示,clr支持类型(静态)字段和实例(非静态)字段。如果是类型字段,容纳字段数据所需的动态内存是在类型对象中分配的,而类型对象是在类型加载到一个AppDomain时创建的。那么,什么时候讲类型加载到一个AppDomain中呢?这通常是在引用了该类型的任何方法首次进行jit编译的时候。如果是实例字段,容纳字段数据所需要的动态内存是在构造类型的实例时候分配的。
由于字段存储在动态内存中,所以他们的值在运行时才能获取。字段还解决了常量存在的版本控制问题。此外,字段可以是任何数据类型,不像常量那样仅仅局限于编译器内置的基元类型。
clr支持readonly字段和read\write字段。大多数字段都是read\write字段,意味着在代码执行过程中,字段值可以多次改变。但readonly字段只能在构造器方法中写入。(构造器方法只能调用一次,即对象首次创建时)编译器和验证机制确保readonly字段不会被构造器意外的任何方法写入。注意,可利用反射修改readonly字段。