CLR - 设计类型

前言

  好记性不如烂“笔头”系列。。。

  目录

类型基础

“运行时”要求每个类型最终都从System.Object 类型派生。

  由于所有类型最终都从System.Object 派生,所以可以保证每个类型的每个对象都有一组最基本的方法。具体地说,System.Object 类型提供了下列的公共实例方法:

公共方法名称 说明
Equals 如果两个对象具有相同的值,就返回true。
GetHashCode 返回对象的值的一个哈希码。
ToString 该方法默认返回类型的完整名称(this.GetType().FullName)。然而,我们经常需要重写这个方法,使它返回一个String 对象,其中包含对象状态的一个表示。例如,核心类型(比如Boolean 和Int32)重写了这个方法,返回它们的值的一个字符串表示。
GetType 返回从Type 派生的一个对象的实例,指出调用GetType 的那个对象是什么类型。返回的Type 对象可以和反射类配合使用,从而获取与对象的类型有关的元数据信息。

  此外,从System.Object 派生的类型能访问下列所示的受保护方法:

受保护的方法名称 说明
MemberwiseClone 这个非虚方法能创建类型的一个新实例,并将新对象的实例字段设与this 对象的实例字段完全一致。返回的是对照新实例的一个引用。
Finalize 在垃圾回收器判断对象应该被作为垃圾收集之后,在对象的内存被实际回收之前,会调用这个虚方法。需要在回收之前执行一些清理工作的类型应该重写这个方法。

  要求所有对象都用new 操作符来创建。下面这行代码展示了如何创建一个class1 对象:

  Program p = new Program("info");

  以下是new 操作符所做的事情。

  1:计算类型及其所有基类型(一直至System.Object)中定义的所有实例字段需要的字节数。堆上的每个对象都需要一些额外的成员——即”类型对象指针“(type object pointer)和“同步块索引”(sync block index)来由CLR 用于管理对象,这些额外成员的字节数会计入对象大小。

  2:从托管堆中分配指定类型要求的字节数,从而分配对象的内存,分配的所有字节都设置为零(0)

  3:初始化对象的“类型对象指针”和“同步块索引”成员。

  4:调用类型的实例构造器,向其传入在对new 的调用中指定的任何实参(上例就是字符串"info")。大多数编译器都在构造器中自动生成代码来调用一个基类构造器。每个类型的构造器在调用时,都要负责初始化由这个类型定义的实例字段。最终调用的是System.Object 的构造器,该构造器只是简单地返回,不会做其他任何事情。

CLR 允许将一个对象转换为它的(实际)类型或者它的任何基类型。

  在C# 中进行类型转换可以使用is 与as 操作符。

  is 操作符检查一个对象是否兼容于指定的类型,并返回一个Boolean 类型:true 或false。注意,is 操作符永远不会抛出异常。

  Object o = new Object();

  Boolean b1 = (o is Object); //返回true

  as 操作符检查一个对象是否兼容于指定的类型,如果兼容,as 会返回对同一个对象的一个非null 引用,反之,则返回null。

  Object o = new Object();

  string str= o as string;//返回null

类型、对象、线程栈、托管堆在运行时的相互联系

  运行程序时,会启动一个进程,每个进程最少有一个线程。一个线程被创建时会分配到1MB 大小的栈。这个栈的空间用于向方法传递实参,并用于方法内部定义的变量。

  现在,Windows 进程已经启动,CLR 已经加载到其中,托管堆已初始化,而且已创建一个线程(连同它的 1MB 栈空间)。现在已经进入 Main() 方法,马上就要执行 Main 中的语句,所以栈和堆的状态如下图所示:

  当JIT 编译器将Main() 方法的IL代码转换成本地CPU 指令时,会注意到其内部引用的类型。这个时候,CLR 要确保定义了这些类型的所有程序集都已加载。然后利用程序集的元数据,CLR 提取与这些类型有关的信息,并创建一些数据结构来表示类型本身。在线程,会创建所需要的所有对象。下图显示了在Main 被调用时,创建类型对象后的状态:

  当CLR 确定方法需要的所有类型对象都已创建,而且Main 方法的代码编译之后,就允许线程开始执行Main 的本地代码。Main 的“序幕”代码执行时,必须在线程栈中为局部变量分配内存。同时作为“序幕”代码的一部分,CLR 会自动将所有变量初始化为null 或0(零)。

  然后Main 执行它的代码来构造一个Person 对象。这造成在托管堆中创建Person 类型的一个实例(也就是一个Person 对象),同时包含类型及其所有基类型(一直至System.Object)中定义的所有实例字段与同步块索引、类型对象指针。CLR 会自动先初始化同步块索引,并将对象的所有实例字段设为null 或0(零),再调用类型的构造器,最后返回Person 对象的内存地址,该地址保存在变量p 中。

  下一步Main 执行到ToString 方法。Jit 编译器在Person 类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT 编译(如果还没有编译过),再调用JIT 编译的代码。

  这时。可以看到Person 类型对象中也包括类型对象指针,这是因为类型对象本质上也是对象。CLR 创建类型对象时,必须初始化这些成员。CLR 开始在一个进程中运行时,会立即为MSCorLib.dll 中定义的System.Type 类型创建一个特殊的类型对象。Person 类型对象都是System.Type 类型对象的一个“实例”。因此,Person 类型对象指针成员会初始化成对System.Type类型对象的引用。因System.Type 类型对象本身也是一个对象,内部的“类型对象指针”成员会指向它本身。

