深入理解C#(第3版)-- 【C#1】第2章 C#1所搭建的核心基础(学习笔记)
2.1 委托
2.1.1 简单委托的构成
为了让委托做某事,必须满足4个条件:
声明委托类型;
必须有一个方法包含了要执行的代码;
必须创建一个委托实例;
必须调用(invoke )委托实例。
1. 声明委托类型
2. 为委托实例的操作找到一个恰当的方法
C# 1要求委托必须具有完全相同的参数类型。
3. 创建委托实例
至于具体用什么形式的表达式来创建委托实例,取决于操作使用实例方法还是静态方法。
4. 调用委托实例
调用Invoke会执行委托实例的操作,向它传递在调用Invoke 时指定的任何参数。

5. 一个完整的例子和一些动机
委托实例的两种方式——显式调用Invoke和使用C#的简化形式。
using System; delegate void StringProcessor(string input); class Person { string name; public Person(string name) { this.name = name; } public void Say(string message) { Console.WriteLine("{0} says:{1}", name, message); } } class Background { public static void Note(string note) { Console.WriteLine("({0})", note); } } class SimpleDelegateUse { static void Main() { Person jon = new Person("Jon"); Person tom = new Person("Tom"); StringProcessor jonsVoice, tomsVoice, background; jonsVoice = new StringProcessor(jon.Say); tomsVoice = new StringProcessor(tom.Say); background = new StringProcessor(Background.Note); jonsVoice("Hello, son."); tomsVoice.Invoke("Hello, Daddy!"); background("An airplane fliespast."); } }
委托的实质是间接完成某种操作
2.1.2 合并和删除委托
委托实例实际有一个操作列表与之关联。这称为委托实例的调用列表(invocation list )。System.Delegate类型的静态方法Combine和Remove负责创建新的委托实例。其中,Combine负责将两个委托实例的调用列表连接到一起,而Remove 负责从一个委托实例中删除另一个实例的调用列表。
委托是不易变的
Delegate.Combine 的显式调用,一般都是使用+和+=操作符。
Delegate.Remove的显式调用,使用-和-= 运算符。
如果委托的签名具有一个非void的返回类型,则Invoke的返回值是最后一个操作的返回值。很少有非void的委托实例在它的调用列表中指定多个操作,因为这意味着其他所有操作的返回值永远都看不见。除非每次调用代码使用Delegate.GetInvocationList 获取操作列表时,都显式调用某个委托。
2.1.3 对事件的简单讨论
事件不是委托类型的字段。
字段风格的事件(field-like event)
我认为将事件看做类似于属性(property)的东西是很有好处的。首先,两者都声明为具有一种特定的类型。对于事件来说,必须是一个委托类型。
在订阅或取消订阅一个事件时,看起来就像是在通过+=和-=运算符使用委托类型的字段。但和属性的情况一样,这个过程实际是在调用方法(add 和remove方法)。对于一个纯粹的事件,你所能做的事情就是订阅(添加一个事件处理程序)或者取消订阅(删除一个事件处理程序)。
2.1.4 委托总结
委托封装了包含特殊返回类型和一组参数的行为,类似包含单一方法的接口;
委托类型声明中所描述的类型签名决定了哪个方法可用于创建委托实例,同时决定了调用的签名;
为了创建委托实例,需要一个方法以及(对于实例方法来说)调用方法的目标;
委托实例是不易变的;
每个委托实例都包含一个调用列表——一个操作列表;
委托实例可以合并到一起,也可以从一个委托实例中删除另一个;
事件不是委托实例——只是成对的add /remove方法(类似于属性的取值方法/赋值方法)。
2.2 类型系统的特征
2.2.1 C#在类型系统世界中的位置
C#1 的类型系统是静态的、显式的和安全的。
1. 静态类型和动态类型
C#是静态类型的:每个变量都有一个特定的类型,而且该类型在编译时是已知的。
为什么称为静态类型?
静态这个词用来描述表达式的编译时类型,因为它们使用不变的(unchanging )数据来分析哪些操作可用。
动态类型的实质是变量中含有值,但那些值并不限于特定的类型,所以编译器不能执行相同形式的检查。
2. 显式类型和隐式类型
显式类型和隐式类型的区别只有在静态类型的语言中才有意义。对于显式类型来说,每个变量的类型都必须在声明中显式指明。隐式类型则允许编译器根据变量的用途来推断变量的类型。
3. 类型安全与类型不安全
使用一些“非正当”的方法,可以使语言将一种类型的值当作另一种完全不同的类型的值,同时不必进行任何转换。
代码清单2-2 使用C代码来演示不安全类型的系统
#include<stdio.h> int main(int argc, char **argv) { char *first_arg = argv[1]; int *first_arg_as_int = (int *) first_arg; printf("%d", * first_arg_as_int); }
2.2.2 C#1的类型系统何时不够用
1. 集合,强和弱
.NET 1.1 内建了以下3种集合类型:
数组——强类型——内建到语言和运行时中;
System.Collections 命名空间中的弱类型集合;
System.Collections.Specialized命名空间中的强类型集合。
数组是强类型的。所以在编译时,不可能将string[] 的一个元素设置成一个FileStream。然而,引用类型的数组也支持协变。
协变(covariance)
只要元素的类型之间允许这样的转换,就能隐式将一种数组类型转换成另一种类型。
协变性:派生程度较大类型分配(赋值)给派生程度较小类型。(子类可以隐式转换为父类)
代码清单2-3 数组协变以及执行时类型检查的演示
string[] strings = new string[5]; object[] objects = strings;//应用协变式转换 objects[0] = new Button();//试图存储一个按钮引用
运行代码清单2-3,会抛出一个ArrayTypeMismatchException异常。这是由于从string[] 转换成object[] 会返回原始引用,无论strings还是objects都引用同一个数组。
2. 缺乏协变的返回类型
参数类型的逆变性(parameter type contravariance )
假定一个接口方法或一个虚方法,其签名是void Process(string x),那么在实现或者覆盖这个方法时,使用一个放宽了限制的签名应该是合乎逻辑的,如void Process(object x)。
逆变性:派生程度较小类型分配(赋值)给派生程度较大类型(子类放宽限制到父类,用父类代替子类)
//协变性和逆变性有支持限制:1.数组,2.泛型委托,3.泛型接口 Action<Child> act_child = (c) => { Console.WriteLine("Id:{0},Name:{1}", c.Id, c.Name); }; Func<Child> fun_str = () => { Child child = new Child { Id = 2, Name = "c2" }; return child; }; Func<Parent> fun_parent = () => { Parent parent = new Parent { Id = 2 }; return parent; }; Action<object> act_obj = (obj) => { Console.WriteLine(obj); }; Action<string> act_str = act_obj;//参数类型的“逆变” // Func<Child> fun_child = fun_parent;//这也会报错 Func<Parent> fun_obj = fun_str;//返回类型的“协变” // Action<Parent> act_parent = act_child;//这会报错
2.2.3 类型系统特征总结
C# 1 是静态类型的——编译器知道你能使用哪些成员;
C# 1 是显式的——必须告诉编译器变量具有什么类型;
C# 1 是安全的——除非存在真实的转换关系,否则不能将一种类型当做另一种类型;
静态类型仍然不允许一个集合成为强类型的“字符串列表”或者“整数列表”,除非针对不同的元素使用大量的重复代码;
方法覆盖和接口实现不允许协变性/逆变性。
2.3 值类型和引用类型
2.3.1 现实世界中的值和引用
影印报纸(值类型的行为), URL网页(引用类型的行为)
类(使用class 来声明)是引用类型,而结构(使用struct来声明)是值类型。
数组类型是引用类型,即使元素类型是值类型(所以即便int 是值类型,int[]仍是引用类型);
枚举(使用enum来声明)是值类型;
委托类型(使用delegate 来声明)是引用类型;
接口类型(使用interface来声明)是引用类型,但可由值类型实现。
2.3.2 值类型和引用类型基础知识
对于值类型的表达式,它的值就是表达式的值,
对于引用类型的表达式,它的值是一个引用,而不是该引用所指代的对象。
两种类型的另一个差异在于,值类型不可以派生出其他类型。这将导致的一个结果就是,值不需要额外的信息来描述值实际是什么类型。把它同引用类型比较,对于引用类型来说,每个对象的开头都包含一个数据块,它标识了对象的实际类型,同时还提供了其他一些信息。
2.3.3 走出误区
误区1 :“结构是轻量级的类”
值类型很“能干”——它们不需要垃圾回收,(除非被装箱)不会因类型标识而产生开销,也不需要解引用。但在其他方面,引用类型显得更“能干”——在传递参数、赋值、将值返回和执行类似的操作时,只需复制4或8 字节(要看运行的是32 位还是64 位CLR ),而不是复制 全部数据。
误区2 :“引用类型保存在堆上,值类型保存在栈上”
第一部分是正确的——引用类型的实例总是在堆上创建的。但第二部分就有问题了。前面讲过,变量的值是在它声明的位置存储的。
误区3 :“对象在C#中默认是通过引用传递的”
引用类型变量的值是引用,而不是对象本身。
2.3.4 装箱和拆箱
对于引用类型的变量,它的值永远是一个引用;
对于值类型的变量,它的值永远是该值类型的一个值。
2.3.5 值类型和引用类型小结
对于引用类型的表达式(如一个变量),它的值是一个引用,而非对象。
引用就像URL ——是允许你访问真实信息的一小片数据。
对于值类型的表达式,它的值是实际的数据。
有时,值类型比引用类型更有效,有时恰好相反。
引用类型的对象总是在堆上,值类型的值既可能在栈上,也可能在堆上,具体取决于上下文。
引用类型作为方法参数使用时,参数默认是以“值传递”方式来传递的——但值本身是一个引用。
值类型的值会在需要引用类型的行为时被装箱;拆箱则是相反的过程。
2.4 C#1之外:构建于坚实基础之上的新特性
2.4.1 与委托有关的特性
代码清单2-4 演示C# 2 在委托实例化上的改进
static void HandleDemoEvent(object sender, EventArgs e) { Console.WriteLine("Handled by HandleDemoEvent"); } //指定委托类型和方法 EventHandler handler; handler = new EventHandler(HandleDemoEvent); handler(nul1, EventArgs.Empty); //隐式转换成委托实例 handler = HandleDemoEvent; handler(nul1, EventArgs.Empty); //用一个匿名方法来指定操作 handler = delegate(object sender, EventArgs e) { Console.writeLine("Handled anonymously"); }; handler(nul1, EventArgs.Empty); //使用匿名方法的简写形式 handler = delegate { Console.WriteLine("Handled anonymously again"); }; handler(nu11, EventArgs.Empty); //使用委托逆变性 MouseEventHandler mouseHandler = HandleDemoEvent; mouseHandler(null, new MouseEventArgs(MouseButtons.None,0, 0, 0, 0));
委托有关的新特性包括:
泛型(泛型委托类型)——C# 2 ;
创建委托实例时使用的表达式——C# 2 ;
匿名方法——C# 2 ;
委托协变性/逆变性——C# 2 ;
Lambda表达式——C# 3 。
2.4.2 与类型系统有关的特性
C# 3 匿名类型、隐式类型的局部变量以及扩展方法。
匿名类型主要是为LINQ而存在的。在LINQ中利用匿名类型,可以有效地创建一个含有大量只读属性的数据转移类型,同时不必真正为它们写代码。
代码清单2-6 演示匿名类型和隐式类型
var jon = new { Name = "Jon", Age = 31}; var tom = new { Name = "Tom", Age = 4}; Console.WriteLine("{0} is {1}", jon.Name, jon.Age); Console.WriteLine("{0} is {1}", tom.Name, tom.Age);
隐式类型(使用var ),匿名类型 (new {...}部分)
首先,C# 3仍是静态类型的语言。C#编译器将jon 和tom 声明成一个像普通类型一样的具体的类型。
其次,这里没有创建两个不同的匿名类型。变量jon 和tom 具有相同的类型,因为编译器根据属性名、类型和顺序判断出只需生成一个类型,即可供两个语句使用。
扩展方法同样是为LINQ而生,但在LINQ之外,它也非常有用。以前经常遇到的一个问题是,由于框架类型没有提供一个特定的方法,所以不得不写一个静态的工具方法(utility method )来实现它。例如,通过翻转现有字符串来创建逆序的新字符串,会用到静态的StringUtil. Reverse方法。有了扩展方法之后,就可以直接调用那个静态方法,好像string 类型本来就提供了该方法一样。所以可以像下面这样写:
string x = "dlrow olleH".Reverse();
C# 4 包含两个与类型系统相关的特性。
1.泛型委托和接口的协变与逆变。
2.C#的动态类型
各个特性是在C#的哪个版本中引入的:
泛型——C# 2 ;
受限的委托协变性/逆变性—— C# 2 ;
匿名类型——C# 3 ;
隐式类型——C# 3 ;
扩展方法——C# 3 ;
受限的泛型协变/逆变——C# 4 ;
动态类型——C# 4 。
2.4.3 与值类型有关的特性
在.NET 1.1 的集合中使用值类型时,人们普遍抱怨的一个问题是,由于所有“常规用途”的API都是用object类型来指定的,所以每次要为集合添加一个结构体值时,都
要涉及装箱。获取值时,则又要拆箱。
泛型使用真实的类型,而不只是一个常规用途的对象,从而弥补了在速度和内存使用上的不足。
人们不再抱怨无法将null赋给值类型的变量(尤其是在操作数据库时)。在此之前,像“等于null的int 值”这样的概念是不存在的,即使数据库 中的整数字段允许为空。
泛型——C# 2 ;
可空类型(可以为null的类型)——C# 2
浙公网安备 33010602011771号