第 11 章 事 件

第 11 章 事 件 本章内容: 设计公开事件的类型 编译器如何实现事件 设计侦听事件的类型 显式实现事件 本章讨论可以在类型中定义的最后一种成员:事件。定义了事件成员的类型允许类型(或类型的实例)通知其他对象发生了特定的事情。例如,Button 类提供了 Click 事件。应用程序中的一个或多个对象可接收关于该事件的通知,以便在Button被单击之后采用特定操作。我们用事件这种类型成员来实现这种类型成员来实现这种交互。具体地说,定义了事件成员成员的类型能提供以下功能。 方法能登记它对事件的关注。 方法能注销它对事件的关注。 事件发生时,登记了的方法将收到通知。 类型之所以能提供事件通知功能,是因为类型维护了一个已登记方法的列表。事件发生后,类型将通知列表中所有已登记的方法。 CLR 事件模型以委托为基础。委托是调用①回调方法的一种类型安全的方式。对象凭借回调方法接收它们订阅的通知。本章会开始使用委托,但委托的完整细节是在第 17 章"委托"中讲述的。 这个“调用”(invoke)理解为“唤出”更恰当。它和普通的“调用”(call)稍有不同。在英语的语境中,invoke 和 call 的区别在于,在执行一个所有信息都已知的方法时,用 call 比较恰当。这些信息包括要引用的类型、方法的签名以及方法名。但是,在需要先“唤出”某个东西来帮你调用一个信息不明的方法时,用 invoke 就比较恰当。但是,由于两者均翻译为“调用”不会对读者的理解造成太大的困扰,所以本书仍然采用约定俗成的方式来进行翻译,只是在必要的时候附加英文原文提醒你区分。 —— 译注 为了帮你完整地理解事件在 CLR 中的工作机制,先来描述事件很有用的一个场景。假定要设计一个电子邮件应用程序。电子邮件到达时,用户可能希望将该邮件转发给传真机或寻呼机。先设计名为 MailManager 的类型来接收传入的电子邮件,它公开 NewMail 事件。 其他类型(如 Fax 和 Pager)的对象登记对于该事件的关注。MailManager 收到新电子邮件会引发该事件,造成邮件分发给每一个已登记的对象。每个对象都用它们自己的方式处理邮件。 应用程序初始化时只实例化一个 MailManager 实例,然后可以实例化任意数量的 Fax 和 Pager 对象。图 11-1 展示了应用程序如何初始化,以及新电子邮件到达时发生的事情。 图 11-1 设计使用了事件的应用程序 图 11-1 的应用程序首先构造 MailManager 的一个实例。 MailManager 提供了 NewMail 事件。构造 Fax 和 Pager 对象时,它们向 MailManager 的 NewMail 事件登记自己的一个实例方法。这样当新邮件到达时, MailManager 就知道通知 Fax 和 Pager 对象。MailManager 将来收到新邮件时会引发 NewMail 事件,使自己登记的方法都有机会以自己的方式处理邮件。...

2024-11-20 · 7 分钟 · SAM

第 10 章 属性

第 10 章 属性 本章内容: 无参属性 有参属性 调用属性访问器方法时的性能 属性访问器的可访问性 泛型属性访问器方法 本章讨论属性,它允许源代码用简化语法来调用方法。CLR 支持两种属性;无参属性,平时说的属性就是指它;有参属性,它在不同的编程语言中有不同的称呼。例如,C# 将有参属性称为索引器,Microsoft Visual Basic 将有参属性称为默认属性。还要讨论如何使用“对象和集合初始化器”来初始化属性,以及如何用 C# 的匿名类型和 System.Tuple 类型将多个属性打包到一起。 10.1 无参属性 许多类型都定义了能被获取或更改的状态信息。这种状态信息一般作为类型的字段成员实现。例如,以下类型定义包含两个字段: 1 2 3 4 public sealed class Employee { public String Name; // 员工姓名 public Int32 Age; // 员工年龄 } 创建该类型的实例后,可以使用以下形式的代码轻松获取(get)或设置(set)它的状态信息: 1 2 3 4 5 Employee e = new Employee(); e.Name = "Jeffrey Richter"; // 设置员工姓名 e.Age = 45; // 设置员工年龄 Console.WriteLine(e.Name); // 显示 "Jeffrey Richter" 这种查询和设置对象状态信息的做法十分常见。但我必须争辩的是,永远都不应该像这样实现。面向对象设计和编程的重要原则之一就是数据封装,意味着类型的字段永远不应该公开,否则很容易因为不恰当使用字段而破坏对象的状态。例如,以下代码可以很容易地破坏一个Employee 对象:...