基元类型、引用类型和值类型

编译器直接支持的类型称为基元类型(primitive type)。

C# 基元类型 FCL 类型 CLS 相容 说明
sbyte System.SBte NO 有符号8位值
byte System.Byte YES 无符号8位值
short System.Int16 YES 有符号16位值
ushort System.UInt16 NO 无符号16位值
int System.Int32 YES 有符号32位值
uint System.UInt32 NO 无符号32位值
long System.Int64 YES 有符号64位值
ulong System.UInt64 NO 无符号64位值
char System.Char YES 16位Unicode字符
float System.Single YES IEEE32位浮点值
double System.Double YES IEEE64位浮点值
bool System.Boolean YES 一个true/false值
decimal System.Decimal YES 一个128位高精度浮点值。常用于不容许舍入误差的金融计算。在128位中,有1位代表值的符号(正负号),有96位代表值本身,并有8位代表一个比例因子。比例因子用作96位整数的除数并指定整数的哪一部分为小数。比例因子隐式地定为数字10的幂,范围从0~28。其余位没有使用
string System.Strign YES 一个字符数组
object System.Object YES 所有类型的基类
dynamic System.Object YES 对于CLR,dynamic 和object 完全一致。然而,C# 编译器允许使用一个简单的语法,让dynamic 变量参与动态调度。

值类型与引用类型

  CLR 支持两种类型——值类型与引用类型。在 C# 中,值类型包括:结构(数值类型、bool类型、用户定义的结构)、枚举和可空类型。其它类型,均为引用类型。

  值类型与引用类型的区别如下:

   值类型  引用类型
 分配位置   栈  堆
 变量表示   值类型变量是局部复制的   引用类型变量指向被分配的对象的内存地址 
 基类  ValueType  Object
 是否可以继承   不能被继承(隐式密封)  可以被继承
 变量间的相互赋值    传值(生成副本,两个变量的值类型字段不会相互影响)  传址(不生成副本,两个变量之间相互影响) 存在内部引用被公开的风险
 是否需要终结器   不需要(允许定义终结器,但是值类型的一个已装箱 实例被 GC 回收时,CLR 不会调用该方法)  需要
 是否需要构造函数  需要(默认的构造函数被保留,作用是设置默认值,所以自定义构造函数必须是带参数的)  需要
 变量何时消亡  离开作用域时  被 GC 回收时
 生命周期是否可预测   可预测  不可预测
 默认值  0  null
 多线程同步  没有同步块,不能使用 Threading.Monitor 类型的各种方法(或者lock语句)让多线程同步实例  支持

  如果所有类型都是引用类型,程序性能会难以接受,所以 CLR 提供了“轻量级“的值类型。值类型的使用缓解了托管堆的压力,减少了垃圾回收次数。值类型(未装箱)比引用类型更轻量级的原因是:

  • 它们不分配在托管堆上。
  • 它们没有堆上对象都有的额外成员——类型句柄和同步块。

