第 21 章 托管堆和垃圾回收

第 21 章 托管堆和垃圾回收 本章内容 托管堆基础 代:提升性能 使用需要特殊清理的类型 手动监视和控制对象生存期 本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存。简单地说,本章要解释 CLR 中的垃圾回收器是如何工作的,还要解释相关的性能问题。另外,本章讨论了如何设计应用程序来最有效地使用内存。 21.1 托管堆基础 每个程序都要使用这样或那样的资源,包括文件、内存缓冲区、屏幕空间、网络连接、数据库资源等。事实上,在面向对象的环境中,每个类型都代表可供程序使用的一种资源。要使用这些资源,必须为代表资源的类型分配内存。以下是访问一个资源所需的步骤。 调用 IL 执行 newobj,为代表资源的类型分配内存(一般使用 C# new 操作符来完成)。 初始化内存,设置资源的初始状态并使资源可用。类型的实例构造器负责设置初始状态。 访问类型的成员来使用资源(有必要可以重复)。 摧毁资源的状态以进行清理。 释放内存。垃圾回收器独自负责这一步。 如果需要程序员手动管理内存(例如,原生 C++ 开发人员就是这样的),这个看似简单的模式就会成为导致大量编程错误的“元凶”之一。想想看,有多少次程序员忘记释放不再需要的内存而造成内存泄漏?又有多少次视图使用已经释放的内存,然后由于内存被破坏而造成程序错误和安全漏洞?而且,这两种bug比其他大多数 bug 都要严重,因为一般无法预测他们的后果或发生的时间①。如果是其他bug, 一旦发现程序行为异常,改正出问题的代码行就可以了。 ① 例如,访问越界的bug 可能取回不相干的数据,使程序结果变得不正确。而且错误没有规律,让人捉摸不定。 ————译注 现在,只要写的是可验证的、类型安全的代码(不要用 C# unsafe 关键字),应用程序就不可能会出现内存被破坏的情况。内存仍有可能泄露,但不像以前那样是默认行为。现在内存泄漏一般是因为在集合中存储了对象,但不需要对象的时候一直不去删除 为了进一步简化编程,开发人员经常使用的大多数类型都不需要步骤 4 (摧毁资源的状态以进行清理)。所以,托管堆除了能避免前面提到的 bug,还能为开发人员提供一个简化的编程模型;分配并初始化资源并直接使用。大多数类型都无需资源清理,垃圾回收器会自动释放内存。 使用需要特殊清理的类型时,编程模型还是像刚才描述的那样简单。只是有时需要尽快清理资源,而不是非要等着 GC ①介入。可在这些类中调用一个额外的方法(称为 Dispose),按照自己的节奏清理资源。另一方面,实现这样的类需要考虑到较多的问题(21.4 节会详细讨论)。一般只有包装了本机资源(文件、套接字和数据库连接等)的类型才需要特殊清理。 ① 垃圾回收、垃圾回收器都可以简称为 GC。 ———— 译注 21.1.1 从托管堆分配资源 CLR 要求所有对象都从托管堆分配。进程初始化时,CLR 划出一个地址空间区域作为托管堆。CLR 还要维护一个指针,我把它称作 NextObjPtr。该指针指向下一个对象在堆中的分配位置。刚开始的时候,NextObjPtr 设为地址空间区域的基地址。 一个区域被废垃圾对象填满后,CLR 会分配更多的区域。这个过程一直重复,直至整个进程地址空间都被填满。所以,你的应用程序的内存受进程的虚拟地址空间的限制。32 为进程最多能分配 1.5 GB,64 位进程最多能分配 8 TB。...

2024-11-20 · 23 分钟 · SAM

第 20 章 异常和状态管理

第 20 章 异常和状态管理 本章内容 定义“异常” 异常处理机制 System.Exception 类 FCL 定义的异常类 抛出异常 定义自己的异常类 用可靠性换取开发效率 设计规范和最佳实践 未处理的异常 对异常进行调试 异常处理的性能问题 约束执行区域(CER) 代码协定 本章重点在于错误处理,但并非仅限与此。错误处理要分几个部分。首先要定义到底什么是错误。然后要讨论如何判断正在经历一个错误,以及如何从错误中恢复。这个时候,状态就成为一个要考虑的问题,因为错误常常在不恰当的时候发生。代码可能在状态改变的中途发生错误。这时需要将一些状态还原为改变之前的样子。当然,还要讨论代码如何通知调用者有错误发生。 在我看来,异常处理是 CLR 最薄弱的一个环节,造成开发人员在写托管代码时遇到许多问题。经过多年的发展,Microsoft 确实进行了一系列显著的改进来帮助开发人员处理错误。但我认为在获得一个真正良好、可靠的系统之前, Microsoft 仍有大量工作要做。针对未处理的异常、约束执行区域(constraind execution region, CER)、代码协定、运行时包装的异常以及未捕捉的异常,本章要讨论处理它们时的改进。 20.1 定义"异常" 设计类型时要想好各种使用情况。类型名称通常是名词,例如 FileStream 或者 StringBuilder。 然后要为类型定义属性、方法、事件等。这些成员的定义方式(属性的数据类型、方法的参数、返回值等)就是类型的编程接口。这些成员代表本身或者类型实例能执行的行动。行动成员通常用动词表示,例如 Read,Write,Flush,Append,Insert 和 Remove等。当行动成员不能完成任务时,就应抛出异常。 重要提示 异常时指成员没有完成它的名称所宣称的行动。 例如以下类定义: 1 2 3 4 5 6 internal class Account { public static void Transfer(Account from, Account to, Decimal amount) { from -= amount; to += amount; } } Transfer 方法接受两个 Account 对象和一个代表账号之间转账金额的 Decimal 值。显然,Transfer 方法的作用是从一个账户扣除钱,把钱添加到另一个账户中。Transfer 方法可能因为多种原因而失败。例如,from或to实参可能为null; from 或 to 实参引用的可能不是活动账户;from 账户可能没有足够的资金;to账户的资金可能过多,以至于增加资金时导致账户溢出;amount 实参为 0、负数或者小数超过两位。...

2024-11-20 · 23 分钟 · SAM

第 19 章 可空值类型

第 19 章 可空值类型 本章内容 C# 对可空值类型的支持 C# 的空接合操作符 C# 对可空值类型的特殊支持 我们知道值类型的变量永远不会为 null;它总是包含值类型的值本身。事实上,这正是“值类型”一次的由来。遗憾的是,这在某些情况下会成为问题。例如,设计数据库时,可将一个列的数据类型定义成一个 32 位整数,并映射到 FCL(Framework Class Library)的 Int32 数据类型。但是,数据库中的一个列可能允许值为空;也就是说,该列在某一行上允许没有任何值。用 Microsoft .NET Framework 处理数据库数据可能变得很困难,因为在 CLR 中,没有办法将 Int32 值表示成 null。 注意 Microsoft ADO.NET 的表适配器(table adapter)确实支持可空类型。遗憾的是 System.Data.SqlTypes 命名空间中的类型没有用可空类型替换,部分原因是类型之间没有“一对一”的对应关系。例如,SqlDecimal 类型最大允许 38 位数,而普通的 Decimal 类型最大允许 38 位数,而普通的 Decimal 类型最大只允许 29 位数。此外, SqlString 类型支持它自己的本地化和比较选项,而普通的 String 类型并不支持这些。 下面是以另一个例子:Java 的 java.util.Date 类是引用类型,所以该类型的变量能设为 null。但 CLR 的 System.DateTime 是值类型,DateTime 变量永远不能设为 null。如果用 Java 写的一个应用程序想和运行 CLR 的 Web 服务交流日期/时间,那么一旦 Java 程序发送 null,就会出问题,因为 CLR 不知道如何表示 null,也不知道如何操作它。...

2024-11-20 · 6 分钟 · SAM

第 18 章 定制特性

第 18 章 定制特性 本章内容 使用定制特性 定义自己的特性类 特性构造器和字段/属性数据类型 检测定制特性 两个特性实例的相互匹配 检测定制特性时不创建从 Attribute 派生的对象 条件特性类 本章讨论 Microsoft .NET Framework 提供的最具创意的功能之一:定制特性(custom attribute)。利用定制特性,可宣告式地为自己的代码构造添加注解来实现特殊功能。定制特性允许为几乎每一个元数据表记录项定义和应用信息。这种可扩展的元数据信息能在运行时查询,从而动态改变代码的执行方式。使用各种 .NET Framework 技术(Windows 窗体、 WPF 和 WCF 等),会发现它们都利用了定制特性,目的是方便开发者在代码中表达式他们的意图。任何 .NET Framework 开发人员都有必要完全掌握定制特性。 18.1 使用定制特性 我们都知道能将 public,private,static 这样的特性应用于类型和成员。我们都同意应用特性具有很大的作用。但是,如果能定义自己的特性,会不会更有用?例如,能不能定义一个类型,指出该类型能通过序列化来进行远程处理?能不能将特性应用于方法,指出执行该方法需要授予特定安全权限? 为类型和方法创建和应用用户自定义的特性能带来极大的便利。当然,编译器必须理解这些特性,才能在最终的元数据中生成特性信息。由于编译器厂商一般不会发布其编译器产品的源代码,所以 Microsoft 采取另一种机制提供对用户自定义特性的支持。这个机制称为定制特性。它的功能很强大,在应用程序的设计时和运行时都能发挥重要作用。任何人都能定义和使用定制特性。另外,面向 CLR 的所有编译器都必须识别定制特性,并能在最终的元数据中生成特性信息。 关于自定义特性,首先要知道它们只是将一些附加信息与某个目标元素关联起来的方式。编译器在托管模块的元数据中生成(嵌入)这些额外的信息。大多数特性对编译器来说没有意义;编译器只是机械地检测源代码中的特性,并生成对应的元数据。 .NET Framework 类库(FCL) 定义了几百个定制特性,可将它们应用于自己源代码中的各种元素。下面是一些例子。 将 DllImport 特性应用于方法,告诉 CLR 该方法的实现位于指定 DLL 的非托管代码中。 将 Serializable 特性应用于类型,告诉序列化格式化器①一个实例的字段可以序列化和反序列化。 ① “格式化器”是本书的译法,文档翻译成“格式化程序”。格式化器是实现了 System.Runtime.Serialization.IFormatter 接口的类型,它知道如何序列化和反序列化一个对象图。————译注 将 AssemblyVersion 特性应用于程序集,设置程序集的版本号。 将 Flags 特性应用于枚举类型,枚举类型就成了位标志(bit flag)集合。 以下 C# 代码应用了大量特性。在 C# 中,为了将定制特性应用于目标元素,要将特性放置于目标元素前的一对方括号中。代码本身做的事情不重要,重要的是对特性有一个认识。...

2024-11-20 · 12 分钟 · SAM

第 17 章 委托

第 17 章 委托 本章内容: 初始委托 用委托回调静态方法 用委托回调实例方法 委托揭秘 用委托回调许多方法(委托链) 委托定义不要太多(泛型委托) C# 为委托提供的简化语法 委托和反射 本章要讨论回调函数。回调函数式一种非常有用的编程机制,它的存在已经有很多年了。Microsoft .NET Framework 通过 委托来提供回调函数机制。不同于其他平台(比如非托管C++)的回调机制,委托的功能要多得多。例如,委托确保回调方法是类型安全的(这是 CLR 最重要的目标之一)。委托还允许顺序调用多个方法,并支持调用静态方法和实例方法。 17.1 初始委托 C “运行时”的 qsort 函数获取指向一个回调函数的指针,以便对数组中的元素进行排序。在 Microsoft Windows 中,窗口过程、钩子过程和异步过程调用等都需要回调函数。在 .NET Framework 中,回调方法的应用更是广泛。例如,可以登记回调方法来获得各种各样的通知,例如未处理的异常、窗口状态变化、菜单项选择、文件系统变化、窗体控件事件和异步操作已完成等。 在非托管 C/C++ 中,非成员函数的地址只是一个内存地址。这个地址不携带任何额外的信息,比如函数期望收到的参数个数、参数类型、函数返回值类型以及函数的调用协定。简单地说,非托管 C/C++ 回调函数不是类型安全的(不过它们确实是一种非常轻量级的机制)。 .NET Framework 的回调函数和非托管 Windows 编程环境的回调函数一样有用,一样普遍。但是,.NET Framework 提供了称为委托的类型安全机制。为了理解委托,先来看看如何使用它。以下代码①演示了如何声明、创建和使用委托: ① 这个程序最好不要通过在 Visual Studio 中新建 “Windows 窗体应用程序”项目来生成。用文本编辑器输入代码,另存为 name.cs。启动“VS2013 开发人员命令提示”,输入 csc name.cs 生成,输入 name 执行。这样可同时看到控制台和消息框的输出。 —— 译注 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 using System; using System....

2024-11-20 · 15 分钟 · SAM

第 16 章 数组

第 16 章 数组 本章内容 初始化数组元素 数组转型 所有数组都隐式派生自 System.Array 所有数组都隐式实现 IEnumerable、ICollection 和 IList 数组的传递和返回 创建下限非零的数组 数组的内部工作原理 不安全的数组访问和固定大小的数组 数组是允许将多个数据项作为集合来处理的机制。CLR 支持一维、多维和交错数组(即数组构成的数组)。所有数组类型都隐式地从 System.Array 抽象类派生,后者又派生自 System.Object。这意味着数组始终是引用类型,是在托管堆上分配的。在应用程序的变量或字段中,包含的是对数组的引用,而不是包含数组本身的元素。下面的代码更清楚地说明了这一点: 1 2 Int32[] myIntegers; // 声明一个数组引用 myIntegers = new Int32[100]; // 创建含有 100 个 Int32 的数组 第一行代码声明 myIntegers 变量,它能指向包含 Int32 值的一维数组。myIntegers 刚开始设为 null,因为当时还没有分配数组。第二行代码分配了含有 100 个 Int32 值的数组,所有 Int32 都被初始化为 0。由于数组是引用类型,所以会在托管堆上分配容纳 100 个未装箱Int32所需的内存块。实际上,除了数组元素,数组对象占据的内存块还包含一个类型对象指针、一个同步块索引和一些额外的成员①。该数组的内存块地址被返回并保存到myIntegers变量中。 ① 这些额外的成员称为 overhead 字段或者说“开销字段”。 —— 译注 还可创建引用类型的数组: 1 2 Control[] myControls; // 声明一个数组引用 myControls = new Control[50]; // 创建含有 50 个 Control 引用的数组 第一行代码声明myControls 变量,它能指向包含 Control 引用的一维数组。myControls 刚开始被设为 null,因为当时还没有分配数组。第二行代码分配了含有 50 个 Control 引用的数组,这些引用全被初始化为null。由于 Control 是引用类型,所以创建数组只是创建了一组引用,此时没有创建实际的对象。这个内存块的地址被返回并保存到 myControls 变量中。...

2024-11-20 · 10 分钟 · SAM

第 15 章 枚举类型和位标志

第 15 章 枚举类型和位标志 本章内容: 枚举类型 位标志 为枚举类型添加方法 本章要讨论枚举类型和位标志。由于 Microsoft Windows 和许多编程语言多年来一直在使用这些结构,相信许多人已经知道了如何使用它们。不过,CLR 与 FCL 结合起来之后,枚举类型和位标志才正成为面向对象的类型。而它们提供的一些非常“酷”的功能,我相信大多数开发人员并不熟悉。让我惊讶的是,这些新功能极大地简化了应用程序开发,个中缘由且听我娓娓道来。 15.1 枚举类型 枚举类型(enumerated type)定义了一组“符号名称/值”配对。例如,以下 Color 类型定义了一组符号,每个符号都标识一种颜色: 1 2 3 4 5 6 7 internal enum Color { White, // 赋值 0 Red, // 赋值 1 Green, // 赋值 2 Blue, // 赋值 3 Orange // 赋值 4 } 当然,也可写程序用 0 表示白色,用 1 表示红色,以此类推。不过,不应将这些数字硬编码到代码中,而应使用枚举类型,理由至少有二。 枚举类型使程序更容易编写、阅读和维护。有了枚举类型,符号名称可在代码中随便使用,程序员不用费心思量每个硬编码值的含义(例如,不用念叨 white 是 0 , 或者 0 是 white)。而且,一旦与符号名称对应的值发生改变,代码也可以简单地重新编译,不需要对源代码进行任何修改。此外,文档工具和其他实用程序(比如调试程序)能向开发人员显示有意义的符号名称。 枚举类型是强类型的。例如,将 Color.Orange 作为参数传给要求 Fruit 枚举类型的方法,编译器会报错。①...

2024-11-20 · 7 分钟 · SAM

第 14 章 字符、字符串和文本处理

第 14 章 字符、字符串和文本处理 本章内容 字符 System.String类型 高效率构造字符串 获取对象的字符串表示:ToString 解析字符串来获取对象:Parse 编码:字符和字节的相互转换 安全字符串 本章将解释在 Microsoft .NET Framework 中处理字符和字符串的机制。首先讨论 System.Char 结构以及处理字符的多种方式。然后讨论更有用的System.String类,它允许处理不可变(immutable)字符串(一经创建,字符串便不能以任何方式修改)。探讨了字符串之后,将介绍如何使用System.Text.StringBuilder类高效地动态构造字符串。掌握了字符串的基础知识之后,将讨论如何将对象格式化成字符串,以及如何使用各种编码方法高效率地持久化或传输字符串。最后讨论System.Security.SecureString类,它保护密码和信用卡资料等敏感字符串。 14.1 字符 在.NET Framework 中,字符总是表示成 16 位 Unicode 代码值,这简化了国际化应用程序的开发。每个字符都是System.Char结构(一个值类型)的实例。System.Char 类型很简单,提供了两个公共只读常量字段:MinValue(定义成 ‘\0’)和MaxValue(定义成'\uffff')。 为Char的实例调用静态GetUnicodeCategory方法返回System.Globalization.UnicodeCategory枚举类型的一个值,表明该字符是由 Unicode标准定义的控制字符、货币符号、小写字母、大写字母、标点符号、数学符号还是其他字符。 为了简化开发,Char类型还提供了几个静态方法,包括IsDigit,IsLetter,IsUpper,IsLower,IsPunctuation,IsLetterOrDigit,IsControl,IsNumber,IsSeparator,IsSurrogate,IsLowSurrogate,IsHighSurrogate和IsSymbol等。大多数都在内部调用了GetUnicodeCategory,并简单地返回true或false。注意,所有这些方法要么获取单个字符作为参数,要么获取一个String以及目标字符在这个String中的索引作为参数。 另外,可调用静态方法 ToLowerInvariant或者ToUpperInvariant,以忽略语言文化(culture)的方式将字符转换为小写或大写形式。另一个方案是调用ToLower和ToUpper方法来转换大小写,但转换时会使用与调用线程关联的语言文化信息(方法在内部查询System.Threading.Thread类的静态 CurrentCulture 属性来获得)。也可向这些方法传递CultureInfo类的实例来指定一种语言文化。ToLower和ToUpper之所以需要语言文化信息,是因为字母的大小写转换时一种依赖于语言文化的操作。比如在土耳其语中,字母U+0069(小写拉丁字母i)转换成大写是 U+0130(大写拉丁字母I,上面加一点),而其他语言文化的转换结果是 U+0049(大写拉丁字母 I)。 除了这些静态方法,Char类型还有自己的实例方法。其中,Equals方法在两个Char实例代表同一个 16 位 Unicode 码位①的前提下返回true。CompareTo方法(由 IComparable和 IComparable<Char>接口定义)返回两个Char实例的忽略语言文化的比较结果。ConvertFromUtf32方法从一个 UTF-32 字符生成包含一个或两个 UTF-16 字符的字符串。ConvertToUtf32方法从一对低/高低理项或者字符串生成一个 UTF-32 字符。ToString方法返回包含单个字符的一个String。与ToString相反的是Parse/TryParse,它们获取单字符的String,`返回该字符的 UTF-16 码位。 ① 在字符编码术语中,码位或称编码位置,即英文的 code point 或 code position,是组成码空间(或代码页)的数值。例如,ASCII 码包含 128 个码位。——维基百科 最后一个方法是 GetNumericValue,它返回字符的数值形式。以下代码演示了这个方法。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using System; public static class Program { public static void Main() { Double d; // '\u0033'是”数字3“ d = Char....

2024-11-20 · 19 分钟 · SAM

第 13 章 接口

第 13 章 接口 本章内容: 类和接口继承 定义接口 继承接口 关于调用接口方法的更多探讨 隐式和显示接口方法实现(幕后发生的事情) 泛型接口 泛型和接口约束 实现多个具有相同方法名和签名的接口 用显式接口方法实现来增强编译时类型安全性 谨慎使用显式接口方法实现 设计:基类还是接口? 对于多继承(multiple inheritance)的概念,虚度程序员并不陌生,它是指一个类从两个或多个基类派生的能力。例如,假定 TransmitData类的作用是发送数据,ReceiveData类的作用是接收数据。现在要创建SocketPort类,作用是发送和接收数据。在这种情况下,你会希望SocketPort从TransmitData和ReceiveData这两个类继承。 有的编程语言允许多继承,所以能从TransmitData和ReceiveData这两个基类派生出SocketPort。但 CLR 不支持多继承(因此所有托管编程语言也支持不了)。CLR 只是通过 接口提供了“缩水版”的多继承。本章将讨论如何定义和使用接口,还要提供一些指导性原则,以便你判断何时应该使用接口而不是基类。 13.1 类和接口继承 Microsoft .NET Framework 提供了System.Object类,它定义了 4 个公共实例方法:ToString,Equals,GetHashCode 和GetType。该类是其他所有类的根据或者说终极基类。换言之,所有类都继承了Object的 4 个实例方法。这还意味着只要代码能操作Object类的实例,就能操作任何类的实例。 由于 Microsoft 的开发团队已实现了 Object 的方法,所以从Object派生的任何类实际都继承了以下内容。 方法签名 使代码认为自己是在操作Object类的实例,但实际操作的可能是其他类的实例。 方法实现 使开发人员定义Object的派生类时不必手动实现Object的方法。 在 CLR 中,任何类都肯定从一个(而且只能是一个)派生类,后者最终从Object派生。这个类称为基类。基类提供了一组方法签名和这些方法的实现。你定义的新类可在将来由其他开发人员用作基类——所有方法签名和方法实现都会由新的派生类继承。 CLR 还允许开发人员定义接口,它实际只是对一组方法签名进行了统一命名。这些方法不提任何实现。类通过指定接口名称来继承接口,而且必须显式实现接口方法,否则 CLR 会认为此类型定义无效。当然,实现接口方法的过程可能比较烦琐,所以我才在前面说接口继承是实现多继承的一种“缩水版”机制。C#编译器和 CLR 允许一个类继承多个接口。当然,继承的所有接口方法都必须实现。 我们知道,类继承的一个重要特点是,凡是能使用基类型实例的地方,都能使用派生类型的实例。类似地,接口继承的一个重点特点是,凡是能使用具名接口类型的实例的地方,都能使用实现了接口的一个类型的实例。下面先看看如何定义接口。 13.2 定义接口 如前所述,接口对一组方法签名进行了统一命名。注意,接口还能定义事件、无参属性和有参属性(C# 的索引器)。如前所述,所有这些东西本质上都是方法,它们只是语法上的简化。不过,接口不能定义任何构造器方法,也不能定义任何实例字段。 虽然 CLR 允许接口定义静态方法、静态字段、常量和静态构造器,但符合 CLS 标准的接口绝不允许,因为有的编程语言不能定义或访问它们。事实上,C#禁止接口定义任何一种这样的静态成员。 C# 用 Interface 关键字定义接口。要为接口指定名称和一组实例方法签名。下面是 FCL 中的几个接口的定义: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public interface IDisposable { void Dispose(); } public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerable<T> : IEnumerable { new IEnumerator<T> GetEnumerator(); } public interface ICollection<T> : IEnumerable<T>, IEnumerable { void Add(T item); void Clear(); Boolean Contains(T item); void CopyTo(T[] array, Int32 arrayIndex); Boolean Remove(T item); Int32 Count { get; } // 只读属性 Boolean IsReadOnly { get; } //只读属性 } 在 CLR 看来,接口定义就是类型定义。也就是说,CLR 会为接口类型对象定义内部数据结构,同时可通过反射机制来查询接口类型的功能。和类型一样,接口可在文件范围中定义,也可嵌套在另一个类型中。定义接口类型时,可指定你希望的任何可见性/可访问性(public,protected,internal等)。...

2024-11-20 · 10 分钟 · SAM

第 12 章 泛型

第 12 章 泛型 本章内容 FCL 中的泛型 泛型基础结构 泛型接口 泛型委托 委托和接口的逆变和协变泛型类型实参 泛型方法 泛型和其他成员 可验证性和约束 熟悉面向对象编程的开发人员都深谙这种编程方式的好处。其中一个好处是“代码重用”,它极大提高了开发效率。也就是说,可以派生出一个类,让它继承基类的所有能力。派生类只需重写虚方法,或添加一些新方法,就可定制派生类的行为,使之满足开发人员的需求。泛型(generic)是 CLR 和编程语言的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。 简单地说,开发人员先定义好算法,比如排序、搜索、交换、比较或者转换等。但是,定义算法的开发人员并不设定该算法要操作什么数据类型;该算法可广泛地应用于不同类型的对象。然后,另一个开发人员只要指定了算法要操作的具体数据类型,就可以开始使用这个算法了。例如,一个排序算法可操作Int32 和 String 等类型的对象,而一个比较算法可操作DateTime和Version等类型的对象。 大多数算法都封装在一个类型中,CLR 允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。此外,CLR 还允许创建泛型接口和泛型委托。方法偶尔也封装有用的算法,所以 CLR 允许在引用类型、值类型或接口中定义泛型方法。 先来看一个简单的例子。Framework 类库(Framework Class Library,FCL)定义了一个泛型列表算法,它知道如何管理对象集合。泛型算法没有设定对象的数据类型。要在使用这个泛型列表算法时指定具体数据类型。 封装了泛型列表算法的FCL类称为 List<T>(读作 List of Tee)。这个类是在 System.Collections.Generic 命名空间中定义的。下面展示了类定义(代码被大幅简化): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [Serializable] public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable { public List(); public void Add(T item); public Int32 BinarySearch(T item); public void Clear(); public Boolean Contains(T item); public Int32 IndexOf(T item); public Boolean Remove(T item); public void Sort(); public void Sort(IComparer<T> comparer); public void Sort(Comparison<T> comparison); public T[] ToArray(); public Int32 Count { get; } public T this[Int32 index] { get; set; } } 泛型 List 类的设计者紧接在类名后添加了一个<T>,表明它操作的是一个未指定的数据类型。定义泛型类型或方法时,为类型指定的任何变量(比如T)都称为类型参数(type parameter)。T是变量名,源代码能使用数据类型的任何地方都能使用T。例如,在List类定义中,T被用于方法参数(Add方法接受一个T类型的参数)和返回值(ToArray方法返回T类型的一维数组)。另一个例子是索引器方法(在C#中称为this)。索引器有一个get访问器方法,它返回T类型的值;一个set访问器方法,它接受T类型的参数。由于凡是能指定一个数据类型的地方都能使用T变量,所以在方法内部定义一个局部变量时,或者在类型中定义字段时,也可以使用T。...

2024-11-20 · 14 分钟 · SAM