2024-11-20 · 13 分钟 · SAM

第 9 章 参数

第 9 章 参数 本章内容: 可选参数和命名参数 隐式类型的局部变量 以传引用的方式向方法传递参数 向方法传递可变数量的参数 参数和返回类型的设计规范 常量性 本章重点在于向方法传递的各种方式,包括如何可选地指定参数,按名称指定参数,按名称指定参数,按引用传递参数,以及如何定义方法来接受可变数量的参数。 9.1 可选参数和命名参数 设计方法的参数时,可为部分或全部参数分配默认值。然后,调用这些方法的代码可以选择不提供部分实参,使用其默认值。此外,调用方法时刻通过指定参数名称来传递参数。以下代码演示了可选参数和命名参数的用法: 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 using System; public static class Program { private static Int32 s_n = 0; private static void M(Int32 x = 9, String s = "A", DateTime dt = default(DateTime), Guid guid = new Guid()) { Console....

2024-11-20 · 9 分钟 · SAM

第 8 章 方法

第 8 章 方法 本章内容: 实例构造器和类(引用类型) 实例构造器和结构(值类型) 类型构造器 操作符重载方法 转换操作符方法 扩展方法 分部方法 本章重点讨论你将来可能遇到的各种方法,包括实例构造器和类型构造器。还会讲述如何定义方法来重载操作符和类型转换(以进行隐式和显示转型)。还会讨论扩展方法,以便将自己的实例在逻辑上“添加”到现在类型中。还会讨论分部方法,允许将类型的实现分散到多个组成部分中。 8.1 实例构造器和类(引用类型) 构造器是将类型的实例初始化为良好状态的特殊方法。构造器方法在“方法定义元数据表”中始终叫做 .ctor(constructor 的简称)。创建引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始状态。 这些附加的字段称为 overhead fields,“overhead”是开销的意思,意味着是创建对象时必须的“开销”。——译注 构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。没有构造器显式重写的所有字段都保证获得 0 或 null值。 和其他方法不同,实例构造器永远不能被继承。也就是说,类只有类自己定义的实例构造器。由于永远不能继承实例构造器,所以实例构造器不能使用以下修饰符:virtual,new,override,sealed 和 abstract。如果类没有显式定义任何构造器,C#编译器将定义一个默认(无参)构造器。在它的实现中,只是简单地调用了基类的无参构造器。 例如下面这个类: 1 2 public class SomeType { } 它等价于: 1 2 3 public class SomeType { public SomeType() : base() { } } 如果类的修饰符为 abstract,那么编译器生成的默认构造器的可访问性就为 protected;否则,构造器会被赋予 public 可访问性。如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译器会报错。如果类的修饰符为static(sealed 和 abstract),编译器根本不会在类的定义中生成默认构造器。 静态类在元数据中是抽象密封类。 —— 译注 一个类型可以定义多个实例构造器。每个构造器都必须有不同的签名,而且每个都可以有不同的可访问性。为了使代码"可验证"(verifiable),类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器。如果派生类的构造器没有显式调用一个基类构造器,C# 编译器会自动生成对默认的基类构造器的调用。最终,System.Object 的公共无参构造器会得到调用。该构造器什么都不做,会直接返回。由于 System.Object 没有定义实例数据字段,所以它的构造器无事可做。 极少数时候可以在不调用实例构造器的前提下创建类型的实例。一个典型的例子是 Object 的 MemberwiseClone 方法。该方法的作用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),然后将源对象的字节数据复制到新对象中。另外,用运行时序列化器(runtime serializer)反序列化对象时,通常也不需要调用构造器。反序列化代码使用System....

2024-11-20 · 12 分钟 · SAM

第 7 章 常量和字段

第 7 章 常量和字段 本章内容: 常量 字段 本章介绍如何向类型添加数据成员,具体就是常量和字段。 7.1 常量 常量是值从不变化的符号。定义常量符号时,它的值必须能在编译时确定。确定后,编译器将常量值保存到程序集元数据中。这意味着只能定义编译器识别的基元类型的常量。在 C# 中,以下类型是基元类型,可用于定义常量:Boolean,Char,Byte,SByte,Int16,UInt16,Int32,Uint32,Int64,UInt64,Single,Double,Decimal和String。然而,C# 也允许定义非基元类型的常量变量(constant variable),前提是把值设为null: 1 2 3 4 5 6 7 using System; public sealed class SomeType { // SomeType 不是基元类型,但 C# 允许 // 值为 null 的这种类型的常量变量 public const SomeType Empty = null; } 由于常量值从不变化,所以常量总是被视为类型定义的一部分。换言之,常量总是被视为静态成员,而不是实例成员。定义常量将导致创建元数据。 代码引用常量符号时,编译器在定义常量的程序集的元数据中查找该符号,提取常量的值,将值签入生成的 IL 代码中。由于常量的值直接嵌入代码,所以在运行时不需要为常量分配任何内存。除此之外,不能获取常量的地址,也不能以传引用的方式传递常量。这些限制意味着常量不能很好地支持跨程序集的版本控制。因此,只有确定一个符号的值从不变化才应定义常量。(将 MaxInt6定义为32767就是一个很好的例子)。下面来演示我刚才所说的内容。首先,请输入以下代码,并将其编译成一个 DLL 程序集。 1 2 3 4 5 6 7 using System; pulbic sealed class SomeLibraryType { // 注意:C# 不允许为常量指定 static 关键字, // 因为常量总是隐式为 static public const Int32 MaxEntriesInList = 50; } 接着用以下代码生成一个应用程序程序集:...

2024-11-20 · 3 分钟 · SAM

第 6 章 类型和成员基础

第 6 章 类型和成员基础 本章内容 类型的各种成员 类型的可见性 成员的可访问性 静态类 分部类、结构和接口 组件、多态和版本控制 第 4 章和第 5 章重点介绍了类型以及所有类型的所有实例都支持的一组操作,并指出所有类型都可划分为引用类型或值类型。在本章及本部分后续的章节,将解释如何在类型中定义各种成员,从而设计出符合需要的类型。第 7 章 ~ 第 11 章将详细讨论各种成员。 6.1 类型的各种成员 类型中可定义 0 个或多个以下种类的成员。 常量 常量是指出数据值恒定不变的符号。这种符号使代码更易阅读和维护。常量总与类型关联,不于类型的实例关联。常量逻辑上总是静态成员。相关内容在第 7 章“常量和字段”讨论。 字段 字段表示只读或可读的数据值。字段可以是静态的;这种字段被认为是类型状态的一部分。字段也可以是实例(非静态);这种字段被认为是对象状态的一部分。强烈建议将字段声明为私有,防止类型或对象的状态被类型外部的代码破坏。相关内容在第 7 章讨论。 实例构造器 实例构造器是将新对象的实例字段初始化为良好初始状态的特殊方法。相关内容在第 8 章“方法”讨论。 类型构造器 类型构造器是将类型的静态字段初始化为良好初始状态的特殊方法。相关内容在第 8 章讨论。 方法 方法是更改或查询类型或对象状态的函数。作用于类型称为静态方法,作用于对象称为实例方法。方法通常要读写类型或对象的字段。相关内容在第 8 章讨论。 操作符重载 操作符重载实际是方法,定义了当操作符作用于对象时,应该如何操作该对象。由于不是所有编程语言都支持操作符重载,所以操作符重载方法不是“公共语言规范”(Common Language Specification, CLS)的一部分。相关内容在第 8 章讨论。 转换操作符 转换操作符是定义如何隐式或显式将对象从一种类型转型为另一种类型的方法。和操作符重载方法一样,并不是所有编程语言都支持转换操作符,所以不是 CLS 的一部分。相关内容在第 8 章讨论。 属性 属性允许用简单的、字段风格的语法设置或查询类型或对象的逻辑状态,同时保证状态不被破坏。作用于类型称为静态属性,作用于对象称为实例属性。属性可以无参(非常普遍),也可以有多个参数(相当少见,但集合类用得多)。相关内容在第 10 章 “属性”讨论。 事件 静态事件允许类型向一个或多个静态或实例方法发送通知。实例(非静态)事件允许对象向一个或多个静态或实例方法发送通知。引发事件通常是为了响应提供事件的类型或对象的状态的改变。事件包含两个方法,允许静态或实例方法登记或注销对该事件的关注。除了这两个方法,事件通常还用一个委托字段来维护已登记的方法集。相关内容在第 11 章“事件”讨论。 类型 类型可定义其他嵌套类型。通常用这个办法将大的、复杂的类型分解成更小的构建单元(building block)以简化实现。...

2024-11-20 · 10 分钟 · SAM

第 5 章 基元类型、引用类型和值类型

第 5 章 基元类型、引用类型和值类型 本章内容: 编程语言的基元类型 引用类型和值类型 值类型的装箱和拆箱 对象哈希码 dynamic 基元类型 本章将讨论 Microsoft .NET Framework 开发人员经常要接触的各种类型。所以开发人员都应熟悉这些类型的不同行为。我首次接触 .NET Framework 时没有完全理解基元类型、引用类型和值类型的区别,造成在代码中不知不觉引入 bug 和性能问题。通过解释类型之间的区别,希望开发人员能避免我所经历的麻烦,同时提高编码效率。 5.1 编程语言的基元类型 某些数据类型如此常用,以至于许多编译器允许代码以简化语法来操纵它们。例如,可用以下语法分配一个整数: 1 System.Int32 a = new System.Int32(); 但你肯定不愿意用这种语法声明并初始化整数,它实在是太繁琐了。幸好,包括 C# 在内的许多编译器都允许换用如下所示的语法: 1 int a = 0; 这种语法不仅增强了代码可读性,生成的 IL 代码还与使用 System.Int32 生成的 IL 代码完全一致。编译器直接支持的数据类型称为 基元类型(primitive type)。基元类型直接映射到 Framework 类库(FCL)中存在的类型。例如,C# 的 int 直接映射到 System.Int32 类型。因此,以下 4 行代码都能正确编译,并生成完全相同的 IL: 1 2 3 4 int a = 0; // 最方便的语法 System.Int32 a = 0; // 方便的语法 int a = new int(); // 不方便的语法 System....

2024-11-20 · 24 分钟 · SAM

第 4 章 类型基础

第 4 章 类型基础 本章内容: 所有类型都从 System.Object 类型转换 命名空间和程序集 运行时的相互关系 本章讲述使用类型和 CLR 时需掌握的基础知识。具体地说,要讨论所有类型都具有对的一组基本行为。还将讨论类型安全性、命名空间、程序集以及如何将对象从一种类型转换成另一种类型。本章最后会解释类型、对象、线程栈和托管堆在运行时的相互关系。 4.1 所有类型都从 System.Object 派生 “运行时”要求每个类型最终都从 System.Object 类型派生。也就是说,以下两个类型定义完全一致: 1 2 3 4 5 6 7 8 9 // 隐式派生自 Object class Emplpyee { … } // 显示派生自 Object class Emplpyee : System.Object { … } 由于所有类型最终都从 System.Object 派生,所以每个类型的每个对象都保证了一组最基本的方法。具体地说, System.Object 类提供了如表 4-1 所示的公共实例方法。 表 4-1 System.Object 的公共方法 公共方法 说明 Equals 如果两个对象具有相同的值,就返回 true。欲知该方法的详情,请参见 5.3.2 节“对象相等性和同一性” GetHashCode 返回对象的值的哈希码。如果某个类型的对象要在哈希表集合(比如 Dictionary)中作为建使用,类型应重写该方法。方法应该为不同对象提供 良好分布 。将这个方法设计到 Object 中并不恰当。大多数类型永远不会在哈希表中作为键使用;该方法本该在接口中定义。欲知该方法的详情,请参见 5....

2024-11-20 · 8 分钟 · SAM

第 3 章 共享程序集和强命名程序集

第 3 章 共享程序集和强命名程序集 本章内容: 两种程序集,两种部署 为程序集分配强名称 全局程序集缓存 在生成的程序集中引用强命名程序集 强命名程序集能防篡改 延迟签名 私有部署强命名程序集 “运行时”如何解析类型引用 高级管理控制(配置) 第 2 章讲述了生成、打包和部署程序集的步骤。我将重点放在所谓的私有部署(private deployment)上。进行私有部署,程序集放在应用程序的基目录(或子目录),由这个应用程序独享。以私有方式部署程序集,可以对程序集的命名、版本和行为进行最全面的控制。 本章重点是如何创建可由多个应用程序共享的程序集。 Microsoft .NET Framework 随带的程序集就是典型的全局部署程序集,因为所有托管应用程序都要使用 Microsoft 在 .NET Framework Class Library(FCL)中定义的类型。 第 2 章讲过, Windows 以前在稳定性上的口碑很差,主要原因是应用程序要用别人实现的代码进行生成和测试。(想想看,你开发的 Windows 应用程序是不是要调用由 Microsoft 开发人员写好的代码?)另外,许多公司都开发了供别人嵌入的控件。事实上, .NET Framework 鼓励这样做,以后的控件开发商会越来越多。 随着时间的推移, Microsoft 开发人员和控件开发人员会修改代码,这或许是为了修复bug、进行安全更新、添加功能等。最终,新代码会进入用户机器。以前安装好的、正常工作的应用程序突然要面对“陌生”的代码,不再是应用程序最初生成和测试时的代码。因此,应用程序的行为不再是可以预测的,这是造成 Windows 不稳定的根源。 文件的版本控制是个难题。取得其他代码文件正在使用的一个文件,即时只修改其中一位(将 0 变成 1,或者将 1 变成 0),就无法保证使用该文件的代码还能正常工作。使用文件的新版本时,道理是一样的。之所以这样说,是因为许多应用程序都有意或无意地利用了 bug。如果文件的新版本修复了 bug,应用程序就不能像预期的那样运行了。 所以现在的问题是:如何在修复 bug 并添加新功能的同时,保证不会中断应用程序的正常运行?我对这个问题进行过大量思考,最后结论是完全不可能!但是,这个答案明显不够好。分发的文件总是有 bug,公司总是希望推陈出新。必须有一种方式在分发新文件的同时,尽量保证应用程序良好工作。如果应用程序不能良好工作,必须有一种简单的方式将应用程序恢复到上一次已知良好的状态。 本章将解释 .NET Framework 为了解决版本控制问题而建立的基础结构。事先说一句:要讲述的内容比较复杂。将讨论 CLR 集成的大量算法、规则和策略。还要提到应用程序开发人员必须熟练使用的大量工具和实用程序。之所以复杂,是因为如前所述,版本控制本来就是一个复杂的问题。 3.1 两种程序集,两种部署 CLR 支持两种程序集:弱命名程序集(weakly named assembly)和强命名程序集(strongly named assembly)。...

2024-11-20 · 10 分钟 · SAM

第 2 章 生成、打包、部署和管理应用程序及类型

第 2 章 生成、打包、部署和管理应用程序及类型 本章内容: .NET Framework 部署目标 将类型生成到模块中 元数据概述 将模块合并成程序集 程序集版本资源信息 语言文化 简单应用程序部署(私有部署的程序集) 简单管理控制(配置) 在解释如何为 Microsoft .Net Framework 开发程序之前,首先讨论一下生成、打包和部署应用程序及其类型的步骤。本章重点解释如何生成仅供自己的应用程序使用的程序集。第 3 章“共享程序集和强命名程序集”将讨论更高级的概念,包括如何生成和使用程序集,使其中包含的类型能由多个应用程序共享。这两章会谈及管理员能采用什么方式来影响应用程序及其类型的执行。 当今的应用程序都由多个类型构成,这些类型通常是由你的和 Microsoft 创建的。除此之外,作为一个新兴产业,组件厂商们也纷纷着手构建一些专用类型,并将其出售给各大公司,以缩短软件项目的开发时间。开发这些类型时,如果使用的语言是面向 CLR 的,这些类型就能无缝地共同工作。换言之,用一种语言写的类型可以将另一个类型作为自己的基类使用,不用关心基类用什么语言开发。 本章将解释如何生成这些类型,并将其打包到文件中以进行部署。另外,还会提供一个简短的历史回顾,帮助开发人员理解 .NET Framework 希望解决的某些问题。 2.1 .NET Framework 部署目标 Windows 多年来一直因为不稳定和过于复杂而口碑不佳。不管对它的评价对不对,之所以造成这种状况,要归咎于几方面的原因。首先,所有应用程序都使用来自 Microsoft 或其他厂商的动态链接库(Dynamic-Link Library,DLL),由于应用程序要执行多个厂商的代码,所以任何一段代码的开发人员都不能百分之百保证别人以什么方式使用这段代码。虽然这种交互可能造成各种各样的麻烦,但实际一般不会出太大问题,因为应用程序在部署前会进行严格测试和调试。 但对于用户,当一家公司决定更新其软件产品的代码,并将新文件发送给他们时,就可能出问题。新文件理论上应该向后兼容以前的文件,但谁能对此保证呢?事实上,一家厂商更新代码时,经常都不可能重新测试和调试之前发布的所有应用程序,无法保证自己的更改不会造成不希望的结果。 很多人都可能遭遇过这样的问题:安装新应用程序时,它可能莫名其妙破坏了另一个已经安装好的应用程序。这就是所谓的“DLL hell”。这种不稳定会对普通计算机用户带来不小的困扰。最终结果是用户必须慎重考虑是否安装新软件。就像我个人来说,有一些重要的应用程序是平时经常都要用到的,为了避免对它们产生不好的影响,我不会冒险去”尝鲜“。 造成 Windows 口碑不佳的第二个原因是安装的复杂性,如今,大多数应用程序在安装时都会影响到系统的全部组件。例如,安装一个应用程序会将文件复制到多个目录,更新注册表设置,并在桌面和”开始“菜单上安装快捷方式。问题是,应用程序不是一个孤立的实体。应用程序备份不易,因为必须复制应用程序的全部文件以及注册表中的相关部分。除此之外,也不能轻松地将应用程序从一台机器移动到另一台机器。只有再次运行安装程序,才能确保所有文件和注册表设置的正确性。最后,即使卸载或移除了应用程序,也免不了担心它的一部分内容仍潜伏在我们的机器中。 第三个原因涉及到安全性。应用程序安装时会带来各种文件,其中许多是由不用的公司开发的。此外, Web 应用程序经常悄悄下载一些代码(比如 ActiveX 控件),用户根本注意不到自己打的机器上安装了这些代码。如今,这种代码能够执行任何操作,包括删除文件或者发送电子邮件。用户完全有理由害怕安装新的应用程序,因为它们可能造成各种各样的危害。考虑到用户的感受,安全性必须集成到系统中,使用户能够明确允许或禁止各个公司开发的代码访问自己的系统资源。 阅读本章和下一章可以知道, .NET Framework 正常尝试彻底解决 DLL hell 的问题。另外, .NET Framework 还在很大程度上解决了应用程序状态在用户硬盘上四处分散的问题。例如,和 COM 不同,类型不再需要注册表中的设置。但遗憾的是,应用程序还是需要快捷方式。安全性方面,.NET Framework 包含称为”代码访问安全性“(Code Access Security)的安全模型。 Windows 安全性基于用户身份,而代码访问安全性允许宿主设置权限,控制加载的组件能做的事情。像 Microsoft SQL Server 这样的宿主应用程序只能将少许权限授予代码,而本地安装的(自宿主)应用程序可获得完全信任(全部权限)。以后会讲到,....

2024-11-20 · 15 分钟 · SAM