装箱与拆箱

  装箱——通过把变量保存在 Object 中,将值类型显示转换为相应的引用类型。装箱过程会造成性能损耗,其步骤如下:

  (1) 在托管堆中分配好内存。

  (2) 值类型的字段复制到新分配的堆内存。

  (3) 返回对象的地址。

  拆箱——把保存在对象引用中的值转换回栈上的相应值类型。拆箱的代价比装箱低的多,拆箱是取得指向包含在一个对象中的原始值类型的指针的过程。拆箱不需要在内存中复制任何字节。一个已装箱实例在拆箱时可能会抛出下列异常:

  • NullReferenceException,要拆箱的对象为 null 时抛出。
  • InvalidCastException,要拆箱的对象不是所期待的值类型的已装箱实例。

  装箱和拆箱的关键之处是:装箱时存放的是值类型的副本,拆箱返回的是值类型的另一个副本。

dynamic 基元类型

  dynamic是一个类型关键字,声明为dynamic的类型与"静态类型"(这里的静态类型是指编译时确定的类型,下同)相比最大的特点它是"动态类型",它会运行时尝试调用方法,这些方法的存在与否不是在编译时检查的,而是在运行时查找,如果方法存在并且参数正确,会正常调用,否则会抛出Microsoft.CSharp.RuntimeBinder.RuntimeBinderException异常。

  var关键字被称为:隐含类型局部变量(Local Variable Type Inference),var只能用作局部变量,不能用于字段、参数等;声明的同时必须初始化;初始化时类型就已经明确了,并且不能再被赋值不能进行隐式类型转换的类型的数据;编译时编译器会对var定义的变量进行类型推断,这时变量的类型已经被确定。dynamic可用于类型的字段,方法参数,方法返回值,可用于泛型的类型参数等;可以赋值给或被赋值任何类型并且不需显式的强制类型转换,因为这些是运行时执行的,这要得益于dynamic类型的动态特性。

  在字段类型、方法参数类型或方法返回类型被指定为dynamic 的前提下,编译器会将这个类型转换为System.Object,并在元数据中向字段、参数或返回类型应用System.Runtime.CompilerServices.DynamicAttribute 的一个实例。如果是一个局部变量被指定为dynamic,变量类型也会成为Object,但不会向局部变量应用DynamicAttribute,因为它的使用限制在方法之内。由于dynamic 其实就是Object,所以还有仅仅将dynamic 变成Object,又或者将Object 变成dynamic,就试图获得两个不同的方法签名。

  任何表达式都能隐式转换为dynamic,因为所有表达式最终都会生成一个从Object 派生的类型。正常情况下,编译器不允许写代码将一个表达式从Object 隐式转型为其他类型必须使用转型。然而,编译器允许使用隐式转型语法将一个表达式从dynamic 转型为其他类型。

类型与成员基础

类型的各种成员

类型的成员 成员说明
常量 常量就是指出数据值恒定不变的一个符号。这些符号通常用于使代码更容易阅读和维护。常量通常与类型关联,而不与类型的实例关联。从逻辑上讲,常量始终是静态成员。
字段 字段表示一个只读或可读/可写的数据值。字段可以是静态的;这这种情况下,字段被认为是类型状态的一部分,字段也可以是实例(非静态),这种情况下,字段被认为是对象状态的一部分。强烈建议将字段声明为私有字段,防止类型或对象的状态被该类型外部的代码破坏。
实例构造器 实例构造吕是将新对象的实例字段初始化为良好初始状态的一种特殊方法。
类型构造器 类型构造器是将类型的静态字段初始化为良好初始状态的一种特殊方法。
操作符重载 操作符重载实际是一个方法,它定义了将一个特定的操作符作用于对象时,应该如何操作这个对象。由于不是所有编程语言都支持操作符重载,所以操作符重载方法还是“公共语言规范”(Common Language Specification,CLS)的一部分。
转换操作符 转换操作符是定义如何隐式或显式地将对象从一种类型转型为另一种类型的方法。和操作符重载方法一样,并不是所有语言都支持转换操作符,所以它们不是CLS 的一部分。
属性 利用属性(property),可以使用一种简单的、字段风格的语法来设置或查询类型或对象的部分逻辑状态,同时保证状态不被遭到破坏。作用于类型的称为静态属性,作用于对象的称为实例属性。属性可以是没有参数的(这种情况非常普遍),也可以有多个参数(这种情况相当少见,但对于集合类来说很常见)。
事件 利用静态事件,一个类型可以向一个或多个静态或实例方法发送通知。而利用实例(非静态)事件,一个对象可以向一个或多个静态或实例方法发送通知。提供事件的类型或对象的状态发生改变,通常就会引发事件。事件包含两个方法,允许静态或实例方法登记或注销对该事件的关注。除了这两个方法,事件通常还使用一个委托字段来维护已登记的方法集。
类型 类型可定义嵌套于其中的其他类型。通常用这个方法将一个大的、复杂的类型分解成更小的构建单元,以简化实现。

