类型的各种成员
类型的成员有:常量、字段、实例构造器、类型构造器、方法、操作符重载、转换操作符、属性、事件、类型
以下C#代码展示了一个类型定义,其中包含所有可能的成员。
public sealed class SomeType{ //嵌套类 private class SomeNestedType { } //常量、只读和静态可读/可写字段 private const int constantField = 1; private readonly String readOnlyField = "2"; private static int staticField = 3; //类型构造器 static SomeType() { } //实例构造器 public SomeType() { } public SomeType(int x) { } //实例方法和静态方法 private String InstanceMethod() { return null; } public static void Main() { } //实例属性 public int SomeProp{ get; set; } //实例有参属性(索引器) public int this[String s]{ get; set;} //实例事件 public event EventHandler SomeEvent; }
类型的可见性
定义文件范围的类型时,可将类型的可见性指定为public或internal。
public类型不仅对定义程序集中的所有代码可见,还对其他程序集中的代码可见。
internal类型则仅对定义程序集中的所有代码可见,对其他程序集中的代码不可见。
定义类型时不显式指定可见性,C#编译器会帮你指定为internal。
//public即可有本程序集也可由其他程序集的代码访问 public class ThisIsAPublicType{} //internal,只可有本程序集的代码访问 internal class ThisisAnInternalType{} //没有显式声明,默认为internal class ThisIsAlsoAnInternalType{}
成员的可访问性
成员的可访问性按照限制最大到限制最小排列:
private,成员只能由定义类型或任何嵌套类型中的方法访问
protected,成员只能由定义类型,任何嵌套类型或者不管在什么程序集中的派生类型中的方法访问
internal,成员只能由定义程序集中的方法访问
protected internal,成员可由任何嵌套类型、任何派生方法(不管在什么程序集)或者定义程序集的任何方法访问
public,成员可由任何程序集的任何方法访问
成员默认是private
如果没有显式声明成员的可访问性,编译器通常默认选择private。
接口成员默认是public
CLR要求接口类型的所有成员都具有public可访问性,因此编译器自动将所有接口成员的可访问性设为public。
原始成员与重写成员必须具有相同的可访问性
派生类型重写基类型定义的成员时,C#编译器要求原始成员与重写成员具有相同的可访问性。
合理使用类型的可见性和成员的可访问性
默认为密封类的优势
定义新类型时,编译器应默认生成密封类,使他不能作为基类使用。但是包括C#编译器在内的许多编译器都默认生成非密封类。
密封类之所以比非密封类好,有以下三个方面的原因
①版本控制
②性能
③安全性和可预测性
定义类时遵循的原则
显式指定类为sealed
定义类时,除非确定要将其作为基类,并允许派生类对他进行转化,否则总是显式地指定为sealed。类默认为internal。
字段定义为private
类的内部,将数据字段定义为private。
避免将成员定义为protected或internal
在类的内部,将自己的方法属性事件定义为private和非虚。当然也会将某个定义为public,一边公开类型的某些功能。
尽量避免上述任何成员定义为protected或internal,因为这使类型面临更大的安全风险。迫不得已,也会尽量选择protected或internal。
virtual永远最后才考虑,因为虚成员会放弃许多控制,丧失独立性,变得彻底依赖于派生类的正确行为。
定义辅助类封装独立的功能
当算法的实现开始变复杂时,定义一些辅助类型来封装独立的功能。
如果定义的辅助类型只有一个超类型使用,就在超类型中嵌套这些辅助类型。
这样除了可以限制范围,还允许嵌套的辅助类型中的代码引用超类型中定义的私有成员。
VS的代码分析工具强制执行了一条设计规则,即对外公开的嵌套类型必须在文件或程序集范围中定义,不能在另一个类型中定义。
因为一些开发人员觉得引用嵌套类型时,所用的语法过于繁琐。
静态类
有一些永远不需要实例化的类,例如Console,Math等。这些类只有static成员。
事实上,这种类的唯一作用就是组合一组相关的成员。例如Math类就定义了一组执行数学运算的方法。
在C#中,要用static关键字定义不可实例化的类。该关键字只能用于类,不能用于结构(值类型)。因为CLR总是允许值类型实例化,这是没办法阻止的。
静态类的限制
①静态类直接从基类System.Object派生,从其他任何基类派生都没有意义。
②静态类不能实现任何接口,只有使用类的实例时,才可调用类的接口方法。
③静态类只能定义静态成员(字段,方法,属性,事件),任何实例成员都会导致编译器报错。
④静态类不能作为字段、方法参数或局部变量使用,因为他们都代表引用了实例的变量,不允许,会报错。
下面是一个定义了静态成员的静态类。代码虽然能通过编译,有一个警告,但该类没有做任何有意义的事情。
public static class AStaticClass { public static void AStaticMethod(){} public static String AStaticProperty(){ get{return s_AStaticField;} set{s_AStaticField=value;} } private static String s_AStaticField; public static event EventHandler AStaticEvent; }
使用关键字static定义类,将导致C#编译器将该类标记为abstract和sealed。编译器不在类型中生成实例构造器方法。
分部类
partial关键字告诉C#编辑器:类、结构或接口的定义源代码可能要分散到一个或多个源代码文件中。
将类型源代码分散到多个文件的原因有三。
源代码控制
使用partial关键字可将类型的代码分散到多个源代码文件中,每个文件都可以单独签出,多个程序员可以同时编辑类型。
在同一个文件中将类或结构分解成不同的逻辑单元
创建一个类型提供多个功能,使类型能提供完整解决方案。为
了简化实现,有时会在一个源代码文件中重复声明同一个分部类型。
然后分部类型的每个部分都实现一个功能,并配以他的全部字段、方法、属性和事件等。
这样就可以方便地看到组合以提供一个功能的全体成员,从而简化编码。
代码拆分
常量
基元类型与非基元类型常量
基元类型常量
常量是值从来不变化的符号。定义常量符号时,他的值必须能在编译时确定。
确定后,编译器将常量值保存在程序集元数据中。这意味着只能定义编译器识别的基元类型的常量。
非基元类型常量
C#也允许定义非基元类型的常量变量constant variable,前提是把值设为null。
public sealed class SomeType{ //SomeType不是基元类型但C#允许置为null的这种;类型的常量变量 public const SomeType Empty=null; }
常量的值直接嵌入代码
常量值从不变化,所以常量总是被视为静态成员,而不是实例成员。
定义常量将会创建元数据。代码引用常量符号时,编译器在定义常量的程序集的元数据中查找该符号,提取常量的值,将值嵌入生成的IL代码中。
所以在运行时不需要为常量分配任何内存。除此之外,不能获取常量的地址,也不能以传引用的方式传递常量。
这些限制意味着常量不能很好的支持跨程序集的版本控制。因此只有确定一个符号的值从不变化才定义常量。
如果希望运行时从一个程序集中提取另一个程序集中的值,那不应该使用常量,而应该使用readonly字段。
字段
字段是一种数据成员,其中容纳了一个值类型的实例或者对一个引用类型的引用。
字段的内存分配
CLR支持类型字段和实例字段。如果是类型字段,容纳字段数据所需的动态内存是在类型对象中分配的。
通常是在引用了该类型的任何方法首次进行JIT编译的时候,将类型加载到一个AppDomain时,创建类型对象。
如果是实例字段,容纳字段数据所需的动态内存是在构造类型的实例时分配。
由于字段存储在动态内存中,所以他们的值在运行时才能获取。
字段解决了常量存在的版本控制问题。而且字段可以是任何数据类型,不像常量仅仅局限于编译器内置的基元类型。
只读字段和可读写字段
CLR支持只读字段和可读写字段。
可读写意味着在代码执行过程中,字段值可多次改变。只读字段只能在构造器方法中写入。
编译器和验证机制确保只读字段不会被构造器以外的任何方法写入。不过可利用反射来修改只读字段。
以常量的代码为例,可以使用一个静态只读字段代替常量来修正版本控制问题。
public sealed class SomeLibraryType{ public static readonly Int32 MaxEntriesInList=50; }
假设DLL程序集到开发人员将50修改为100,并重新生成程序集。
当应用程序代码重新执行时,她将自动提取字段到新值100.应用程序不需要重新生成就可以直接运行。
引用类型只读字段
当某个字段是引用类型,并且该字段被标记为readonly时,不可改变的是引用,而非字段引用的对象。
public sealed class AType{ public static readonly Char[] InvalidChars=new Char[]{'A','B','C'}; } public sealed class AnotherType{ public static void M(){ //下面三行代码是合法的,可以通过编译 AType.InvalidChars[0]='X'; AType.InvalidChars[1]='Y'; AType.InvalidChars[2]='Z'; //下面一行代码是非法的,无法通过编译 //因为不能让InvalidChars引用别的东西 AType.InvalidChars=new Char[]{'X','Y','Z'}; } }