- 常量 常量是在编译时设置其值并且永远不能更改其值的字段。使用常量可以为特殊值提供有意义的名称以代替数字文本,以使代码变得更容易阅读及维护。定义常量请使用关键字const。private const Int32 SomeConstant = 1;
- 字段 字段存储着类满足其设计所必须拥有的数据。例如,表示日历日期的类可能有三个整数字段:一个表示月份,一个表示日期,还有一个表示年份。强烈建议将字段声明为私有字段,防止类型的状态被该类型外部的代码破坏,外部访问字段应通过属性或方法来进行。private readonly Int32 SomeReadInlyField = 2; //只读字段
- 实例构造器 用于创建和初始化实例。创建新对象时将调用类构造函数。static SomeType() { }
- 类型构造器 类型构造器是 static方法,不能带任何参数。public SomeType(Int32 x) { }
- 方法 是通过指定访问级别、返回值、方法名称和任何方法参数在类或结构中声明的。这些部分统称为方法的“签名”。 方法参数括在括号中,并用逗号隔开。空括号表示方法不需要参数。作用于类型时,称为静态方法;作用于实例时,称为实例方法。方法一般会对类型或对象的字段执行读写操作。
- 操作符重载 它实际上是一个方法,定义了将一个特定的操作符作用于对象时,应当如何操作。
- 转换操作符 定义如何隐式或者显式地将对象从一种类型转换为另一种类型的方法。
- 属性 利用属性,可以使用一种简单的、字段风格的语法来设置或查询类型或对象的部分逻辑状态。它可以是没有参数的,也可以是有参数的。pulict Int32 this[String s]{ get;set;}
- 事件 利用事件,可以向一个或多个静态或实例方法发送通知。事件包含两个方法,用于登记或者注销对该事件的关注(+=/-=)。事件通常使用一个委托类型来维护可登记的方法。public event EventHandler SomeEvent;
- 类型 类型可定义嵌套于其中的其他类型(内部类,嵌套类)。通常用这种方式将一个大且复杂的类型分解成较小的类型,以简化开发。
C#中的访问修饰符 internal 可以说是介于 Public 和 Private 之间,可以使类型在同程序集中可以被互相访问。但有时会有这样的需求,我们希望一个程序集中的类型可以被外部的某些程序集可以访问,这时当然不能设置成 Public,否则可以被所有的外部程序集访问。要达到上述要求我们可以使用友元程序集。
使用 System.Runtime.CompilerServices 命名空间构建一个 [assembly: InternalsVisibleTo("TestA")] 的 attribute 标明它认为的“友元”的其他程序集。
注意,一个程序集在确认了自己的友元程序集之后,那些友元程序集就能访问该程序集所有 internal 类型和成员。
提示:“友元程序集”只适用于发布时间相同的程序集,最好打包一起发布。因为如果错开时间太久,就可能导致兼容性问题、如果程序集必须在不同的时间发布,应尝试设计 public 类。
CLR术语 | C#术语 | 描述 |
---|---|---|
private | private | 只限于包含类型 |
Family | protected | 受保护的,子类能继承,其他类不能直接访问 |
Family and Assembly | (不支持) | 成员只能由定义类型、任何嵌套类型或者同一个程序集中定义的任何派生类型中的方法访问 |
Assembly | internal | 只限于当前程序集 |
Family or Assembly |
protected internal |
同一个程序集内部的子类可以继承,其他类不能访问 |
public | public | 不受限制 |
在C#中,如果没有显示声明成员的可访问性,编译器通常(但不是总是)默认选择 private。
CLR要求接口类型的所有成员都具有 public 可访问性。禁止开发人员显示指定。
static 修饰符用于标记声明为静态类 (static class) 的类。静态类不能实例化,不能用作类型,而且仅可以包含静态成员。只有静态类才能包含扩展方法的声明。
静态类声明受以下限制:
• 静态类不能包含 sealed 或 abstract 修饰符。但是,注意,因为无法实例化静态类或从静态类派生,所以静态类的行为就好像既是密封的又是抽象的。
• 不能显式指定基类或所实现接口的列表。静态类隐式从 object 类型继承。
• 静态类只能包含静态成员。注意,常量和嵌套类型归为静态成员。
• 静态类不能含有声明的可访问性为 protected 或 protected internal 的成员。
partial 这个关键字告诉C#编译器,一个类、结构或者接口的定义源代码可能要分散到一个或者多个源文件中。
组件软件编程(Component Software Programming,CSP)正是OOP(面向对象编程)发展到极致的一个成果。
- 组件(.NET中称为程序集或)有“已经发布”的意思。
- 组件有自己的标识(名称、版本、语言文化和公钥)。
- 组件永远维持自己的标识。
- 组件清楚指明它所依赖的组件(引用元数据表)。
- 组件要文档花它的类和成员。C#语言通过源代码内的 XML 文档和编译器的 /doc 机制提供这个功能。
- 组件要发布一个在任何“维护版本”中都不会改变的接口(对象模型)。“维护版本”代表组件的新版本,向后兼容。
在.NET中,版本号包含4个部分:
主版本号(major version)、次版本号(minor version)、内部版本号(build number)和修订号(revison)。
major/minor 部分代表程序集的一个连续的、稳定的功能集,而 build/revison 部分通常代表这个功能的一次维护。
C#提供了5个能影响组件版本控制的关键字
C#关键字 | 类型 | 方法/属性/事件 | 常量/字段 |
---|---|---|---|
abstract | 表示不能构造该类型的实例 | 表示为了构造派生类型的实例,派生类型必须重写并实现这个成员 | (不允许) |
virtual | (不允许) | 表示这个成员可由派生类型重写 | (不允许) |
override | (不允许) | 表示派生类型重写了基类型的成员 | (不允许) |
sealed | 表示该类型不能用作基类型 | 表示这个成员不能被派生类型重写,只能将该关键字应用于准备重写一个虚方法 | (不允许) |
new |
应用于嵌套类型、方法、属性、事件、常量或字段时,表示该成员与基类中相似的成员无任何关系。 |
6.6.1CLR如何调用虚方法、属性和事件
在类型上执行的操作,称为静态方法,在类型的实例上执行操作,称为非静态方法。
call
这个 IL 指令调用静态方法、实例方法和虚方法。用 call 指令调用静态方法时,必须指定是哪个类型定义了要由 CLR 调用的方法。用 call指令调用实例方法或虚方法时,必须指定引用了一个对象的变量。 call 指令假定变量不为 null 。换言之,变量本身的类型指明了要由 CLR 调用的方法是哪个类型中定义的。如果变量的类型没有定义该方法,就检查基类型来查找匹配的方法,call 指令经常用于以非虚的方式调用一个虚方法。
callvirt
这个 IL 指令可调用实例方法和虚方法,不能调用静态方法。用 callvirt 指令调用实例方法和虚方法时,必须指定引用一个对象的变量。调用非虚实例方法时,变量的类型指明了最终由 CLR 调用方法是哪个类型中定义的。用 callvirt 调用虚实例方法时, CLR 会调查发出调用的那个对象的实际类型,然后以多台方式调用方法。为了确定类型,用来发出调用的变量绝对不能是 null。如果是, callvirt 会造成 CLR 抛出一个 NullReferenceException 异常。
static void Main(string[] args)
{
Console.WriteLine(); //调用一个静态方法 public static void WriteLine();
Object o = new Object();
o.GetHashCode(); //调用一个虚实例方法 public virtual int GetHashCode();
o.GetType(); //调用一个非虚实例方法 public Type GetType();
}
IL代码
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] object obj2)
L_0000: nop
L_0001: call void [mscorlib]System.Console::WriteLine() //call指令调用了Console的writeLine方法
L_0006: nop
L_0007: newobj instance void [mscorlib]System.Object::.ctor()
L_000c: stloc.0
L_000d: ldloc.0
L_000e: callvirt instance int32 [mscorlib]System.Object::GetHashCode() //使用callvirt调用GetHashCode
L_0013: pop
L_0014: ldloc.0
L_0015: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType() //callvirt调用GetType
L_001a: pop
L_001b: ret
}
C#使用 callvirt 调用 GetType 方法,有店出乎意料,因为 GetType 并不是虚方法。这个因为CLR知道 GetType 不是虚方法,所以在JIT编译好的代码中,会直接以非虚的方式调用 GetType 。
为什么 C#编译器不干脆生成 call 指令?C#工作组任务JIT编译器应生成代码来核实发出调用的对象不为 null。意味着对非虚实例方法调用要慢一点。
以下C#代码抛出一个 NullReferenceException 异常。另外一些编程语可能可以正常工作。
public sealed class Program
{
public Int32 GetFive() { return 5; }
static void Main(string[] args)
{
Program p = null;
Int32 x = p.GetFive(); //“System.NullReferenceException”类型的未经处理的异常
}
}
理论上来说,上面代码没有问题,变量 p 的确为 null 。但在调用一个非虚方法(GetFive)时,CLR唯一需要知道的就是 p 的数据类型(Program)。如果 GetFive 真的获得调用,this实参值将为 null。由于没有在 GetFive 方法内部使用这个实参,所以不会抛出 NullReferenceException 异常。但是C#编译器生成一个 callvirt 指令,而不是 call 指令,所以会抛出异常而结束。
有时候,编译器会使用 call 而不是 calvirt 来调用虚方法,
internal class SomeClass
{
public override string ToString()
{
//编译器会使用IL指令“call”
//以非虚方式调用 object的 Tostring方法
//如果使用'callvirt'而不是'call'
//那么该方法将递归地调用其本身,直至栈溢出
return base.ToString();
}
}
调用 base.ToString 时(一个虚方法),C#编译器会生成一个 call 指令确保以非虚方式调用基类型中的 ToString 方法。这是必要的,因为如果以虚方法调用 ToString,调用会递归执行,直至线程栈溢出,这显然不是你期望的。
设计一个类型时,尽量减少定义的虚方法数量
- 首先是速度慢,
- 其次 JIT 编译器不能内嵌虚方法。
- 虚方法使组件的版本控制变得更脆弱。
- 定义一个基类型,经常用到重载的简便方法,如果希望这些方法是多态,最好的办法是使最复杂的方法成为虚方法,简便的方法为非虚方法
public class Set
{
private Int32 m_length = 0;
public Int32 Find(Object value)
{
return Find(value, 0, m_length);
}
public Int32 Find(Object value, Int32 startIndex)
{
return Find(value, startIndex, m_length - startIndex);
}
public virtual Int32 Find(Object value, Int32 startIndex, Int32 endIndex)
{
//可被重写的实现放这里
}
//其他方法放这里
}