类型的可见性

  在文件范围中定义类型时(相对于将类型的定义嵌套在另一个类型中),可以将类型的可见性指定为public 或internal。public 类型不仅对它的定义程序集中的所有代码可见,还对其他程序集中的代码可见。internal 类型仅对定义程序集中的所有代码可见,对其他程序集中的代码不可见。C# 编译器默认将类型的可见性设为internalCLR 和C# 通过友元程序集(friend assembly)来提供让指定的程序集访问本程序集中的internal 类型。

成员的可访问性

CLR 术语 C# 术语 描述
Private private 成员只能由定义类型或任何嵌套类型中的方法访问
Family protected 成员只能由定义类型、任何嵌套类型或者不管在什么程序集中的一个派生类型中的方法访问
Family or Assembly (不支持) 成员只能由定义业、任何嵌套类型或者同一程序集中定义的任何派生类型中的方法访问
Assembly internal 成员只能由定义程序集中的方法访问
Family or Assembly protected internal 成员可由任何嵌套类型、任何派生类型(不管在什么程序集)或者定义程序集中的任何方法
Public public 成员可由任何程序集的任何方法访问

  在C# 中,没有显式声明成员的可访问性,编译器通常(但不总是)默认选择private。CLR 要求接口类型的所有成员都具有public 可访问性。C# 编译器知道这一点,因此禁止开发人员显式指定接口成员的可访问性;编译器会自动将所有成员的可访问性设为public。

  一个派生类型重写在它的基类型中定义的一个成员时,C# 编译器要求原始成员和重写成员具有相同的可访问性。即,基类型中的成员是protected 的,派生类的重写成员也必须是protected 的。但是,这只是C# 语言本身的一个限制,而不是CLR 的。CLR 允许放宽派生类中的成员的可访问性。

表态类

  有一些永远不需要实例化的类,例如Console,Math,Environment 和ThreadPool 类。这些类只有static 成员。事实上,这种类唯一的作用就是将一组相关的成员组合到一起例如,Math 类定义了一组执行数学运算的方法。在C# 中,要用static 关键字定义不可实例化的类。这个关键字只能应用于类,不能应用于结构(值类型)。这是因为CLR 总是允许值类型实例化,这是没办法阻止的。

  C# 编译器对静态类进行了如下限制。

  • 静态类必须直接从基类System.Object 派生,从其他任何基类派生没有任何意义。继承只适用于对象,而不能创建静态类的实例。
  • 静态类不能实现任何接口,这是因为只有使用类的一个实例时,才可以调用类的接口方法。
  • 静态类只能定义静态成员(字段、方法、属性和事件),任何实例成员都将导致编译器报错。
  • 静态类不能作为字段、方法参数或局部变量使用,因为它们都代表引用了一个实例的变量,而这是不允许的。编译器检测到任何这样的用法都会报错。

多态和版本控制

C# 关键字 类型 方法/属性/事件 常量/字段
abstract 表示不能构造该类型的实例 表示为了构造派生类型的实例,派生类型必须重写并实现这个成员。 (不允许)
virtual (不允许) 表示这个成员可由派生类型重写 (不允许)
override (不允许) 表示派生类型重写了基类型的成员 (不允许)
sealed 表示该类型不能作为基类型 表示这个成员不能被派生类型重写,只能将该关键字应用于准备重写一个虚方法的方法 成员
new 应用于嵌套类型、方法、属性、事件、常量或字段时,表示该成员与基类中相似的成员无任何关系

