类型的各种成员
- 常量
- 是指出
数据值恒定不变
的符号, 总与类型关联,不与类型实例关联. 逻辑上总是静态成员.
- 是指出
- 字段
- 表示只读或可读/可写的数据值.
- 字段可以是静态的,这种被认为是
类型
状态的一部分. - 字段也可以是实例非静态,这种被认为是
对象
状态的一部分.
- 实例构造器
- 将
新对象的实例字段
初始化的特殊方法.
- 将
- 类型构造器
- 将
类型的静态字段
初始化的特殊方法.
- 将
- 方法
- 是更改或查询类型或对象状态的函数.
- 作用于类型称为静态方法.
- 作用于对象称为实例方法.
- 操作符重载
- 实际上是方法.定义了操作符用于对象时应该如何操作该对象. 不是所有编程语言都支持,所以不属于CLS.
- 转换操作符
- 定义如何隐式或显示将对象从一种类型转换为另一种类型的方法.不是所有编程语言都支持,所以不属于CLS.
- 属性
- 允许用简单的字段风格的语法设置或查询类型或对象的逻辑状态,同时保证状态不被破坏.
- 作用于类型的称为静态属性.
- 作用于对象的称为实例属性.
- 属性可以无参(非常普遍)
- 多个参数(集合类用得多)
- 事件
- 静态事件允许
类型
向一个或多个静态或实例方法
发送通知. - 非静态(实例)事件允许
对象
向一个或多个静态或实例方法发送通知. - 事件包含两个方法:允许静态或实例方法
登记
或注销
对该事件的关注. - 通常还用一个
委托字段
来维护已登记
的方法集
- 静态事件允许
类型
- 可以定义其他嵌套
类型
(内/外部类
).
元数据是整个Microsoft .Net Framework开发的关键 .元数据实现了编程语言,类型和对象的无缝集成.
- 可以定义其他嵌套
源代码中定义的所有成员都造成编译器生成元数据.有些数据还造成编译器生成额外的成员和额外的元数据.
public sealed class SomeType // 1
{
// 嵌套类
private class SomeNestedType { } // 2
// 常量
private const Int32 c_SomeConstant = 1; // 3
// 只读
private readonly String m_SomeReadOnlyField = "2"; //4
// 静态可读/可写字段
private static Int32 s_SomeReadWriteField = 3; // 5
// 类型构造器
static SomeType(){} // 6
// 实例构造器
public SomeType(Int32 x){} // 7
public SomeType(){} // 8
// 实例方法和静态方法
private String InstanceMethod(){ return null; } // 9
public static void Main(){} // 10
// 实例属性
public Int32 SomeProp // 11
{
get { return 0;} // 12
set { } // 13
}
// 实例有参属性
public Int32 this[String s] // 14
{
get { return 0; } // 15
set { } // 16
}
// 实例事件
public event EventHandler SomeEvent; // 17
例如: 事件成员(17)造成编译器生成
- 一个字段
- 两个方法
- 事件(一些额外的元数据)
类型的可见性
- public 类型:不仅对
定义程序集
中的所有代码可见,还对其他程序集
中的代码可见. - internal 类型: 仅对
定义程序集
中的所有代码可见. 对其他程序集代码不可见.
定义类型时如果不显示指定可见性,C#编译器会指定为internal
.
什么情况下要友元程序集?
假设下述情形:
一个公司有A团队和B团队, A团队定义了一组工具类,并希望团队使用这些类型. 但是他们不能将所有类型都生成到一个程序集中,而都要生成自己的程序集.
为了使B团队使用A的工具类,A团队必须将工具类定义为public
. 这样意味着工具类对所有程序集公开,连C团队也能使用. 这不是A团队所希望的.
为了做到用internal
定义工具类,同时B团队也能访问,C#可以通过友元程序集
功能实现.
示例做法
- 生成程序集时,用System.Runtime.CompilerServices命名空间下的InternalsVisibleTo特性,标明它认为是友元的其他程序集.
- 传入标识 友元程序集名称 和 公钥的字符串参数(不能包含版本,语言文化和处理器架构)
- 之后被标注的程序集能访问该程序集的 所有internal类型,以及 这些类型的internal成员.
using System.Runtime.CompilerServices;
// 这里指定Wintellect和Microsoft是友元程序集
[Assembly:InternalsVisibleTo("Wintellect, Publickey=12314..sbasd")]
[Assembly:InternalsVisibleTo("Microsoft, Publickey=asdas...basd")]
internal sealed class SomeInternalType{...}
internal sealed class AnotherSomeInternalType{...}
C#编译器在编译友元程序集(不含InternalsVisibleTo特性的程序集)要求使用/out:
开关, 因为编译器要知道准备编译的程序集的名称,从而判断生成的程序集是不是友元程序集.(意思是编译Wintellect和Microsoft程序集时,在代码编译结束前,C#编译器是不知道输出文件名的,因此使用/out: 能极大增强编译性能.) 使用/t:module开关来编译模块,而且该模块还是友元程序集的一部分,那就需要C#编译器使用/moduleassemblyname:
开关来编译该模块.
成员的可访问性
每种编程语言都有自己的一组术语以及相应的语法. CLR自己定义了一组可访问性修饰符.
例如:
- CLR使用
Assembly
表明成员对同一程序集内的所有代码可见. - C#对应的术语是
internal
表中总结了6个应用于成员的可访问性修饰符. 从第一行到最后一行按照限制最大到限制最小的顺序排列.
private
:只能由定义类型
和任何嵌套类型
中的方法访问protected
: 能被定义类型的方法
,任何嵌套类型中的方法
和无论在什么程序集中
的派生类型中的方法
访问.internal
: 只能被当前定义的程序集中
的方法访问. (比public限制高,限制了程序集)protected internal
:能被任何内/外部类(嵌套类)
,无论在什么程序集中的派生类型
,当前程序集中的任何方法
访问.public
: 能被任何程序集中的任何方法
访问.
在C#中,如果没有显示声明成员的可访问性,编译器通常默认选择private.
CLR 要求接口类型的所有成员都具有public可访问性. C#编译器因此禁止开发人员显示指定接口成员的可访问性.自动设为public.
C#限制派生类重写基类方法需要具有相同的可访问性.
CLR允许放宽限制
,但不允许收紧限制
(基类protected,派生类可以public,但不可以是private). 否则就无法派生类转为基类时,获取不到基类方法的访问权.
静态类
例如Console,Math,Environment,ThreadPool类,永远不需要实例化的类,只有static成员.
static
关键字定义不可以实例化的类. 只能应用于类,不能应用于结构(值类型). CLR总是允许值类型实例化.
C#编译器对静态类做了如下限制
- 静态类必须直接从基类System.Object派生. 继承只适用于对象,静态类不能创建实例.因此从其他任何积累下派生都没有意义.
- 静态类不能实现任何接口. 只有使用类的实例时,才能调用类的接口方法.
- 静态类只能定义静态成员(字段,方法,属性和事件).
- 静态类不能作为字段,方法参数或局部变量使用. 因为它们都代表引用实例的变量.
IL会将static定义类标记为absrtact和sealed. 并且不生成实例构造器方法.
分部类,结构和接口
partial
关键字告诉C#编译器: 类,结构或接口的定义源代码可能要分散到一个或多个源代码文件中.
- 源代码控制
- 多个程序员修改一个类,使用
partial
为多个文件后,每个文件可以单独签出,多个程序员能同时编辑类型.
- 多个程序员修改一个类,使用
- 在同一个文件中将类或结构分解不同的逻辑单元
- 有时会创建一个类型提供多个功能,为简化实现,会在源代码中重复声明一个分部类型,分部类型的每个部分都实现一个功能.也可以完整的删除一个功能.
- 代码拆分
要将partial
关键字应用于所有文件中的类型. 编译到一起时, 编译器会合并代码. 分部类功能完全由C#编译器实现. CLR对此一无所知.
C#关键字及其对组件版本控制的影响
这些关键字直接对应CLR用于支持组件版本控制的功能.
CLR如何调用虚方法,属性和事件
属性和事件实际作为方法实现.
方法是什么?
方法代表类型或类型上的实例上执行某些操作的代码. 在类型上操作是静态方法,在实例上操作是非静态方法.
CLR允许定义多个同名方法,根据返回类型不同或者参数不同
除了IL语言,包括C#大多数语言在判断方法的唯一性时,除了方法名之外,都只以参数为准,方法返回类型会被忽略.
- 非虚实例方法
public Int32 MethodA()
- 虚方法
public virtual String MethodB()
静态方法
public static Int32 MethodC()
编译这些方法,编译器会在程序集的方法定义表中写入记录项,每个记录项都用一组
flag标志
指明方法的类型.调用这些方法时, 生成调用代码的编译器会检查方法定义的
标志flag
,来生成对应的IL代码来调用.
callvirt和call指令调用非虚方法,虚方法,静态方法
CLR提供两个方法调用指令:
- call
- 用call指令调用静态方法,必须指定定义此静态方法的类型.
- 用call指令调用实例方法或虚方法,必须指定引用了对象的变量(就是调用方法的变量的定义类型).
- 如果调用方法的变量没有定义此方法,就会检查基类来查找匹配方法.
- 常用于以非虚方式调用虚方法.
- callvirt
- 用callvirt可调用实例方法和虚方法,不能调用静态方法.
- 必须指定引用了对象的变量.
- 用callvirt调用非虚方法,变量的类型指明了方法的定义类型.
- 用callvirt调用虚实例方法,CLR会调查发出的调用对象的实际类型,然后以多态方式调用方法.
- 为了确定类型,发出调用的变量不能为null. JIT编译器会生成代码来验证变量的值是不是null.(抛出NUllReferenceException异常)
- 用callvirt调用非虚实例方法也会执行这种null检查. 所以用callvirt指令执行速度比call指令稍慢.
// 调用静态方法
Console.WriteLine();
Object o = new Object();
// 调用虚方法
o.GetHashCode();
// 调用非虚方法
o.GetType();
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 28 (0x1c)
.maxstack 1
.locals init (object V_0)
IL_0000: nop
// 静态方法会使用call指令
IL_0001: call void [System.Console]System.Console::WriteLine()
IL_0006: nop
IL_0007: newobj instance void [System.Runtime]System.Object::.ctor()
IL_000c: stloc.0
IL_000d: ldloc.0
// 这里用callvirt 是因为GetHashCode是虚方法
IL_000e: callvirt instance int32 [System.Runtime]System.Object::GetHashCode()
IL_0013: pop
IL_0014: ldloc.0
// 这里用callvirt , Type是非虚方法,在JIT编译好的代码中会以非虚方式调用
// 为什么不用直接用call指令呢? 因为C#团队认为JIT编译器应生成代码来验证发出调用的对象不为null
IL_0015: callvirt instance class [System.Runtime]System.Type [System.Runtime]System.Object::GetType()
IL_001a: pop
IL_001b: ret
} // end of method Program::Main
如果调用非虚方法时, C#编译器生成的是callvirt指令,那么发出调用的对象如果为null就会抛出异常.
C#用callvirt
指令调用所有 实例方法. 用call
指令调用 静态方法.
不建议把非虚方法改为虚方法. 这是因为某些编译器会用call而不是callvirt指令调用非虚方法. 并且引用此方法的代码没有重新编译,就会造成应用程序行为无法预料. 用C#写的引用代码不会出问题,因为C#用callvirt指令调用所有实例方法.
编译器有时用call而不是callvirt调用虚方法.
特殊的语句:base.ToString();
public override String ToString()
{
// 编译器使用IL指令call
// 以非虚方式调用Object的ToString方法
// 如果编译器用callvirt而不是call
// 那么该方法将递归调用自身,直至栈溢出
return base.ToString();
}
// 如果编译器生成的是callvirt,那么是以虚方式(多态方式)调用, base.ToString还是会调用到重写的当前方法中
// 再次执行base.ToString导致循环
// call则是以非虚的方式调用,不会调用到重写的方法中,而是基类自己的ToString方法.
因为callvirt是以虚方式调用, 会导致base.ToString递归执行,直至线程栈溢出.
编译器调用值类型定义的方法时倾向于使用call指令,因为值类型时密封的,即使有虚方法也不用考虑多态性. 值类型的实例的本质保证它永不为null. 所以永远不抛出NullReferenceException异常.
如果以虚方式调用值类型的虚方法,CLR要获取对值类型的对象的引用,以便引用方法表.需要装箱.
call和callvirt的区别
call的callvirt的区别主要有两点:
call
可以调用 静态方法,实例方法和虚方法
call
一般是以 非虚的方式 来调用函数的.callvirt
不能调用 静态方法,只能调用实例方法和虚方法
,callvirt
是以 已多态(虚方式)的方式 来调用函数的.
设计类型时要注意
- 虚方法的调用速度比调用非虚方法慢.
- JIT编译器不能内嵌虚方法. 进一步影响性能
- 使最复杂(参数最多)的方法成为虚方法,使所有重载的简便方法成为非虚方法.
private Int32 m_lenth = 0;
// 重载的简便方法是非虚的
public Int32 Find(Object value)
{
return Find(value,0,m_lenth);
}
// 重载的简便方法是非虚的
public Int32 Find(Object value, Int32 startIndex)
{
return Find(value,0,m_lenth -startIndex);
}
// 功能最丰富的方法是虚方法,可以被重写
public virtual Int32 Find(Object value, Int32 startIndex, Int32 endIndex)
{
// 可被重写的实现写这里..
//.....
}
合理使用类型的可见性和成员的可访问性
默认生成的类是非密封里, 允许使用关键字sealed
将类型显示标记为密封.
- 性能更好
- 如果调用的是密封类的虚方法(C#编译器在生成callvirt指令), JIT会优化并生成代码用非虚方式调用.
调用虚方法在性能上不及调用非虚方法, 因为CLR必须在运行时查找对象的类型,判断要调用的方法由哪个类型定义.
如果JIT编译器看到使用的是密封类型的虚方法调用, 密封类不会有派生类,就可以采用非虚的方式调用虚方法(call指令),从而生成更高效的代码.
class Program
{
static void Main(string[] args)
{
Point p = new Point(3,4);
// C#编译器在此生成callvirt调用虚方法
// 但是JIT编译器会优化这个调用,并生成
// 代码来非虚地调用ToString,是因为Point是密封类
Console.WriteLine(p.ToString());
}
}
public sealed class Point
{
private Int32 m_x, m_y;
public Point(int mX, int mY)
{
m_x = mX;
m_y = mY;
}
public override string ToString()
{
return String.Format($"{m_x},{m_y}");
}
}
开发人员希望从现有类型派生出一个类,在其中添加额外字段或状态信息. 还希望在派生类中定义帮助方法(helper method)
或简便方法(convenience method)
来操作这些额外的字段. 如果是密封类就不合适,可以利用C#的扩展方法来模拟帮助方法
, 还可以利用ConditionalWeakTable类
模拟跟踪状态.