CLR via C#, 4th -- 【设计类型】 -- 第6章类型和成员基础
6.1 类型的各种成员
常量
常量是指出数据值恒定不变的符号。这种符号使代码更易阅读和维护。常量总与类型关联,不与类型的实例关联。常量逻辑上总是静态成员。相关内容在第7章“常量和字段"讨论。
字段
字段表示只读或可读/可写的数据值。字段可以是静态的:这种字段被认为是类型状态的一部分。字段也可以是实例(非静态):这种字段被认为是对象状态的一部分。强烈建议将字段声明为私有,防止类型或对象的状态被类型外部的代码破坏。相关内容在第7章讨论。
实例构造器
实例构造器是将新对象的实例字段初始化为良好初始状态的特殊方法。相关内容在第8章“方法”讨论。
类型构造器
类型构造器是将类型的静态字段初始化为良好初始状态的特殊方法。相关内容在第8章讨论。
方法
对象称为实例方法。方法通常要读写类型或对象的字段。相关内容在第8章讨论。
操作符重载
操作符重载实际是方法,定义了当操作符作用于对象时,应该如何操作该对象。由于不是所有编程语言都支持操作符重载,所以操作符重载方法不是“公共语言规范”(Common Language Specification,CLS)的一部分。相关内容在第8章讨论。
转换操作符
转换操作符是定义如何隐式或显式将对象从一种类型转型为另一种类型的方法。和操作符重载方法一样,并不是所有编程语言都支持转换操作符,所以不是CLS的一部分。相关内容在第8章讨论。
属性
属性允许用简单的、字段风格的语法设置或查询类型或对象的逻辑状态,同时保证状态不被破坏。作用于类型称为静态属性,作用于对象称为实例属性。属性可以无参(非常普遍),也可以有多个参数(相当少见,但集合类用得多),相关内容在第10章“属性”讨论。
事件
静态事件允许类型向一个或多个静态或实例方法发送通知。实例(非静态)事件允许对象向一个或多个静态或实例方法发送通知。引发事件通常是为了响应提供事件的类型或对象的状态的改变。事件包含两个方法,允许静态或实例方法登记或注销对该事件的关注。除了这两个方法,事件通常还用一个委托字段来维护已登记的方法集。相关内容在第11章“事件”讨论。
类型
类型可定义其他嵌套类型。通常用这个办法将大的、复杂的类型分解成更小的构建单元(building block)以简化实现。
using System; public sealed class SomeType { // 1 // Nested class private class SomeNestedType { } // 2 // Constant, readonly, and static read/write field private const Int32 c_SomeConstant = 1; // 3 private readonly String m_SomeReadOnlyField = "2"; // 4 private static Int32 s_SomeReadWriteField = 3; // 5 // Type constructor static SomeType() { } // 6 // Instance constructors public SomeType(Int32 x) { } // 7 public SomeType() { } // 8 // Instance and static methods private String InstanceMethod() { return null; } // 9 public static void Main() {} // 10 // Instance property public Int32 SomeProp { // 11 get { return 0; } // 12 set { } // 13 } // Instance parameterful property (indexer) public Int32 this[String s] { // 14 get { return 0; } // 15 set { } // 16 } // Instance event public event EventHandler SomeEvent; // 17 }

6.2 类型的可见性
要定义文件范围的类型(而不是将类型定义嵌套到另一个类型中)时,可将类型的可见性指定为public或internal,public类型不仅对定义程序集中的所有代码可见,还对其他程序集中的代码可见。internal类型则仅对定义程序集中的所有代码可见,对其他程序集中的代码不可见。定义类型时不显式指定可见性,C#编译器会帮你指定为interna(限制比public大)。
友元程序集
我们希望TeamA能有一个办法将他们的工具类型定义为internal,同时仍然允许团队TeamB访问这些类型。CLR和C#通过友元程序集(friend assembly)提供这方面的支持。用一个程序集中的代码对另一个程序集中的内部类型进行单元测试时,友元程序集功能也能派上用场。
// This assembly's internal types can be accessed by any code written // in the following two assemblies (regardless of version or culture): [assembly:InternalsVisibleTo("Wintellect, PublicKey=12345678...90abcdef")] [assembly:InternalsVisibleTo("Microsoft, PublicKey=b77a5c56...1934e089")] internal sealed class SomeInternalType { ... } internal sealed class AnotherInternalType { ... }
生成程序集时,可用System.Runtime.CompilerServices命名空间中的InternalsVisibleTo特性标明它认为是“友元”的其他程序集。该特性获取标识友元程序集名称和公钥的字符串参数(传给该特性的字符串绝不能包含版本、语言文化和处理器架构)。注意当程序集认了“友元”之后,友元程序集就能访问该程序集中的所有internal类型,以及这些类型的internal成员。
using System; internal sealed class Foo { private static Object SomeMethod() { // This "Wintellect" assembly accesses the other assembly's // internal type as if it were a public type SomeInternalType sit = new SomeInternalType(); return sit; } }
由于程序集中的类型的internal成员能从友元程序集访问,所以要慎重考虑类型成员的可访问性,以及要将哪些程序集声明为友元。注意C#编译器在编译友元程序集(不含InternalsVisibleTo特性的程序集)时要求使用编译器开关/out:<file>。使用这个编译器开关的原因在于,编译器需要知道准备编译的程序集的名称,从而判断生成的程序集是不是友元程序集。你或许以为C#编译器能自己判断,因为平时都是它自己确定输出文件名。但事实上,在代码结束编译之前,C#编译器是不知道输出文件名的。因此,使用/out:<file>编译器开关能极大增强编译性能。
同样地,如果使用C#编译器的/t:module开关来编译模块(而不是编译成程序集),而且该模块将成为某个友元程序集的一部分,那么还需要使用C#编译器的/moduleassemblyname:<string>开关来编译该模块,它告诉编译器该模块将成为哪个程序集的一部分,使编译器设置模块中的代码,使它们能访问另一个程序集中的internal类型。
6.3成员的可访问性
TABLE 6-1 M ember Accessibility
|
CLR Term |
C# Term |
Description |
|
Private |
private |
The member is accessible only by methods in the defining type or any nested type. |
|
Family |
protected |
The member is accessible only by methods in the defining type, any nested type, or one of its derived types without regard to assembly. |
|
Family and Assembly |
(not supported) |
The member is accessible only by methods in the defining type, any nested type, or by any derived types defined in the same assembly. |
|
Assembly |
internal |
The member is accessible only by methods in the defining assembly. |
|
Family or Assembly |
protected internal |
The member is accessible by any nested type, any derived type (regardless of assembly), or any methods in the defin -ing assembly. |
|
Public |
public |
The member is accessible to all methods in any assembly. |
通过对IL代码进行验证,可确保被引用成员的可访问性在运行时得到正确兑现-即使语言的编译器忽略了对可访问性的检查。另外极有可能发生的情况是:语言编译器编译的代码会访问另一个程序集中的另一个类型的public成员,但到运行时却加载了程序集的不同版本,新版本中的public成员变成了protected或private成员。
在C#中,如果没有显式声明成员的可访问性,编译器通常(但并不总是)默认选择private(限制最大的那个),CLR要求接口类型的所有成员都具有public可访问性。C编译器知道这一点,因此禁止开发人员显式指定接口成员的可访问性;编译器自动将所有成员的可访问性设为public.
派生类型重写基类型定义的成员时,C#编译器要求原始成员和重写成员具有相同的可访问性。也就是说,如果基类成员是protected的,派生类中的重写成员也必须是protected的。
但这是C#的限制,不是CLR的。从基类派生时,CLR允许放宽但不允许收紧成员的可访问性限制。例如,类可重写基类定义的protected方法,将重写方法设为public(放宽限制)。
但不能重写基类定义的protected方法,将重写方法设为private(收紧限制),之所以不能在派生类中收紧对基类方法的访问,是因为CLR承诺派生类总能转型为基类,并获取对基类方法的访问权。如果允许派生类收紧限制,CLR的承诺就无法兑现了。
6.4 静态类
在C#中,要用static关键字定义不可实例化的类。该关键字只能应用于类,不能应用于结构(值类型)。因为CLR总是允许值类型实例化,这是没办法阻止的。
C#编译器对静态类进行了如下限制。
- 静态类必须直接从基类System.Object派生,从其他任何基类派生都没有意义。继承只适用于对象,而你不能创建静态类的实例。
- 静态类不能实现任何接口,这是因为只有使用类的实例时,才可调用类的接口方法。
- 静态类只能定义静态成员(字段、方法、属性和事件),任何实例成员都会导致编译器报错。
- 静态类不能作为字段、方法参数或局部变量使用,因为它们都代表引用了实例的变量,而这是不允许的。编译器检测到任何这样的用法都会报错。
使用关键字static定义类,将导致C#编译器将该类标记为abstract和sealed.
6.5 分部类、结构和接口
- 源代码控制
- 在同一个文件中将类或结构分解成不同的逻辑单元
- 代码拆分
在Microsoft Visual Studio中创建新项目时,一些源代码文件会作为项目一部分自动创建。这些源代码文件包含模板,能为项目开个好头。使用Visual Studio在设计图面上拖放控件时,Visual Studio自动生成源代码,并将代码拆分到不同的源代码文件中。
要将partial关键字应用于所有文件中的类型。这些文件编译到一起时,编译器会合并代码,在最后的.exe或.dl1程序集文件(或netmodule模块文件)中生成单个类型。“分部类型”功能完全由C#编译器实现,CLR对该功能一无所知,这解释了一个类型的所有源代码文件为什么必须使用相同编程语言,而且必须作为一个编译单元编译到一起。
6.6 组件、多态和版本控制
组件软件编程(Component Software Programming,CSP)正是OOP发展到极致的成果。下面列举组件的一些特点。
- 组件(NET Framework称为程序集)有“已经发布”的意思。
- 组件有自己的标识(名称、版本、语言文化和公钥)。
- 组件永远维持自己的标识(程序集中的代码永远不会静态链接到另一个程序集中..NET总是使用动态链接).
- 组件清楚指明它所依赖的组件(引用元数据表)。
- 组件应编档它的类和成员。C#语言通过源代码内的XML文档和编译器的/doc命令行开关提供这个功能。
- 组件必须指定它需要的安全权限。CLR的代码访问安全性(Code Access Security,CAS)机制提供这个功能。
- 组件要发布在任何“维护版本”中都不会改变的接口(对象模型)。“维护版本"(servicing version)代表组件的新版本,它向后兼容组件的原始版本。通常,“维护版本”包含bug修复、安全补丁或者一些小的功能增强。但在“维护版本”中,不能要求任何新的依赖关系,也不能要求任何附加的安全权限。
NET Framework中的版本号包含4个部分:主版本号(major version)、次版本号(minor version)、内部版本号(build number)和修订号(revision)。例如,版本号为1.2.3.4的程序集,其主版本号为1,次版本号为2,内部版本号为3,修订号为4,major/minor部分通常代表程序集的一个连续的、稳定的功能集,而build/revision部分通常代表对这个功能集的一次维护。
假定某公司发布了版本号为2.7.0.0的程序集。之后,为了修复该组件的bug,他们可以生成一个新的程序集,并只改动版本号的build/revision部分,比如2.7.1.34,这表明该程序集是维护版本,向后兼容原始版本(2.7.0.0)
另一方面,假定该公司想生成程序集的新版本,而且由于发生了重大变化,所以不准备向后兼容程序集的原始版本。在这种情况下,公司实际是要创建全新组件,major/minor版本号(比如3.0.0.0)应该和原来的组件不同。
TABLE 6-2 C# Keywords and How They Affect Component Versioning
|
C# Keyword |
Type |
Method/Property/Event |
Constant/Field |
|
abstract |
Indicates that no instances of the type can be constructed |
Indicates that the derived type must override and implement this member before instances of the derived type can be constructed |
(not allowed) |
|
virtual |
(not allowed) |
Indicates that this member can be overridden by a derived type |
(not allowed) |
|
override |
(not allowed) |
Indicates that the derived type is overriding the base type’s member |
(not allowed) |
|
sealed |
Indicates that the type cannot be used as a base type |
Indicates that the member cannot be overridden by a derived type. This keyword can be applied only to a method that is overriding a virtual method |
(not allowed) |
|
new |
When applied to a nested type, meth-od, property, event, constant, or field,indicates that the member has no relationship to a similar member that may exist in the base class |
|
|
6.6.1 CLR如何调用虚方法、属性和事件
方法代表在类型或类型的实例上执行某些操作的代码。在类型上执行操作,称为静态方法:在类型的实例上执行操作,称为非静态方法。
internal class Employee { // A nonvirtual instance method public Int32 GetYearsEmployed() { ... } // A virtual method (virtual implies instance) public virtual String GetProgressReport() { ... } // A static method public static Employee Lookup(String name) { ... } }
编译代码,编译器会在程序集的方法定义表中写入记录项,每个记录项都用一组标志(flag)指明方法是实例方法、虚方法还是静态方法。
写代码调用这些方法,生成调用代码的编译器会检查方法定义的标志(lag),判断应如何生成IL代码来正确调用方法。CLR提供两个方法调用指令。
call
该IL指令可调用静态方法、实例方法和虚方法。用call指令调用静态方法,必须指定方法的定义类型。用call指令调用实例方法或虚方法,必须指定引用了对象的变量。
call指令假定该变量不为null。换言之,变量本身的类型指明了方法的定义类型。如果变量的类型没有定义该方法,就检查基类型来查找匹配方法。call指令经常用于以非虚方式调用虚方法。
callvirt
该IL指令可调用实例方法和虚方法,不能调用静态方法。用callvirt指令调用实例方法或虚方法,必须指定引用了对象的变量。用callvirt指令调用非虚实例方法,变量的类型指明了方法的定义类型。用callvirt指令调用虚实例方法,CLR调查发出调用的对象的实际类型,然后以多态方式调用方法。为了确定类型,发出调用的变量绝不能是null.换言之,编译这个调用时,JT编译器会生成代码来验证变量的值是不是null.如果是,callvirt指令造成CLR抛出NulReferenceException异常。正是由于要进行这种额外的检查,所以callvirt指令的执行速度比call指令稍慢。注意,即使callvirt指令调用的是非虚实例方法,也要执行这种null检查。
using System; public sealed class Program { public static void Main() { Console.WriteLine(); // Call a static method Object o = new Object(); o.GetHashCode(); // Call a virtual instance method o.GetType(); // Call a nonvirtual instance method } }
注意,C#编译器用call指令调用Console的WriteLine方法。这在意料之中,因为writeLine是静态方法。接着用callvirt指令调用GetHashCode,这也在意料之中,因为GetHashCode是虚方法。最后,C#编译器用callvirt指令调用GetType方法。这就有点出乎意料了,因为GetType不是虚方法。但这是可行的,因为码进行JIT时,CLR知道GetType不是虚方法,所以在JIT编译好的代码中,会直接以非虚方式调用GetType.
那么,为什么C#编译器不干脆生成call指令呢?答案是C#团队认为,JIT编译器应生成代码来验证发出调用的对象不为null。这意味着对非虚实例方法的调用要稍慢一点。这也意味着以下C#代码将抛出NullReferenceException异常。注意,在另一些编程语言中,以下代码能正常工作。
public sealed class Program { public Int32 GetFive() { return 5; } public static void Main() { Program p = null; Int32 x = p.GetFive(); // In C#, NullReferenceException is thrown } }
上述代码理论上并无问题。变量p确实为null,但在调用非虚方法(GetFive)时,CLR唯一需要知道的就是p的数据类型(Program)。如果真的调用GetFive,this实参值是null。由于GetFive方法内部并未使用该实参,所以不会抛出NullReferenceException异常。但由于C#编译器生成callvirt而不是call指令,所以上述代码抛出了NullReferenceException异常。
编译器调用值类型定义的方法时倾向于使用call指令,因为值类型是密封的。这意味着即使值类型含有虚方法也不用考虑多态性,这使调用更快。此外,值类型实例的本质保证它永不为null,所以永远不抛出NullReferenceException异常。最后,如果以虚方式调用值类型中的虚方法,CLR要获取对值类型的类型对象的引用,以便引用(类型对象中的)方法表,这要求对值类型装箱。
设计类型时应尽量减少虚方法数量。
- 首先,调用虚方法的速度比调用非虚方法慢。
- 其次,JIT编译器不能内嵌(inline)虚方法,这进一步影响性能。
- 第三,虚方法使组件版本控制变得更脆弱,详情参见下一节。
- 第四,定义基类型时,经常要提供一组重载的简便方法(convenience method),如果希望这些方法是多态的,最好的办法就是使最复杂的方法成为虚方法,使所有重载的简便方法成为非虚方法。
public class Set { private Int32 m_length = 0; // This convenience overload is not virtual public Int32 Find(Object value) { return Find(value, 0, m_length); } // This convenience overload is not virtual public Int32 Find(Object value, Int32 startIndex) { return Find(value, startIndex, m_length startIndex); } // The most featurerich method is virtual and can be overridden public virtual Int32 Find(Object value, Int32 startIndex, Int32 endIndex) { // Actual implementation that can be overridden goes here... } // Other methods go here }
6.6.2 合理使用类型的可见性和成员的可访问性
密封类之所以比非密封类更好,有以下三个方面的原因。
版本控制
如果类最初密封,将来可在不破坏兼容性的前提下更改为非密封。但如果最初非密封,将来就不可能更改为密封,因为这将中断派生类。另外,如果非密封类定义了非密封虚方法,必须在新版本的类中保持虚方法调用顺序,否则可能中断派生类。
性能
如上一节所述,调用虚方法在性能上不及调用非虚方法,因为CLR必须在运行时查找对象的类型,判断要调用的方法由哪个类型定义。但是,如果JT编译器看到使用密封类型的虚方法调用,就可采用非虚方式调用虚方法,从而生成更高效的代码。之所以能这么做,是因为密封类自然不会有派生类。
安全性和可预测性
类必须保护自己的状态,不允许被破坏。当类处于非密封状态时,只要它的任何数据字段或者在内部对这些字段进行处理的方法是可以访间的,而且不是私有的,派生类就能访问和更改基类的状态。另外,派生类既可重写基类的虚方法,也可直接调用这个虚方法在基类中的实现。一旦将某个方法、属性或事件设为virtual,基类就会丧失对它的行为和状态的部分控制权。所以,除非经过了认真考虑,否则这种做法可能导致对象的行为变得不可预测,还可能留下安全隐患。
以下是我自己定义类时遵循的原则。
- 定义类时,除非确定要将其作为基类,并允许派生类对它进行特化,否则总是显式地指定为sealed类。如前所述,这与C#以及其他许多编译器的默认方式相反。另外,我默认将类指定为internal类,除非我希望在程序集外部公开这个类。幸好,如果不显式指定类型的可见性,C#编译器默认使用的就是internal,如果我真的要定义一个可由其他人继承的类,同时不希望允许特化,那么我会重写并密封继承的所有虚方法。
- 类的内部,我总是毫不犹豫地将数据字段定义为private,幸好,C#默认就将字段标记为private,事实上,我情愿C#强制所有字段都标记为private,根本不允许protected internal和public等等。状态一旦公开,就极易产生问题,造成对象的行为无法预测并留下安全隐患。即使只将一些字段声明为internal也会如此。即使在单个程序集中,也很难跟踪引用了一个字段的所有代码,尤其是假如代码由几个开发人员编写,并编译到同一个程序集中。
- 在类的内部,我总是将自己的方法、属性和事件定义为private和非虚。幸好,C#默认也是这样的。当然,我会将某个方法、属性和事件定义为public,以便公开类型的某些功能。我会尽量避免将上述任何成员定义为protected或internal,因为这会使类型面临更大的安全风险。即使迫不得已,我也会尽量选择protected或internal,virtual永远最后才考虑,因为虚成员会放弃许多控制,丧失独立性,变得彻底依赖于派生类的正确行为。
- OOP有一条古老的格言,大意是当事情变得过于复杂时,就搞更多的类型出来。当算法的实现开始变得复杂时,我会定义一些辅助类型来封装独立的功能。如果定义的辅助类型只由一个“超类型”使用,我会在“超类型”中嵌套这些辅助类型。这样除了可以限制范围,还允许嵌套的辅助类型中的代码引用“超类型”中定义的私有成员。但是,Visual Studio的代码分析工具(FxCopCmd.exe)强制执行了一条设计规则,即对外公开的嵌套类型必须在文件或程序集范围中定义,不能在另一个类型中定义。之所以会有这个规则,是因为一些开发人员觉得引用嵌套类型时,所用的语法过于繁琐。我赞同该规则,自己绝不会定义公共嵌套类型。
6.6.3对类型进行版本控制时的虚方法的处理
namespace CompanyA { public class Phone { public void Dial() { Console.WriteLine("Phone.Dial"); // Do work to dial the phone here. } } }
namespace CompanyB { public class BetterPhone : CompanyA.Phone { public void Dial() { Console.WriteLine("BetterPhone.Dial"); EstablishConnection(); base.Dial(); } protected virtual void EstablishConnection() { Console.WriteLine("BetterPhone.EstablishConnection"); // Do work to establish the connection. } } }
CompanyB编译上述代码时,C#编译器生成以下警告消息:
warning cs0108:"CompanyB.BetterPhone.Dial()"隐藏了继承的成员
"CompanyA.Phone.Dial()".如果是有意隐藏,请使用关键字new
该警告告诉开发人员BetterPhone类正在定义个Dial方法,它会隐藏Phone类定义的Dial.
新方法可能改变Dial的语义(这个语义是CompanyA最初创建Dial方法时定义的)。
编译器就潜在的语言不匹配问题发出警告,这是一个令人欣赏的设计。编译器甚至贴心地告诉你如何消除这条警告,办法是在BetterPhone类中定义Dial时,在前面加一个new关字。以下是改正的BetterPhone类:
namespace CompanyB { public class BetterPhone : CompanyA.Phone { // This Dial method has nothing to do with Phone's Dial method. public new void Dial() { Console.WriteLine("BetterPhone.Dial"); EstablishConnection(); base.Dial(); } protected virtual void EstablishConnection() { Console.WriteLine("BetterPhone.EstablishConnection"); // Do work to establish the connection. } } }
public sealed class Program { public static void Main() { CompanyB.BetterPhone phone = new CompanyB.BetterPhone(); phone.Dial(); } }
现在,假定几家公司计划使用CompanyA的Phone类型。再假定这几家公司都认为在Diall方法中建立连接的主意非常好。CompanyA收到这个反馈,决定对Phone类进行修订:
namespace CompanyA { public class Phone { public void Dial() { Console.WriteLine("Phone.Dial"); EstablishConnection(); // Do work to dial the phone here. } protected virtual void EstablishConnection() { Console.WriteLine("Phone.EstablishConnection"); // Do work to establish the connection. } } }
现在,一旦 CompanyB编译它的BetterPhone类型(从CompanyA的新版本Phone派生),编译器将生成以下警告消息:
warning cs0114:"CompanyB.BetterPhone.EstablishConnection()"将隐藏继承的成员"CompanyA.Phone.EstablishConnection()".
若要使当前成员重写该实现,请添加关键字override.否则,添加关键字new.
编译器警告Phone和BetterPhone都提供了EstablishConnection方法,而且两者的语义可能不一致。只是简单地重新编译BetterPhone,可能无法获得和使用第一个版本的Phone类型时相同的行为
如果CompanyB认定EstablishConnection方法在两个类型中的语义不一致,CompanyB可以告诉编译器使用BetterPhone类中定义的Dial和EstablishConnection方法,它们与基类型 Phone中定义的EstablishConnection方法没有关系。CompanyB可以为EstablishConnection方法添加new关键字来告诉编译器这一点。
namespace CompanyB { public class BetterPhone : CompanyA.Phone { // Keep 'new' to mark this method as having no // relationship to the base type's Dial method. public new void Dial() { Console.WriteLine("BetterPhone.Dial"); EstablishConnection(); base.Dial(); } // Add 'new' to mark this method as having no // relationship to the base type's EstablishConnection method. protected new virtual void EstablishConnection() { Console.WriteLine("BetterPhone.EstablishConnection"); // Do work to establish the connection. } } }
在这段代码中,关键字new告诉编译器生成元数据,让CLR知道BetterPhone类型的EstablishConnection方法应被视为由BetterPhone类型引入的新函数。这样CLR就知道Phone和BetterPhone这两个类中的方法无任何关系。
还有一个办法是,CompanyB可以获得CompanyA的新版本Phone类型,并确定Dial和EstablishConnection在Phone中的语义正好是他们所希望的。这种情况下,CompanyB可通过完全移除Dial方法来修改他们的BetterPhone类型。另外,由于CompanyB现在希望告诉编译器,BetterPhone的Establi shConnection方法和Phone的EstablishConnection方法是相关的,所以必须移除new关键字。但是,仅仅移除new关键字还不够,因为编译器目前还无法准确判断BetterPhone的EstablishConnection方法的意图。为了准确表示意图,CompanyB的开发人员还必须将BetterPhone的EstablishConnection方法由virtual改变为override。以下代码展示了新版本的BetterPhone类。
namespace CompanyB { public class BetterPhone : CompanyA.Phone { // Delete the Dial method (inherit Dial from base). // Remove 'new' and change 'virtual' to 'override' to // mark this method as having a relationship to the base // type's EstablishConnection method. protected override void EstablishConnection() { Console.WriteLine("BetterPhone.EstablishConnection"); // Do work to establish the connection. } } }
该输出结果表明,在Main中调用Dial方法,调用的是由Phone定义、并由BetterPhone继承的Dial方法。然后,当Phone的Dial方法调用虚方法EstablishConnection时,实际调用的是BetterPhone类的EstablishConnection方法,因为它重写了由Phone定义的虚方法EstablishConnection.
浙公网安备 33010602011771号