CLR 如何调用虚方法、属性和事件

  属性和事件实际是作为方法实现的。

  方法代表在类型或类型的实例上执行某些操作的代码。在类型上执行操作,称为静态方法;在类型的实例上执行操作,称为非静态方法。任何方法都有一个名称、一个和一个返回值(可以是void)。

  CLR 提供了两个方法调用指令。

  • call  这个IL指令可调用静态方法、实例方法和虚方法。用call 指令调用静态方法时,必须指定是哪个类型定义了要由CLR 调用的方法。用call 指令调用实例方法或虚方法时,必须指定引用了一个对象的变量。call 指令假定变量不为null。换言之,变量本身的类型指明了要由CLR 调用的方法是在哪个类型中定义的。如果变量的类型没有定义该方法,就检查基类型来查找匹配的方法。call 指令经常用于以非虚的方式调用一个虚方法。
  • callvirt  这个IL 指令可调用实例方法和虚方法,但不能调用静态方法。用callvirt 指令调用实例方法或虚方法时,必须指定引用了一个对象的变量。用callvirt 指令调用非虚实例方法时,变量的类型指明了最终由CLR 调用的方法是在哪个类型中定义的。用callvirt 指令调用虚实例方法时,CLR 会调查发出调用的那个的实际类型,然后以多态方式调用方法。为了确定类型,用来发出调用的变量绝对不能是null。换言之,编译这个调用时,JIT 编译器会生成代码来验证变量的值是不是null。如果是,callvirt 指令会造成CLR 抛出一个NullReferenceException 异常。正是由于要进行这种额外的检查,所以callvirt 指令的执行速度比call 指令稍慢。注意,即使callvirt 指令调用的是一个非虚的实例方法,也要执行这种null 检查。

  无论是用call 还是callvirt 来调用实例方法或虚方法,这些方法通常接收一个的this 作为方法的第一个参数。this 引用 的是要操作的对象。

  设计一个类型时,应尽量减少所定义的虚方法数量。理由如下:

  • 1.调用虚方法的速度比调用非虚方法慢(C# 不影响,调用虚方法时同样使用callvirt 指令);
  • 2.JIT 编译器不能内嵌(inline)虚该方法;
  • 3.虚方法使组件(程序集)的版本控制变得更脆弱;
  • 4.定义一个基类型时,经常需要提供一组重载的简便该方法,使所有重载的简便该方法成为非虚该方法。

  用new 关键字表示成员与基类型无任何关系时,不会影响基类型的方法调用。

  用override 关键字表示成员重写基类型时,派生类的成员会“覆盖”基类型的成员,但在派生类子中可通过base 关键字来调用基类型的成员。

常量与字段

  常量(constant)是一个特殊的符号,它有一个从不变化的值。定义常量符号时,它的值必须能在编译时确定。确定之后,编译器将常量的值保存到程序集的元数据中。一般为基元数据方可定义为常量。常量总是被视为静态成员,而不是实例成员。定义常量将导致创建元数据。代码引用一个常量符号时,编译器会在定义常量的程序集的元数据中查找该符号,提取常量的值,并将值嵌入生成的IL 代码中。由于常量的值直接嵌入代码,所能在运行时不需要为常量分配任何内存。

  字段(field)是一种数据成员,其中容纳了一个值类型的实例或者对一个引用类型的引用。

CLR 术语 C# 术语 说明
Static static 这种字段是类型状态的一部分,而不是对象状态的一部分
Instance (默认) 这种字段与类型的一个实例关联,而不是与类型本身关联
InitOnly readonly 这种字段只能由一个构造器方法中的代码写入
Volatile volatile 看到访问这种字段的代码,编译器、CLR 或硬件就不会执行一些“线程不安全”的代码措施。

  CLR 支持readonly 字段和read/write 字段(默认)。read/write 字段在代码执行过程中,可多次改变。readonly 字段只能在构造器方法中写入。注意,可利用反射来修改readonly 字段。

方法

实例构造器和类(引用类型)

  构造器(constructor)是允许将类型的实例初始化为良好状态的一种特殊方法构造器方法在“方法定义元数据表”始终中叫.ctor (代表constructor)。创建一个引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始状态。如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译器会报错。如果类的修饰符为static(sealed 和abstract)编译器根本不会在类的定义中生成一个默认构造器。

实例构造器和结构(值类型)

  值类型(struct)构造器的工作方式与引用类型(class)的构造器截然不同。CLR 总是允许创建值类型的实例,并且没有办法阻止值类型实例化。CLR 允许为值类型定义构造器。但执行这种构造器的唯一方式是写代码来显式地调用它们。即,如果没有用new 操作符来调用值类型的构造器,那么值类型实例化时将不调用构造器,故值类型的字段都将为0。C# 编译器不允许(CLR 允许)值类型定义无参构造器。因此在C# 中不能为实例字段设置初始值。即下列代码是错误的。

    private struct SomeValType { private Int32 m_x = 5; /*不能在值类型中内联实例字段的初始化*/ }

类型构造器

  类型构造器(type constructor),也称为静态构造器(static constructor)、类构造器(class constructor)或者类型初始化器(type initializer)。类型构造器可应用于接口(C# 编译器不允许)、引用类型和值类型。实例构造器的作用是设置类型的实例的初始状态。对应地,类型构造器的作用是设置类型的初始状态。类型默认没有定义类型构造器,如果定义,也只能定义一个。此外,类型构造器永远没有参数。类型构造器必须将它们标记为static。此外,类型构造器应该总是私有的;C# 会自动把它们标记为private,且不可显式地标记为private。之所以必须私有,是为了阻止任何由开发人员写的代码调用它,对它的调用总是由CLR 负责的。最后,如果类型构造器抛出一个未自责的导演,CLR 会认为类型不可用。试图访问该类型的任何字段或方法,都将抛出一个System.TypeInitializationException 异常。类型构造器中的只能访问类型的静态字段,并且它的常规就是初始化这些字段。

操作符重载方法

  CLR 规范要求操作符重载方法必须是public 和static 方法。另外,C# 编程语言要求操作符重载方法至少有一个参数的类型与当前定义这个方法的类型相同。下面是例子:

  public sealed class Complex{ public static Complex operator + (Complex c1,Complex c2){...} }

转换操作符方法

  有时需要将对象从一个类型转换为另一个类型。例如,有时不得不将Byte 类型转换为Int32 类型。当源类型和目标类型都是编译器的基元类型时,编译器自己就知道如何生成转换对象所需的代码。如果源类型或目标类型不是基元类型,编译器会生成代码,要求CLR 执行转换(强制转型)。而转换操作符是将对象从一个类型转换成另一个类型的方法。可以使用特殊的语法来定义转换操作符方法。CLR 规范要求转换操作符重载方法必须是public 和 staitc 方法。险些之外,C# (以及其他许多语言)要求参数类型和返回类型二者必有其一与定义转换方法的类型相同。下面是示例代码:

  

扩展方法

  关于扩展方法,有一些附加的规则和原则需要谨记:

  • C# 只支持扩展方法,不支持扩展属性、扩展事件、扩展操作符等;
  • 扩展方法(第一个参数前面有this 的方法)必须在非泛型的静态类中声明。然而,类名没有限制,可以随便叫什么名字。当然,扩展方法至少要有一个参数,而且只有第一个参数能用this 关键字标记。
  • C# 编译器查找静态类中定义的扩展方法时,要求这些静态类本身必须具有文件作用域。换言之,如果静态类嵌套在另一个类中,C# 编译器会报错。
  • 由于静态类可以取任何名字,所以C# 编译器要花一定时间来寻找扩展方法,它必须检查文件作用域中的所有静态类,并扫描它们的所有静态方法来查找一个匹配。为增强性能,并避免找到非你所愿的一个扩展方法,C# 编译器要求“导入”扩展方法。(using namespace1;)
  • 多个静态类可以定义相同的扩展方法。如果编译器检测到存在两个或多个扩展方法,就会报错。这时应该显式的调用静态类中的静态方法。
  • 使用这个功能时要谨慎,因为并不是所有程序员都熟悉它,用一个扩展方法扩展一个类型时,同时也扩展 了派生类型,所以,不应将System.Object 用作扩展方法的第一个参数,否则这个方法在所有表达式类型上都能调用,造成Visual Studio 的“智能感知”窗口被垃圾信息污染。
  • 扩展方法有潜在的版本控制问题。当被扩展的类型添加一个和扩展方法原型相同的实例方法时,那么在重新编译代码时,编译器会绑定到重新添加的实例方法上,而不是静态扩展方法。这样就会导致出现一些问题。这个版本控制问题是使用扩展方法时必须慎重的另一个原因。
posted @ 2014-05-26 19:46  岩石草  阅读(1284)  评论(1编辑  收藏  举报