1.C#2.0之2.0简介(完成)

     以前的十八章都是C#1.0所引入的内容,接下来写C#2.0引入的几项语言扩展,2.0引入的重要的是泛型、匿名方法、迭代器、不完整类型:

          泛型:可以让类、结构、接口、委托和方法通过它们所存储和操作的数据的类型被参数化;它们提供了更强的编译时类型检查,减少了显示转换,装箱操作和运行时类型检查;

          匿名方法:让代码以内联的方式潜入到期望委托值得地方。它与Lisp编程语言中的λ函数(lambda)相似。C#2.0支持"closures"的创建,在其中匿名方法可以访问相关布局变量和参数;

          迭代器:可以递增计算和产生值的方法。迭代器让类型指定foreach语句如何迭代它的所有元素变得很容易;

          不完整类型:可以让类、结构、接口被拆分成多个部分存储在不同的源文件中,这更利于开发和维护;此外,不完整类型允许某些类型的机器生成的部分与用户编写的部分之间的分离,从而使增加由工具产生的代码很容易。

     C#2.0的语言扩展主要被设计用于确保与现存的代码之间最大的兼容性。

  1.1泛型

   1.1.1 为什么使用泛型

     如果没有泛型,通用目的的数据结构可以采用object类型存储任何类型的数据。比如Stack类在一个object数组中存储数据,它的两个方法使用object接受和返回数据:

     这样虽然让Stack类更加灵活,但还是有缺点。比如你可以用任何类型的值压入Push堆栈,但取回一个值是,Pop方法的结果必须被显示强制转换为合适的类型,这样为了一个运行时类型检查去编写代码而带来的性格不利影响是很讨厌的。这种装箱和拆箱的操作增加了性能开销,因为它们涉及动态内存分配和运行时类型检擦。

     Stack类更大的问题是它不能强制放置在堆栈上的数据结构,比如用Customer实例可以压入堆栈,但取回时可能被强制转换为错误的类型:

     虽然上面的代码时Stack类的一种不恰当用法,但代码从技术上说是正确的,并不会报告编译时错误,问题会知道代码执行时才会冒出来,抛出一个InvalidCastException异常。

     但如果Stack类具有指定其元素类型的能力,那么很显然它能从这种能力得到好处。泛型,使这变得可能。

   1.1.2 创建和使用泛型

     泛型为创建具有类型参数的类型提供了工具。比如:

     这个例子声明了一个带有类型参数T的泛型Stack类。类型参数在类名字之后的"<"和">"分节符中间指定的。这里没有object与别的类型的相互转化,Stack<T>的实例接受它们被创建时的类型,并存储哪个类型的数据。参数类型T充当一个占位符,直到使用的时候才指定一个实例的类型。比如:

     这就表示int作为T的类型参数。Stack<int>类型被称为构造类型;在Stack<int>类型中,T的每次出现都被使用类型参数int代替;与非泛型Stack相比,它提供了更高效的存储效率。泛型提供了强类型,如果T是int,若将Customer对象压入该堆栈会出现错误。

     泛型类型声明可以有任意数量的类型参数。比如Dictionary类可能有两个类型参数:一个用于键的类型,一个用于值的类型。

     所以被使用时要提供两个参数。

   1.1.3 泛型类型实例化

     与非泛型类型相似,被编译过的泛型类型也是由中间语言(IL)指令和元数据表示的。泛型类型的表示当然也对类型参数的存在和使用进行了编码。

     当应用程序首次创建一个构造泛型类型的实例时,.NET公共语言运行时的实时编译器(JIT)将在进程中把泛型IL和元数据转换为本地代码,并且将类型参数替换为实际的类型。对于那个构造泛型类型的后续引用将会使用相同的本机代码。从一个泛型类型创建一个特定构造类型的过程,称为泛型类型实例化。

     .NET公共语言运行时使用值类型为每个泛型类型实例创建了一个本地代码的特定拷贝,但对于所有的引用类型,它将共享本地代码的单一拷贝。

   1.1.4 约束

     一般来说,泛型类不限于只是根据类型参数存储值,还可能在给定类型参数的类型的对象上调用方法。比如:

     因为K所指定的类型参数可能是任何类型,所以用Compareto方法出现编译时错误;大家想到的办法应该是强制类型转换吧。

     但这样需要在运行时的动态类型检擦,增加了开销;更糟糕的是,它将错误报告推迟到运行时,如果键没有实现IComparable接口将会抛出InvalidCastException异常。

     为了提供更强的编译时类型检查,并减少类型强制转换,C#允许为每个类型参数提供一个约束的可选列表。类型参数约束指定了类型必须履行的一种需求,其目的是为了该类型参数用做实参。

     约束使用单词where声明,随后是类型参数的名字,接着是类或接口类型的列表,以及可选的构造函数约束new()。

     例如这样就可以不报错了,编译器将会确保K的任何类型实参是实现了IComparable接口的类型,这样就不需要强制转换。

     对于一个给定的类型参数,可以指定任意数量的接口作为约束,但只能有一个类。每个约束的类型有一个单独的where语句。比如:

     这个例子,构造函数约束new(),确保为使E用做类型参数的类型具有一个公有的、无参数的构造函数,并且它允许泛型类使用new E()创建该类型的实例。

     对于类型参数约束,应该小心使用,尽管它提供了更强的编译时类型检查,但也限制了泛型类型的可能的用方。在上面的案例中,泛型类List<T>可能约束T实现IComparable接口,但这样就导致没有实现IComparable接口的类型不能使用List<T>。

   1.1.5 泛型方法

     在某些情况,类型参数对于整个类不是必须的,而只在特定方法内是必须的。在创建一个接受泛型类型作为参数的方法时常常就是这样,要让方法也起Stack<T>的作用,方法就必须也作为泛型方法而编写。

     泛型方法在方法的名字后面加"<"">"分界符,分界符之间指定了一个或多个类型参数,类型参数可以在参数列表、返回类型和方法体之内被使用。比如:

     使用这个泛型方法,可以将多个项目压入任意Stack<T>中。

     在调用时,必须提供T作为类型参数传递给方法,在很多情况,编译器使用一种称为类型推断的处理过程,从传递给方法的其他参数推断正确的类型参数。就像上面这个例子,第一个正式参数是Stack<int>类型,后续的参数也是int类型,因此编译器可以推断类型参数值必须是int类型,所以调用这个方法时不用指定类型参数。

  1.2匿名方法

     事件句柄和其他回调函数经常需要通过专门的委托调用,从来都不是直接调用。虽然如此,我们还是只能将事件句柄和回调函数的代码放在特定方法种,再显式地为这个方法创建委托。

     相反,匿名方法允许一个委托关联地代码被内联地写入使用委托的地方,使得代码对于委托得实例很直接。除此之外,匿名方法还共享了对本地语句包含得函数成员的访问。为了使命名方法实现共享,需要手工创建辅助类,并将本地成员"提升"为类的域。

     下面有个例子,有一个输入表单,它包含一个列表框文本框和按钮;当按下按钮时,文本框中一个包含文本的项就被添加到列表框:

     即使只有一条语句需要执行按钮Click事件的响应,这条语句也必须放在一个具有完整的参数列表的单独的方法中,还必须手工创建引用那个方法的EventHandler委托。如果使用匿名方法,则就会变得很简洁:

     匿名方法就是这样,由关键字delegat和一个可选的参数列表,以及封闭在"{"和"}"分界符中的语句组成;上面这个匿名方法没有使用由委托提供的参数,所以可以省略。如果要的话,匿名方法可以包含一个参数列表。

     在这个例子中,将会发生一次从匿名方法到EventHandler委托类型(Click事件的类型)的隐式转换。这种隐式转换是可能的,因为参数列表和委托类型的返回值与匿名方法是兼容的。关于兼容的确切规则是:

          如果下列有一条成立,那么委托的参数列表和匿名方法是兼容的:

                *.匿名方法没有参数列表,且委托没有out参数;

                *.匿名方法包含的参数列表与委托的参数在数量、类型和修饰符上是精确匹配的。

          如果下列有一条成立,那么委托的返回类型与匿名方法兼容:

                *.委托的返回类型是void,匿名方法没有返回语句,或只有不带表达式的return语句;

                *.委托类型不是void,并且在匿名方法中,所有return语句相关的表达式可以被隐式转换到委托类型。

     在委托类型的隐式转换发生以前,委托的参数列表和返回类型二者都必须与匿名方法兼容。

     下面的例子使用匿名方法编写了"内联"函数。匿名方法作为Function委托类型而传递:

     Apply方法应用double[]元素的给定的Function,并返回一个double[]作为结果。在Main方法中,传递给Apply的第二个参数是一个匿名方法,它与Function委托类型兼容。该匿名方法只返回参数的平凡,而Apply调用的结果是一个double[],在a中包含了值的平方。

     MultiplyAllBy方法返回一个通过给定factor与在参数数组a中的每个值相乘而创建的double[]。为了得到结果,MultiplyAllBy调用了Apply方法,并传给它一个匿名方法。

     如果一个本地变量或参数的作用域包括了匿名方法,则该变量或参数被称为匿名方法的外部变量,在MultiplyAllBy方法中,a和factor是传递给Apply的匿名方法的外部变量,因为匿名方法引用了factor,factor被匿名方法所捕获。通常,局部变量的生存期被限制在它所关联的块或语句的执行区;然而被捕获的外部变量将一直存活到委托所引用的匿名方法可以被垃圾回收为止。

     正如面前写的,匿名方法可以被隐式转换到与之兼容的委托类型。对于一个方法组,C#2.0允许这种相同类型的转换,即在几乎任何情况下都不需要显式的实例化委托。比如:

     使用简短的形式时,编译器将自动推断哪一个委托类型需要实例化,最后效果和第一个形式一样。

  1.3迭代器

     C#的foreach语句被用于迭代一个可枚举集合的所有元素。为了可以被枚举,集合必须具有一个无参数GetEnumerator方法,它返回一个enumerator(枚举器)。一般情况,枚举器很难实现,这个问题使用迭代器就大大地简化了。

     迭代器是产生值得有序序列得一个语句块。迭代器不同于有一个或多个yield语句存在的常规语句块。

         yield return语句:长生迭代的下一个值;

         yield break语句:指明迭代已经完成。

     只要函数成员的返回类型是枚举器接口或可枚举接口,迭代器就可以被用做函数体。

     迭代器不是一种成员,它只是实现函数成员的方式。一个通过迭代器被实现的成员,可以被其他可能/不可能通过迭代器而被实现的成员覆盖和重载。

     下面的Stack<T>类使用迭代器实现了它的GetEnumerator方法。这个迭代器依序枚举了堆栈中从顶到低的所有元素。

     GetEnumerator方法的存在使得Stack<T>成为一个可枚举类型,它使得Stack<T>的实例可被用在foreach语句中。

     上面这个输入:9 8 7 6 5 4 3 2 1 0

     foreach语句隐式地调用了集合的无参数GetEnumerator方法以获得一个枚举器。由集合所定义的只能有一个这样的无参数GetEnumerator方法;但经常有多种枚举方法,以及通过参数控制枚举的方法。在这种情况,集合可以使用迭代器实现返回可枚举接口之一的属性和方法。例如,Stack<T>可能引入两个IEnumerable<T>类型的新属性:TopToBottom和BottomToTop:

     TopToBottom属性的get访问器只是返回this,因为堆栈自身是可枚举的;BottomToTop属性返回一个使用C#迭代器实现的枚举。下面的例子展示了属性如何用于枚举堆栈元素:

     当然,这些属性同样也可以在foreach语句之外使用。下面的例子将调用属性的结果传递给一个单独的Print方法;该例子也展示了一个用做FromToBy接受参数的方法体的迭代器:

     该例子的输入为:9 8 7 6 5 4 3 2 1 0

                     0 1 2 3 4 5 6 7 8 9 

                     10 12 14 16 18 20

     泛型和非泛型可枚举接口包含一个单一的成员,一个不接受参数的GetEnumerator方法,并且返回一个枚举器接口;一个枚举充当一个枚举器工程。每当调用一个正确地实现了可枚举接口的类的GetEnumerator方法时,都会产生一个独立的枚举器。假定枚举的内部状态在两次调用GetEnumerator之间没变化,返回的枚举器应该产生相同集合、相同顺序的枚举值。下面的例子中,即使枚举的生存期发生交叠,这一点也应该保持:

     上面的代码打印了整数1到10的乘法表。注意,FromTo方法只要被调用一次用来产生枚举e。然而,e.GetEnumerator()被调用了多次(通过foreach),以产生多个等价的枚举器。这些枚举器都封装了FromTo声明中指定的迭代器代码。注意:迭代器代码修改了from参数。

     不过,枚举器是独立运作的,因为每个枚举器都给出from和to参数自己的拷贝。枚举器之间过渡状态的共享是众多细微的瑕疵之一,当实现枚举和枚举器时应避免。C#迭代器的设计可用于避免这些问题,从而以一种简单直观的方式实现健壮的枚举和枚举器。

  1.4不完整类型

     尽管在一个单一的文件中,为一个类型维护所有的源代码是一个良好的编程事实践,但有时一个类型变得非常大,这将成为一个不切实际的限制。此外,程序员经常使用源代码生成器产生应用程序的初始结构,并且修改结果代码。遗憾的是,当源代码在将来被再次发射时,现存的修改将会被覆盖。

     不完整类型可以让类、结构和接口被拆分成多个部分存储在不同的源文件,这就利于开发和维护;此外,不完整类型允许某些类型的机器生成的部分与用户编写的部分之间的分离,这样就增加由工具产生的代码很容易。

     当多个部分定义一个类型时,可以使用一个新的类型修饰符partial

     这就是不完整类的例子,它在两个部分中实现。这两部分可以在不同的源文件,第一个部分时机器生成,第二个是手工创建。

     当这两部分一起编译时,结果代码和被作为一个单一而编写的类是一样的;不完整类型的所有部分必须一起编译,这样各个部分在编译时就可以被融合在一起。注:不允许对已经被编译的类型进行扩展。

原文地址:https://www.cnblogs.com/dreamoffire/p/10166281.html