《框架设计(第2版)》:CLR_Via_C#_第1页
《框架设计(第2版)》:CLR_Via_C#_第2页
《框架设计(第2版)》:CLR_Via_C#_第3页
《框架设计(第2版)》:CLR_Via_C#_第4页
《框架设计(第2版)》:CLR_Via_C#_第5页
已阅读5页,还剩71页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

147第6章 类型和成员基础第部分 类型的设计=第6章 类型和成员基础=第7章 常量和字段=第8章 方法:构造器、操作符、转换操作符 和参数=第9章 属性=第10章 事件=第6章 管理错误和异常 第一类型和成员基础本章内容l 类型成员的种类l 类型的可见性l 成员的可访问性l 静态类l 部分类、结构和接口l 组件、多态和版本控制在第部分“类型的使用”中,我们重点介绍类型以及任何类型的所有实例通用的操作,并阐述如何将类型划分为引用类型和值类型。在本章及本部分的后续章节,我们将示范如何使用可以在类型中定义的各种成员来设计类型。从第7章到第10章,我们将详细讨论各种成员。6.1 类型成员的种类类型可以定义0个或者多个下述类型的成员:l 常量(第7章) 常量就是一个用来标识数据值恒定不变的符号。这些符号通常用于使代码更容易阅读和维护。常量通常与类型关联,而不与类型的实例关联。从逻辑上讲,常量通常是静态成员。l 字段(第7章) 字段表示一个只读或可读可写的数据值。字段可以是静态的;这种情况下,字段是类型状态的一部分;字段也可以是实例(非静态的),这种情况下,字段是对象状态的一部分。强烈建议将字段声明为私有字段,以免被该类型外部的代码破坏类型或者对象的状态。l 实例构造器(第8章) 实例构造器是一种将新对象的实例字段初始化为有效初始状态的特殊方法。l 类型构造器(第8章) 类型构造器是一种将类型的静态字段初始化为有效初始状态的特殊方法。l 方法(第8章) 方法是一个执行改变或者查询类型状态(静态方法)或者对象状态(实例方法)操作的函数。方法通常读或写类型或者对象的字段。l 操作符重载(第8章) 操作符重载指对对象应用特定操作符时定义对象如何操作的方法。因为并不是所有的编程语言都支持操作符重载方法,所以操作符重载方法不属于公共语言规范(Common Language Specification,CLS)。l 转换操作符(第8章) 转换操作符是定义如何隐式或者显式地将对象从一种类型转换(或强制转换)到另一种类型的方法。和操作符重载方法一样,并不是所有的编程语言都支持转换操作符,因此转换操作符也不属于CLS。l 属性(第9章) 属性是指允许使用一个简单的、字段形式的语法来设置或者查询类型(静态属性)或对象(实例属性)的部分逻辑状态,并且保证该状态不被破坏的一种机制。属性可以没有参数(这种情况非常普遍),也可以有多个参数(这种情况相当少见,但是经常在集合类(collection class)中使用)。l 事件(第10章) 静态事件(static event)是指允许类型为监听类型(listening type)或者监听对象(listening object)发送通知的机制。实例(非静态)事件(instance event)是指允许对象为监听类型或者监听对象发送通知的机制。事件的触发通常是为了响应产生事件的类型或者对象的状态发生的改变。事件包含两个方法,允许类型或者对象(通常称为监听者(listener)订阅或者注销事件。除了这两个方法,事件通常还使用一个委托字段(delegate field)来维护已订阅该事件的监听者。l 类型 类型可以在类型内部嵌套地定义其他类型。通常使用这个方法将一个大的、复杂的类型分解成小的构建块(building block),以此来简化实现。再次声明本章的目的不是为了详细地描述各种类型的成员,而是阐明各种类型的成员都拥有的共性,为后面的章节打一个基础。无论使用什么编程语言,相应的编译器都必须能够处理前述列表中所有类型的成员的源代码,并且能够为每个成员生成元数据(metadata)和中间语言(Intermediate Language,IL)代码。元数据的格式与源代码所使用的编程语言无关,因此元数据的格式都是相同的,这使得CLR成为名副其实的“公共语言运行库”(Common Language Runtime,CLR)。元数据是所有语言都可以生成和使用的公共信息,它使某一编程语言编写的代码可以无缝地访问另一个完全不同的编程语言编写的代码。CLR同样也使用公共元数据格式,CLR用它们决定常量、字段、构造器、方法、属性和事件的行为在运行时如何表现。简单地说,元数据就是整个Microsoft.NET Framework开发平台的关键,它允许编程语言、类型和对象之间的无缝集成。下面的C#代码示范了包含所有可能的成员的类型定义,这段代码可以通过编译(不过会出现警告信息),但是它并不能代表我们通常所要创建的类型,因为这段代码中定义的大部分方法根本没有做任何有价值的事情。这里仅仅是为了示范编译器如何将类型及其成员转换成元数据。再次声明,在后续章节中我们将对这些成员逐一进行讨论。using System; public sealed class SomeType / 1 /嵌套类 private class SomeNestedType / 2 /常量、只读字段和静态读写字段 private const Int32 SomeConstant = 1; / 3 private readonly Int32 SomeReadOnlyField = 2;/ 4 private static Int32 SomeReadWriteField = 3; / 5 /类型构造器 static SomeType() / 6 /实例构造器 public SomeType() / 7 public SomeType(Int32x) / 8 /实例方法和静态方法 private String InstanceMethod() returnnull;/ 9 public static void Main() /10 /实例无参属性 public Int32 SomeProp /11 get return0; /12 set /13 /实例有参属性 public Int32 thisString s /14 getreturn0; /15 set /16 /实例事件 public event EventHandler SomeEvent; /17 如果编译刚才定义的类型,然后用ILDasm.exe来查看得到的元数据,将看到如图6.1中所示的输出结果。图6.1 上述代码的元数据在ILDasm.exe中的显示输出注意,编译器为定义在源代码中的所有成员都产生了相关的元数据。实际上,对于其中的一些成员编译器,还为它们产生了额外的成员以及额外的元数据。例如,事件成员(代码中编号为17)使编译器生成了一个字段、两个方法及一些额外的元数据。这里并不希望大家能完全理解其中的内容。但是,当学习完后面几章内容后,希望大家能回顾一下这个范例,看看成员是如何定义的,以及对编译器生成的元数据有何影响。6.2 类型的可见性在文件范围内定义类型时(对应是在某一类型中嵌套地定义另一个类型),可以指定类型的可见性为public或者internal。public类型不仅对定义该类型的程序集中的所有代码可见,而且对其他程序集中的代码也可见。internal类型仅对定义该类型的程序集中的所有代码可见,而对其他程序集中的代码不可见。定义类型时,如果没有显式地指定类型的可见性,C#编译器会将类型的可见性设为internal(两者之中约束性比较强的一个)。下面用几个例子加以说明:using System;/下述类型的可见性为public,它既可以被本程序集中的代码访问,又可以被其他程序集中的代码访问public class ThisIsAPublicType . /下述类型的可见性为internal,它只可以被本程序集中的代码访问internal class ThisIsAnInternalType . /因为没有显式地声明该类型的可见性,所以下述类型的可见性为internalclass ThisIsAlsoAnInternalType . 友元程序集假定下述情形:某公司的工作组TeamA在某个程序集中定义了一组工具类型(utility type),并且他们希望公司的另一个工作组TeamB中的成员可以使用这些类型。但是由于各种原因,如时间安排、地理位置、不同的成本中心或报告结构,这两个工作组不能将他们所有的工具类型构建在一个程序集中,相反,每个工作组都生成了自己的程序集。为了使工作组TeamB的程序集使用工作组TeamA的程序集中的类型,TeamA必须将他们所有的工具类型的可见性定义为public。但是,这意味着工作组TeamA的程序集中的工具类型将对所有的程序集公开可见,另一公司的开发人员可以编写使用这些公开的工具类型的代码,而这并不是所期望的。或许这些工具类型确实假定工作组TeamB保证他们编写代码时使用工作组TeamA的工具类型(译注:即工作组TeamB必须使用工作组TeamA定义的工具类型,这意味着又不得不将TeamA的工具类型的可见性定义为public)。我们希望有这样一种方法,即工作组TeamA将他们的工具类型的可见性定义为internal,同时仍然允许工作组TeamB访问这些类型。CLR和C#通过友元程序集(friend assembly)来实现。在构建程序集的过程中,程序集可以通过使用命名空间(namespace) System.Runtime. CompilerServices中定义的属性InternalsVisibleTo来标明程序集认为是“友元”的其他程序集。该属性拥有一个字符串参数来标识友元程序集的名称和公钥(public key)(传递给属性的字符串不能包含版本、语言文化和处理器架构的信息)。注意,友元程序集可以访问程序集中的所有internal类型,以及这些类型的所有internal成员。下面的范例示范了程序集如何将两个强命名的程序集“Wintellect”和“Microsoft”指定为它的友元程序集:using System;using System.Runtime.CompilerServices; / 对于InternalsVisibleTo属性/该程序集中可见性为internal的类型可以被下面两个程序集中的任何代码访问(不用考虑这两个程序集的版本和语言文化):assembly:InternalsVisibleTo(Wintellect, PublicKey=12345678.90abcdef)assembly:InternalsVisibleTo(Microsoft, PublicKey=b77a5c56.1934e089)internal sealed class SomeInternalType . internal sealed class AnotherInternalType . 从友元程序集中访问上述程序集中的internal类型没有什么价值。例如,下面范例示范了公钥为“12345678.90abcdef”的友元程序集“Wintellect”如何访问上述程序集中内部类型SomeInternalType。using System;internal sealed class Foo private static Object SomeMethod() /这个“Wintellect”程序集可以访问其他程序集中可见性为internal的类型,就好像这个 /类型的可见性是public的一样 SomeInternalType sit = new SomeInternalType(); return sit; 程序集中类型的internal成员可以被友元程序集访问,所以需要认真考虑类型成员的可访问性以及将哪个程序集声明为友元程序集。注意,C#编译器在编译友元程序集(没有包含属性InternalsVisibleTo的程序集)时,需要使用编译器开关/out:。使用编译器开关的原因在于,编译器需要知道要编译的程序集的名称,从而确定是否将最后所得到的程序集作为友元程序集。或许你认为C#编译器会主动地确定最后得到的程序集是不是友元程序集,虽然C#编译器通常主动地确定输出文件的名称;但是,C#编译器直到代码结束编译时才能确定输出文件的名称。因此,使用/out:编译器开关可以极大地改善编译的性能。同样,如果在编译模块时(与程序集相对)使用C#编译器的/t:module开关,那么该模块将成为友元程序集的一部分,另外还需要使用C#编译器的/moduleassemblyname:开关来编译模块,该开关向编译器声明模块将是哪个程序集的一部分,以便编译器可以允许模块中的代码访问其他程序集中的内部类型。重要提示友元程序集特征只能由在同一时间进度表上发布(ship)的程序集使用,甚至要求程序集一起发布。这是因为友元程序集之间的相互依赖程度很强,以至于在不同时间进度表上发布的友元程序集极有可能导致兼容性问题。如果希望程序集在不同的时间进度表上发布,应将任何一个程序集都可以使用的类设置为public,并通过设置StrongNameIdentityPermission类的LinkDemand请求限制可访问性。6.3 成员的可访问性定义类型(包括嵌套类型)的成员时,可以指定成员的可访问性。成员的可访问性表明目标代码可以合法访问哪些成员。CLR定义了所有可能的可访问性修饰符,但是编程语言在对成员应用可访问性时,挑选了一些修饰符,并将它们术语化以供开发人员使用。例如,CLR使用术语Assembly来表明成员对同一程序集内的所有代码可见,而C#语言将其命名为internal。表6.1展示了6个可以用于成员可访问性的修饰符。表6.1的行按约束性从最高约束private到最低约束public的顺序排列。表6.1 成员的可访问性CLR术语C#术语描 述Privateprivate成员只能由定义该成员的类型中的方法或者该类型的所有嵌套类型中的方法访问Familyprotected成员只能由定义该成员的类型中的方法、该类型的所有嵌套类型中的方法或者该类型的一个派生类型(与程序集无关)的方法访问Family和Assembly(不支持)成员只能由定义该成员的类型中的方法、该类型的所有嵌套类型中的方法或者同一程序集中定义的该类型的所有派生类型中的方法访问Assemblyinternal成员只能由定义该成员的程序集中的方法访问Family或Assemblyprotected internal成员可以由定义该成员的类型的所有嵌套类型、所有派生类型(与程序集无关)的方法或者定义该成员的程序集中的所有方法访问Publicpublic成员可以由所有程序集的所有方法访问当然,对于任何可访问的成员,都必须定义在一个可见的类型内。例如,如果程序集AssemblyA定义了一个internal类型,该类型拥有一个公共方法,那么程序集AssemblyB中的代码不能调用程序集AssemblyA中的公共方法,因为程序集AssemblyA中的internal类型对于程序集AssemblyB不可见。编译代码时,编程语言的编译器负责检查代码是不是正确地引用了类型和成员。如果代码错误地引用了类型或者成员,那么编译器负责生成一个合适的错误消息。另外,在运行时,JIT编译器在将IL代码编译成本机CPU指令的过程中同样确保对字段和方法的引用是合法的。例如,如果JIT编译器检测到代码正在试图不正当地访问一个私有字段或者方法,那么,JIT编译器将分别抛出一个FieldAccessException或者MethodAccessException 异常。尽管编程语言的编译器忽略可访问性的检查,但是在运行时检验IL代码确保已引用成员的可访问性是值得的,因为存在另一种极有可能的情况,即编程语言的编译器将代码编译成访问另一个类型(另一个程序集)中的公共成员,但是,在运行时却加载了一个不同版本的程序集,而在新版本的程序集中,公共成员的可访问性已经改为protected或者private。在C#中,如果没有显式地声明成员的可访问性,那么,编译器通常(并不总是)将成员的可访问性默认设为private(可访问性修饰符中约束性最强的一个)。CLR要求接口类型的所有成员都是公开的。C#编译器知道这一点,因此禁止编程人员显式指定接口成员的可访问性,它会将所有成员的可访问性设置为public。更多信息更多信息请参考C#语言规范中“已声明的可访问性”一节,了解C#语言可以对类型和成员使用什么类型的可访问性,以及C#语言根据声明位置的上下文选择哪一种默认可访问性。另外,请注意CLR提供了一个称为Family and Assembly的可访问性,但是,C#语言并不支持这个可访问性,因为C#团队认为不需要这个可访问性,因此决定不在C#语言中集成它。派生类型重写在基础类型中定义的成员时,C#编译器要求原始成员和重写成员拥有相同的可访问性。也就是说,如果基类中成员是受保护的,那么派生类中的重写成员也必须是受保护的。但是,这只是C#语言的约束,而不是CLR的约束。当从基类派生类时,CLR允许成员的可访问性的约束性变得较低,而不允许变得较高。例如,类可以重写在其基类中定义的受保护的方法,并将重写的方法设置为public(更容易访问)。但是,类不能重写在其基类中定义的受保护的方法,并将重写的方法设置为private(更难访问)。类不能将基类方法的可访问性设置得更严格,因为派生类的用户通常可以强制转换基础类型来获得对基类方法的访问。如果CLR允许了派生类型的方法更难访问,那么,CLR将声称该方法不能强制实施。6.4 静 态 类CLR中有一些永远不需要实例化的类,例如Console,Math,Environment和ThreadPool类。这些类仅拥有静态成员,实际上,这些类只是将一组相关的成员组合在一起。例如,Math类定义了一组与数学运算相关的方法。C#允许通过使用C#中的关键字static定义非实例化(non-instantiable)的类。关键字static仅可以用于类,而不能用于结构(值类型),这是因为CLR要求值类型必须实例化,并且没有方法停止或阻止该实例化过程。C#编译器对静态类强加如下所示:l 静态类必须直接从基类System.Object派生,这是因为从其他基类派生的类由于继承性仅适用于对象的缘故而没有任何意义,而且不能创建静态类的实例。l 静态类不能实现任何接口,这是因为只有使用类的实例时才去调用类的接口 方法。l 静态类只能定义静态成员(字段、方法、属性和事件),任何实例成员都将导致编译器产生错误。l 静态类不能用作字段、方法参数或者局部变量,这是因为这些用法都将表明变量引用了实例,而这是不允许的。如果编译器检测到这样的用法,编译器就会产生一个错误。l 下面的代码是静态类的范例,该类定义了一些静态成员;该代码可以通过编译(不过会出现警告信息),但是这个类没有做任何有意义的事情。using System; 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; 如果将上述代码编译为库(DLL)程序集,并通过ILDasm.exe查看结果,会发现结果如图6.2所示。通过使用关键字static定义的类将导致C#编译器将该类同时标记为abstract和sealed。另外,编译器不会在类型中生成实例构造器方法。注意,图6.2中没有实例构造器(.ctor)方法。图6.2 ILDasm.exe表明类在元数据中是抽象、密封的6.5 部分类、结构和接口本节将讨论部分类(partial class)、结构和接口。此处需要注意C#编译器完全支持该特征(其他编译器同样也支持该特征),但CLR不需要了解部分类、结构和接口。实际上,由于本书关注的重点是C#所支持的CLR特性,所以为了完整性增加了本节的内容。关键字partial向C#编译器声明某个单独类、结构或者接口定义的源代码可以划分为一个或者多个源代码文件。将某个类型的源代码分隔成多个文件主要有如下两大原因:l 源代码控制(Source control) 假定某个类型的定义由许多源代码组成,而且有一程序开发人员在没有使用源代码控制方法校验类型定义的情况下对源代码进行了签出(check out)。今后,其他程序开发人员如果不对源代码进行合并,就不能对类型进行修改。使用关键字partial可以将类型的代码分为多个源代码文件,每个文件都可以单独签出,因此多个程序开发人员可以同时对类型进行编辑。l 代码拆分器(Code splitter) 在Microsoft Visual Studio中创建新的Windows窗体(form)或者Web窗体项目时,就会在项目中自动生成一些源代码文件。这些源代码文件中包含可以直接用于构建这些类型的项目的模板。使用Visual Studio设计中心在Windows窗体或者Web窗体上拖放控件时,Visual Studio自动编写源代码,并且将代码分别写入多个源代码文件中,这种方法确实提高了工作效率。事实是生成的代码写入同一个正在使用的源代码文件中,这种情况的问题就在于如果偶然编辑了生成的代码,可能会导致设计中心功能紊乱。以Visual Studio 2005为例,创建新的项目时,Visual Studio创建两个源代码文件:一个用于用户的代码,另一个用于设计中心生成的代码。因为设计中心生成的代码位于一个单独的文件,所以偶然编辑它的可能性就会很小。两个文件的类型中都采用关键字partial。在将这两个文件编译在一起的过程中,编译器将代码组合,在最后所得到的结果.exe或.dll程序集(或者.netmodule模块文件)中生成一个类型。正如本节前面所述,C#编译器完全支持部分类型(partial type)特征,而CLR却完全不支持部分类型,这就是为什么某个类型的所有源代码文件必须使用同一种编程语言的原因,而且所有的源代码文件必须编译成一个单独的编译单元。6.6 组件、多态和版本控制面向对象的编程语言(Object-Oriented Programming,OOP)已经面世好多年。在20世纪70年代末、80年代初首次使用面向对象的编程语言时,应用程序的规模非常小,而且应用程序运行所需的全部代码都是由一家公司编写的。的确,应用程序背后还存在操作系统,而且应用程序确实使用了操作系统中可以使用的功能,但在当时,操作系统提供的功能与今天的操作系统相比确实少得可怜。今天,软件变得相当复杂,而且用户要求应用程序提供更丰富的功能,如GUI、菜单、鼠标输入、手写板输入、打印输出、网络功能等。出于这种原因,近几年操作系统和开发平台已经取得实质性的进展。另外,对于应用程序的开发人员想按照用户的期望编写应用程序的所有代码已经不再可行,而且成本上也不划算。今天,应用程序由许多家不同的公司编写的代码组成,然后使用面向对象的规范结合在一起。组件软件编程(Component Software Programming,CSP)让OOP达到了这样的境界,下面是组件的相关属性:l 组件(.NET中称为程序集)具有“发布”的含义。l 组件拥有标识(identity)(名称、版本、语言文化和公钥)。l 组件永远保存这些标识(程序集中的代码永远不会静态地连接到另一个程序集;.NET通常使用动态链接)。l 组件清楚地表明它所依赖的组件(引用元数据表)。l 组件应为它的类和成员存档。C#语言使用源代码内的XML文档编制以及编译器的/doc命令行开关提供存档功能。l 组件必须指定所需的安全权限,CLR中的代码访问安全性(Code Access Security,CAS)功能提供该机制。l 组件发布一个对任何修订(servicing)都不会改变的接口(对象模型)。修订是组件的新版本,它的意图是与组件的原始版本向后兼容。通常,修订版包括程序错误修复、安全补丁或者一些小功能的增强,但是修订不需要任何新的依赖关系,或者额外的安全权限。如最后一点所述,CSP的很大部分用来处理版本控制。组件随着时间不断改变,而且组件按照不同的时间进度表其发布也不同。版本控制对CSP提出了一个全新的复杂性问题,该问题在OOP中不存在,这是因为在OOP中,所有的代码都是由一家单独的公司编写、测试,并且作为一个单独的单元发布。本节将讨论组件的版本控制。在.NET中,版本号包含4个部分:主版本号、次版本号、内部版本号和修订版本号。例如,版本号为的程序集,其主版本号为1,次版本号为2,内部版本号为3,修订版本号为4。主/次版本号通常用于表示程序集的标识,内部/修订版本号通常用于表示程序集的修订版。假定某个公司安装了一个版本号为的程序集。如果该公司稍后想修改该组件中的错误,那么该公司将会生成一个新的程序集,新程序集的版本的内部/修订版本号将会发生改变,如4。这表明该程序集是一个修订版,它与原始版本的组件(版本)向后兼容。从另一方面来讲,如果该公司想生成一个新版本的程序集,而且该版本的程序集有非常明显的改变,另外还不准备与程序集的原始版本向后兼容,那么,该公司将真正创建一个全新的组件,新的程序集的版本号中的主/次版本号应与已有组件的版本号不同(如)。 注意此处只是说明了如何考虑程序集的版本号。然而,CLR并不是按照这种方式对待版本号的。现在,CLR将版本号看作一个不透明的值,如果某个程序集依赖于另一个版本号为的程序集,那么CLR仅试图加载版本号为的程序集(除非此处存在一个重定向的绑定)。但是,Microsoft计划在未来的版本中改变CLR的加载器(loader),以便加载程序集时,对于指定的主/次版本号的程序集可以加载最新的内部/修订版本号的程序集。例如,在CLR的未来版本中,如果加载器试图寻找版本号为的程序集,并且存在一个版本号为的程序集,那么加载器将自动挑选程序集最新的修订版。这将是CLR加载器最受欢迎的改进,我们翘首以待。前面讨论了如何使用版本号来更新组件的标识,以便反映出组件的新版本。下面要讨论CLR和编程语言(如C#)提供的一些特征,这些特征允许程序开发人员编写的代码不受所用组件可能发生的变化的影响。当某个组件(程序集)中定义的类型在另一个组件(程序集)中用作基类时,版本控制问题便随之而来。非常明显,如果发生改变的基类的版本低于派生类的版本,派生类的行为同样会发生改变,且可能会使类的行为不正常。在多态情形中,派生类型重写基础类型中定义的虚方法,绝对会发生这种现象。C#提供了5个可以用于类型或者类型成员的影响组件版本控制的关键字,这些关键字与CLR中支持组件版本控制的特征直接匹配。表6.2给出了与组件版本控制相关的C#的关键字,并且指出了每个关键字是如何影响类型和类型成员的定义的。表6.2 C#语言中的关键字及对组件版本控制的影响方法C#关键字类 型方法/属性/事件常量/字段abstract表示该类型不能构建实例表示在构建派生类型的实例之前派生类型必须重写并实现这个成员(不允许)virtual(不允许)表示这个成员可以由派生类型重写(不允许)override(不允许)表示派生类型重写了基础类型的成员(不允许)sealed表示该类型不能用作基础类型表示这个成员不能被派生类型重写,该关键字仅用于重写了虚方法的方法(不允许)new应用于嵌套类型、方法、属性、事件、常量或者字段时,表示该成员与基类中类似的成员没有关系在6.6.3节“类型版本控制过程中虚方法的处理”将示范这些关键字的值和使用方法。但是在讨论版本控制问题之前,我们先讨论一下CLR实际上是如何调用虚方法的。6.6.1 CLR如何调用虚方法、属性和事件本节将讨论方法,但是本节中的讨论也与虚属性(virtual property)和虚事件(virtual event)相关。属性和事件实际上是作为方法实现的,这将在相应的章节进行示范。方法表示在类型(静态方法)或者类型的实例(非静态方法)上执行操作的代码。所有的方法都有名称、签名和返回值(可能为void)。CLR允许类型定义多个名称相同的方法,只要这些方法具有不同的参数集或者不同的返回值。因此可以定义两个具有相同名称、相同参数的方法,只要这两个方法的返回值类型不同。但是,除了IL汇编语言外,好像没有其他语言具有该“特征”,大多数语言(包括C#语言在内)在确定惟一性时,要求方法的参数不同,而忽略方法返回值的类型。(C#在定义转换操作符方法时实际上放松了此限制,详见第8章的介绍。)下面示范的Employee类定义了3个不同种类的方法:internal class Employee /非虚实例方法 public Int32 GetYearsEmployed() . /虚方法(虚拟隐含着实例) public virtual String GenProgressReport() . /静态方法 public static Employee Lookup(String name) . 当编译器编译上述代码时,编译器在最后得到的程序集的方法定义表中写入三个条目,每个条目都有一个标记来表明方法是实例、虚的还是静态的。所编写的代码调用这些方法时,编译器生成的调用代码检查方法定义的标记,以此来确定如何生成正确的IL代码,以便正确进行调用。CLR为方法的调用提供了以下两个IL指令:l IL指令call可以用来调用静态方法、实例方法和虚方法。使用call指令调用静态方法时,必须指定CLR要调用的方法的类型。使用call指令调用实例方法或者虚方法时,必须指定变量来引用对象。call指令假定变量不为null,换句话说,也就是变量本身的类型指出了用什么类型定义CLR要调用的方法。如果变量的类型没有定义方法,则检查基础类型来匹配方法。指令call通常用来非虚拟地调用虚方法。l IL指令callvirt用来调用实例方法和虚方法,而不能调用静态方法。使用callvirt指令调用实例方法或者虚方法时,必须指定变量来引用对象。使用IL指令callvirt指令调用非虚实例方法时,变量的类型指出了用什么类型定义CLR要调用的方法。使用IL指令callvirt调用虚实例方法时,CLR查找用来调用的对象的实际类型,然后多形式地调用方法。为了决定类型,用来调用的变量通常不能为null,换句话说,也就是编译该调用时,JIT编译器生成验证变量是否为null的代码,如果变量为null,callvirt指令引发CLR抛出一个NullReferenceException异常。这种额外的检查意味着IL指令callvirt的执行速度比call指令稍慢。注意,即使callvirt指令用来调用非虚实例方法时,也要执行这种变量是否为null的检查。现在,我们将这两个调用指令放在一起,看看C#是如何使用这两个不同的IL指令的:using System; public sealed class Program public static void Main() Console.WriteLine();/调用一个静态方法 Object o = new Object(); o.GetHashCode();/调用一个虚实例方法 o.GetType();/调用一个非虚实例方法 编译上述代码,查看最后得到的IL代码,结果如下所示:.method public hideby sigstatic void Main() cil managed .entrypoint /代码大小 26(0x1a) .maxstack 1 .locals init(objectV_0) IL_0000: call void System.Console:WriteLine() IL_0005: newobj instance void System.Object:.ctor() IL_000a: stloc.0 IL_000b: ldloc.0 IL_000c: callvirt instance int32 System.Object:GetHashCode() IL_0011: pop IL_0012: ldloc.0 IL_0013: callvirt instance class System.Type System.Object:GetType() IL_0018: pop IL_0019: ret /Program:Main方法结束首先注意,C#编译器使用IL指令call调用Console的WriteLine方法,这与期望是相符的,因为WriteLine方法是静态方法。接着注意,C#编译器使用IL指令callvirt调用GetHashCode方法,这也与期望相符,因为GetHashCode方法是虚方法。最后注意,C#编译器同样使用IL指令callvirt调用GetType方法,这令人惊讶,因为GetType方法不是虚方法。但是,该调用可以正常调用,这是因为JIT编译上述代码时,CLR知道GetType方法不是虚方法,因此,JIT编译的代码自然将以非虚的方式调用GetType方法。当然,问题在于,为什么C#编译器只生成call指令,而不是其他指令呢?答案就是因为C#的工作组决定JIT编译器应生成验证所使用的对象的代码,以确定调用不为null。这意味着对非虚的实例方法的调用要比其应有的运行速度稍慢一点,同样这也意味着下面的C#代码将抛出一个NullReferenceException异常。在其他一些编程语言中,下述代码将正确运行。using System; public sealed class Program public Int32 GetFive()return5; public static void Main() Program p = null; Int32 x = p.GetFive();/在C#中,会抛出一个NullReferenceException异常 从理论上讲,上述代码运行良好。的确,变量p为null,但是当调用非虚方法(如GetFive)时,CLR仅需要了解p的数据类型(p的数据类型为Program)。如果确实调用了GetFive方法,this参数的值将为null。因为GetFive方法中没有使用这个参数,因此不会抛出NullReferenceException异常。但是,因为C#编译器生成了一个callvirt指令,而不是生成了一个call指令,所以上述代码将抛出一个NullReferenceException异常并结束。重要提示如果将某个方法定义为非虚拟的,那么,将来永远不能将方法改为虚拟的。这是因为某些编译器会使用call指令而不是callvirt指令来调用非虚拟的方法。如果将方法从非虚拟的改为虚拟的,而且没有重新编译所涉及的代码,那么虚方法将被非虚拟地调用,致使应用程序产生无法预测的行为。如果所涉及的代码是用C#编写的,这就不是一个问题了,因为C#使用callvirt指令调用所有的实例方法。但是,如果所涉及的代码使用了不是C#的其他编程语言,这将产生问题。有时,编译器会使用call指令代替callvirt指令来调用虚方法。起初这可能会令人惊讶,但是下述代码将说明为什么有时需要这么做:internal class SomeClass /ToString是一个定义在基类Object中的虚方法 public override String ToString() /编译器使用IL指令call以非虚拟的方式调用object的ToString方法 /如果编译器用callvirt指令取代call指令,那么该方法将递归地调用其本身,直至堆栈溢出 return base.ToString(); 调用虚方法base.ToString时,C#编译器生成一个call指令来确保非虚拟地调用基础类型中的ToString方法。需要这样做的原因在于:如果虚拟地调用ToString方法,那么调用将会递归执行,直至线程的堆栈溢出,这明显不是希望的结果。编译器在调用值类型定义的方法时倾向于使用指令call,因为值类型是密封的。这意味着即使对于虚方法,也不存在多态,这将改善调用的性能,使调用速度更快。另外,值类型实例的本质保证了它永远不为null,因此永远不会抛出NullReferenceException异常。最后,如果虚拟地调用值类型的虚方法,那么,CLR为了在其内部引用方法表,需要引用值类型的类型对象,这需要对值类型进行装箱(boxing)。装箱给堆栈增加了更多的压力,强制进行更频繁的垃圾收集,使性能受到影响。无论是否使用call指令和callvirt指令来调用实例方法或者虚方法,这些方法通常接收一个隐藏的this参数作为方法的第一个参数。this参数引用要进行操作的对象。在设计类型的过程中,应尽量减小所定义的虚方法的数量。首先,调用虚方法的速度比调用非虚方法的速度要慢;其次,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, 0, m_length); /功能最丰富的方法是虚拟的,它可以被重写 public virtual Int32 Find(Object value, Int32 startIndex, Int32 endIndex) /重写的实际实现在此实现 /其他方法在此实现 6.6.2 巧妙使用类型的可见性和成员的可访问性对于.NET Framework,应用程序由多个不同公司制作的程序集中定义的类型组成。这意味着开发人员很少对所使用的组件以及这些组件中定义的类型进行控制。开发人员通常无法访问源代码(甚至都不知道组件是使用何种编程语言创建的),而且组件对于不同的时间进度表其版本也不同。另外,由于多态和受保护的成员,基类开发人员必须信任派生类开发人员所编写的代码。理所当然,派生类的开发人员也必须信任所继承的基类的代码。在设计组件和类型时,不得不实际考虑一下这些问题。本节中,我们将用较少的篇幅介绍一下如何在设计类型时考虑这些问题。具体来讲,我们重点介绍设置类型的可见性和成员的可访问性的正确方式,以便你一举成功。首先,定义一个新的类型时,编译器应将类默认密封,使类不能用作基类。然而,许多编译器,包括C#编译器,默认方式是非密封类,但是允许程序开发人员通过使用关键字sealed显式地将类标

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论