partial method

C#新增的特性中引起争议的有许多,分部方法(Partial Method)算是一个。分部方法通常被定义在一个分部类中,在常规的类文件中也可实现。如果分部方法没有被实现,编译器就不会、对他们进行编译。

  分部方法有着严格的限制。它们必须是私有的,不能返回值,不能有输出参数。因为任何针对没有被实现的分部方法的调用都会简单地被忽略,所以说这些限制是非常有必要的。反过又意味着,分部方法不能作为一个明确分配的变量。Visual Basic也有分部方法,尽管VB不需要对变量的明确分配,它也有同样的限制。

  有那么多的限制,有人可能会问,“它们有什么优点?”。这个问题问得好,基本上,分部方法仅被代码生成器在处理轻量级事件的时候使用。就像 Alexander Jung所解释的 :

  分部方法通常(也可能是唯一相关的)的应用场景就是在代码生成的时候用于处理轻量级事件。假设你解析一个数据库或者一个XML文件,然后生成了数据类,结果你会发现有数十个类、几百个属性以及一大堆泛型和模板文件等。分部方法另外一个经常被用到的地方是验证,或者让属性的setter去更新另一个属性。所以如果你要使用产生的代码,或者在运行时有几百个事件和数千个方法调用的话( 其实大多数情况下只用到了其中的一点点),就让分部方法来吧。分部方法在声明和使用时要比事件容易得多,如果没有用到它们,它们就会消失。

  性能的提升并不是没有代价的。从分部方法必须是私有的限制中,Alexander发现了它们的不足之处:

  缺点:如果你喜欢元数据驱动的应用,并且已经被ASP.NET的数据绑定所困扰时(因为没有其他的方法可以附上元数据)……那么,就准备着在将来丢失信息吧。如果你需要为属性的setter增加一些事件(基于跟踪和调试的需要),如果你需要某个动态的行为(比如附上某个通用规则引擎)等等,那么就让我们祈祷代码分析器的开发人员能够预知这个场景(或者已经做好了准备)吧。你有了一个清晰的层的分离,那么实体就应该对UI一无所知吗?是的,将代码直接放到数据类中会破坏层的关系,但是你可以手动地用分部方法实现真正的事件啊。

  另外一些人对于C#中的分部方法也是忧虑重重,大部分是关于代码设计器的使用的。Stefan Wenig写道:

  首先,我不是非常热衷于设计器。我忧虑的是设计器也许很快就会将我们送上过去基于COM开发时的老路,数百个设计器和向导产生了那么多没人想去看的ATL和MCF代码。在我们陷于设计器、创建的无用文件和复杂的构建过程时,使用Ruby的家伙们在笑,因为他们用几行代码就可以解决(联想一下上世纪90年代COM/C++和Java的比较)。难道对于基于代码的开发人员生产率不是C#所首要考虑的(看看VB的设计器驱动的RAD路线图)?我们不应该再沉浸于基于设计器的,企业类库思想的,乐于使用软件工厂代码设计器的幻想中了。团结起来,抵制它们!

  Ayende Rahien也没有嘴软:

  让我们一起埋葬这些代码设计器吧,竖起分部方法的辉煌墓碑!


****************************************
写过JavaScript的朋友或许早就一眼盯住了上文中提到的新关键字“var”,是啊,多么熟悉的字眼儿,如今竟然也在C#当中出现了!如何使用呢?让我们来看一段很简短的代码吧:

1: static void Main(string[] args)
2: {
3: var name = "ZeroCool";
4: var age = 24;
5:
6: Console.WriteLine(name + " is " + age + " years old.");
7: }

  如果您是在LINQ项目中编写这段代码,那么就可以运行。也许您会问,用var有什么好处呢,是否object也可以代替它呢?是的,可以代替,object是基类型,当然可以用它来声明任何类型的变量。但是,如果用object来声明值类型就会引起装箱操作,这会降低性能。另外,如果您要用几个object声明的变量进行运算的话,您必须得把它们挨个转换成相应的类型才行,这样做其实是费力不讨好的。关键字var的优点就在于它使得您可以非常便利地声明隐式类型变量,编译器可以在用到该变量的时候根据它的值来反推出它的类型。注意,var并不是具体的变量类型,它只是一个声明符,它的作用就是将数据类型的声明交给编译器去做。您可以用它来声明任何类型的变量,正如上一段程序中的变量age,您肯定知道它是一个int型变量,但实际上您写下这句代码的时候它还不是,只有在编译的时候编译器根据它的值反推出它是int型时,它才是真正意义上的int型。

  前面看到的是用var关键字声明一些简单类型的变量,那么复杂类型的变量例如数组、集合等,是否也可以使用var关键字呢?答案是肯定的,请看:

1: static void Main(string[] args)
2: {
3: var memberNames = new[] { "ZeroCool", "Michael", "Somebody" };
4:
5: foreach (var item in memberNames)
6: {
7: Console.WriteLine(item.ToString());
8: }
9:
10: Console.ReadLine();
11: }

  上面这段代码是可以正常运行的,同样的,.NET为我们提供的其它类型我们都可以用var来声明,同时也包括用户自定义类型。

  另外,更强大的一点是,隐式类型数组中的元素可以通过匿名对象初始器来创建匿名类型的数据结构,例如:

1: static void Main(string[] args)
2: {
3: var memberInfo = new[] {
4: {
5: Name = "ZeroCool",
6: Details = new[] { "ZeroCool@Autumoon.com", "China" }
7: },
8: {
9: Name = "Michael",
10: Details = new[] { "Michael@Mail.com", "China", "Programmer" }
11: }
12: };
13: }

  但是在使用var关键字声明变量的时候有几点注意事项:

声明变量时必须初始化;
错误:var name;
初始器必须是一个表达式;
错误:var names = {"ZeroCool", "Autumoon"};
初始器表达式必须是一个不为null类型的编译时类型;
错误:var name = null;
局部变量的声明中不能包含多个声明符;
错误:var name = "ZeroCool", country = "China";
初始器不能使用被声明变量自身;
错误:var name = name + " A";
隐式类型数组在声明时必须确保数组内部成员的类型唯一;
错误:var names = new[] {"ZeroCool", 123};
隐式类型的内容比较简单,使用起来也非常简便,我们先介绍到这里,接下来将是Extension Methods(扩展方法),
****************************
过去曾经有很长一段时间,直至现在,存在这样的一种观点,就是C#比Java的实现更漂亮。《Think in java》的作者Bruce Eckel曾经公开质疑过Java 5提供的泛型。不过说实在,我一直不喜欢看Bruce Eckel的书,感觉上他不是一个有经验有深度的技术人员。

  我也很长一段实现认同这样的观点,因为人云亦云!

  在C# 2.0支持泛型,而且在虚拟机级别支持,一开始接触时,感觉是很震撼的,感觉到泛型从此走入主流应用开发了。和C++相比,没有C++模板那样强的功能,完全做不到产生式编程的效果,也做不到编译期计算的效果,但是它简单实用。

  Java 5也开始支持泛型,而且最终正式发行比C# 2.0要早,我之前就使用过Beta版本C#的泛型,也熟悉C++的模板语法,可能是内心的傲慢,或者是懒惰,开始时只是将就着按照传统的经验使用Java 5提供的泛型。
  对事物的一知半解总是令人困扰的,在阅读分析JDK源码时,总会遇到一些Java 5额外提供的泛型用法,一开始忽略不计,但是看多了总会注意到的。
  例如java.util.Collections类中的sort方法和binarySearch方法的接口:
public static <T>void sort(List<T>list, Comparator<? super T>c);

public static <T>int binarySearch(List<? extends Comparable<? super T>>list, T key);
  extends和super这两个关键字是C#和C++的泛型中都没有的,为什么需要这样的功能呢?

例如如下情形:
class A { }

class B extends A {}

void addAll(List<A> items) {}
如下代码:
List<A> aList = ;

List<B> bList = ;

addAll(aList); //可以

addAll(bList); //编译不通过
addAll(bList)是无法编译通过的,这一点在Java、C#、C++中都是如此,怎么办呢?  在java中如下处理,修改addAll的接口,改为:
void addAll(List<? extends A>items) {}
这样,addAll(aList)和addAll(bList)都能够编译通过了。

  另外super关键在算法中更是好用,如上面介绍的Collections.sort方法。如果你想在C#中实现一个和java.util.Collections.sort一样的方法,你会发现那是做不到的!

  为什么C#和C++无法提供这样的功能呢?因为C#和C++都是运行时的泛型支持,bList和aList的类型是不一样的,List<A>和List<B>的实际类型都是不一样的,运行时对泛型的支持目前还无法象处理数组参数那样具备协变能力。而Java的实现是编译器的特性,这样做的缺点就是性能没有得到提升,但是可以提供更好的语法糖。

  想起ajoo以前发表的一个观点,就是在应用开发中,泛型提供的关键是类型安全,性能反而是其次。我对此十分认同,重新审视java的泛型,我们会发现其设计颇具创新,而且向后兼容良好!

  总结一下我的观点:
  Java的泛型,语法有创新,更好用,向后兼容,编写泛型算法更方便,但是没有带来性能提升。

  C#泛型,实现有创新,在虚拟机级别支持,运行时支持泛型,性能有提升,但是不好编写泛型算法,不向后兼容。
******************************************
ASP.NET 2.0支援两种编译模型(Compilation Model):
一为动态编译(Dynamic Compilation),另一个为先行编译(Precompilation)。

这让程序设计师可以有更宽广的选择以决定不同网站何时该用何种编译模型,不但弹性大大提升,且若採用先行编译网站执行效能还可以更高,分述如下:

(一)ASP.NET网站动态编译(Dynamic Compilation)

在ASP.NET 1.0时就已经支援网站动态编译,也就是使用者第一次请求网站网页时,ASP.NET会先将网站程式编译成一个.dll组件档,而后续的请求就会以此来回应,而编译过后的网站执行效能明显较未编译网站快上许多。

然而虽说ASP.NET 1.0具有动态编译的特性,但它只支援如.aspx、.ascx、web.config或global.asax这几种档桉类型,只要它们有异动就会触发系统进行动态编译,但这个模式有个很明显的问题存在,就是像bin目录下的组件、资源档、Web Services等等在程式设计阶段也常进行修改,但这些档桉即使用异动也不会触发系统重新进行编译,因此每每VS.NET 2003的专桉有修改异动,必须手动重新编译整个专桉,如此使用浏览器执行网页才会显示最新修改的程式页面。

但是可能不少人嫌烦或者是初学者根本不知道修改后要手动重新编译,因此微软针对动态编译又再进行了更人性化的改良,现在针对类别、Web Service、具型别的DataSet、Master Page、Themes也支援异动时的动态编译,各位只要针对IE浏览器重新Refresh就会自动触发系统进行重新编译,看到的也当然是最新的画面,省却程式设计师必须手动进行编译,算是一个贴心的改良。

ASP.NET 2.0动态编译和ASP.NET 1.0很像,但是更完美了,且当您建置(Build)整个Web网站后,在bin目录并不会产生.dll的专桉程式,许多ASP.NET 1.0的程式设计师开始惊慌、疑惑与不安,为什麽找不到专桉.dll?没有.dll档要如何部署网站?等等的疑惑,其实没什麽好疑惑的,各位之所以会疑惑是因为你把ASP.NET 1.0当作是普世的标准,凡是违反它的作法皆为异类,进而ASP.NET 2.0的动态编译就成为您眼中的〝异类〞,但那是人的执着心与本位主义作崇的关係,事实上ASP.NET 2.0的动态编译才是更完美,完美到根本不再需要.dll,只要有使用者进行请求时(Request),系统会自动进行动态编译(仍然看不见.dll档),所以若您要部署网站时,利用複製网站工具将.aspx、.aspx.cs、Web.config、类别档全部複製一份到新网站就行了(唯独没有.dll档),剩下的事情动态编译会替您全部打理好。

(二)ASP.NET网站先行编译(Precompilation)

除了上面所讲的动态编译外,ASP.NET 2.0尚提供先行编译(Precompilation)网站的功能,它透过「ASPNET_ Compiler.exe」这个指令来预先编译整个网站,祭司用通俗观点来说明这样的做法有几个好处:

(1)节省网页第一次编译的时间。以往在ASP.NET 1.0这个编译的机制虽然有效加速ASP.NET网站整体性能,但许多使用者或不明究裡的初学者却抱怨第一次执行感觉好慢,而预先编译整个网站是连第一次都省掉了,大概也不会有人再抱怨这个问题了。

(2)保护网页程式码智慧财产。在ASP.NET 1.0时可以将Code Behind编译进dll之中,但是若是以In-Line Code开发或HTML标籤开发的程式则是一点保护作用也没有;此外即便您用Code Behind模式开发Web应用程式,仍然会有许多标籤会产生在.aspx之中,这种情况尤以ASP.NET 2.0更甚,如SqlDataSource连SQL命令都会显示在HTML之中;故透过预先编译不但连程式码都可以编译进去,甚至连.aspx网页中的HTML标籤也可以一併编译进去,对于程式码的保护可以说多了一层保障与选择。

然而我们来看看微软对于先行编译好处的官方说法:

(1)由于页面和程式码档不需在第一次要求时编译,因此使用者可得到更快的回应时间,这对于经常更
新的大型网站特别有用。
(2)使用者浏览网页之前,识别编译时期错误的方法。
(3)不需原始程式码,即可建立可部署到实际执行伺服器已编译网站版本的能力。
******************
曾一直赞扬异步编程模型 (APM) 的优点,强调异步执行 I/O 密集型操作是生产高响应和可伸缩应用程序及组件的关键。这些目标是可以达成的,因为 APM 可让您使用极少量的线程来执行大量的工作,而无需阻止任何线程。遗憾的是,使用 APM 构建典型的应用程序或组件有些难度,因此许多程序员不愿意去做。

有几个因素使得 APM 难以实现。首先,您需要避免状态数据驻留于线程的堆栈上,因为线程堆栈上的数据不能迁移到其他线程。避免基于堆栈的数据意味着必须避免方法参数和局部变量。多年以来,开发人员已喜欢上参数和局部变量,因为它们使编程变得简单多了。

其次,您需要将代码拆分为几个回调方法,有时称为“续”。例如,如果在一个方法中开始异步读取或写入操作,之后必须实现要调用的另一个方法(可能通过不同的线程),以处理 I/O 操作的结果。但程序员就是不习惯考虑以这种方法进行数据处理。在方法和线程之间迁移状态意味着状态必须进行打包,导致实现过程复杂化。

再次,众多有用的编程构造 — 如 try/catch/finally 语句、lock 语句、using 语句,甚至是循环(for、while 和 foreach 语句)— 不能在多个方法之间进行拆分。避免这些构造也增加了实现过程的复杂性。

最后,尝试提供多种功能,如协调多个重叠操作的结果、取消、超时,以及将 UI 控件修改封送到 Windows® 窗体或 Windows Presentation Foundation (WPF) 应用程序中的 GUI 线程,这都为使用 APM 增加了更多的复杂性。

在本期的专栏中,我将演示 C# 编程语言的一些最新添加内容,它们大大简化了异步编程模型的使用。之后我会介绍我自己的一个类,称为 AsyncEnumerator,它建立在这些 C# 语言功能的基础上,用来解决我刚提到的问题。我的 AsyncEnumerator 类能够让您在代码中使用 APM 变得简单而有趣。通过此类,您的代码会变得可伸缩且高响应,因此没有理由不使用异步编程。请注意,AsyncEnumerator 类是 Power Threading 库的一部分,并且依赖于同样是此库一部分的其他代码;读者可从 Wintellect.com 下载该库。


匿名方法和 lambda 表达式

图 1 中,SynchronousPattern 方法显示了如何同步打开并读取文件。该方法简单明了;它会构造一个 FileStream 对象,分配 Byte[],调用 Read,然后处理返回的数据。C# using 语句可确保完成数据处理后会关闭该 FileStream 对象。

ApmPatternWithMultipleMethods 方法显示了如何使用公共语言运行时 (CLR) 的 APM,来执行与 SynchronousPattern 方法相同的操作。您会立即看到实现过程要复杂得多。请注意,ApmPatternWithMultipleMethods 方法会启动异步 I/O 操作,操作完成时会调用 ReadCompleted 方法。同时请注意,两个方法之间的数据传递是通过将共享数据封装到 ApmData 类的实例来完成的,为此我必须专门进行定义,以便启用这两个方法之间的数据传递。还应注意,不能使用 C# using 语句,因为 FileStream 是在一个方法中打开,然后在另一个方法中关闭的。为弥补这个问题,我编写了代码,用于在 ReadCompleted 方法返回之前显式调用 FileStream 的 Close 方法。

ApmPatternWithAnonymousMethod 方法展示了如何使用 C# 2.0 称为匿名方法的新功能重新编写此代码,通过此功能您可以将代码作为参数传递到方法。它能有效地让您将一个方法的代码嵌入到另一个方法的代码中。(我在所著书籍“CLR via C#”(CLR 编程之 C# 篇)(Microsoft Press, 2006) 中详细说明了匿名方法。)请注意,ApmPatternWithAnonymousMethod 方法要简短得多,也更易于理解 — 在习惯使用匿名方法后就可以体会到这一点。

首先,请注意该代码较简单,因为它完全包含在一个方法内。在此代码中,我将调用 BeginRead 方法启动异步 I/O 操作。所有 BeginXxx 方法会将其第二个至最后一个参数视为一个引用方法的委托,即 AsyncCallback,该方法在操作完成时由线程池线程进行调用。通常,使用 APM 时,您必须编写单独的方法,为该方法命名,并通过 BeginXxx 方法的最后一个参数将额外数据传递到该方法。但是,匿名方法功能允许只编写单独的内嵌方法,这样启动请求和处理结果的所有代码便会和环境协调。实际上,该代码看上去与 SynchronousPattern 方法有些类似。

其次,请注意 ApmData 类不再是必需的;您不需要定义该类、构造其实例以及使用它的任何字段!这是如何实现的?其实,匿名方法的作用不仅仅限于将一个方法的代码嵌入另一个方法的代码中。当 C# 编译器检测到外部方法中声明的任何参数或局部变量也用于内部方法时,该编译器实际上会自动定义一个类,并且两个方法之间共享的每个变量会成为此编译器定义的类中的字段。然后,在 ApmPatternWithAnonymousMethod 方法内,编译器会生成代码以构造此类的实例,且引用变量的任何代码都会编译成访问编译器所定义类的字段的代码。编译器还使得内部方法成为新类上的实例方法,允许其代码轻松地访问字段,现在两个方法可以共享数据。

这是匿名方法的出色功能,它可让您像使用方法参数和局部变量一样编写代码,但实际上编译器会重新编写您的代码,从堆栈中取出这些变量,并将它们作为字段嵌入对象。对象可在方法之间轻松传递,并且可以从一个线程轻松迁移到另一个线程,这对于使用 APM 而言是十分完美的。由于编译器会自动执行所有的工作,您可以很轻松地将最后一个参数的空值传递到 BeginRead 方法,因为现在没有要在方法和线程之间显式传递的数据。但是,我仍然无法使用 C# using 语句,因为此处有两个不同的方法,尽管看上去似乎只有一个方法。

以下内容显示了执行图 1 中摘录的代码后的输出。


Primary ThreadId=1
ThreadId=1: 4D-5A-90-00-03 (SynchronousPattern)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithMultipleMethods)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithAnonymousMethod)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithLambdaExpression)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithIterator)


我让 Main 方法显示应用程序主线程的托管线程 ID。然后我让 ProcessData 方法显示执行该方法的线程的托管线程 ID。如您所见,输出显示了所有异步模式让主线程之外的其他线程执行结果,而同步模式则让应用程序的主线程执行所有工作。

还应指出,C# 3.0 引入了一个新功能,称为 lambda 表达式。在执行同一操作时,lambda 表达式功能的语法比 C# 匿名方法功能更简洁。实际上,这对于 C# 团队来说是一个麻烦,因为现在它必须记录和支持产生相同结果的两个不同语法。为了使用 C# 3.0 lambda 表达式功能,ApmPatternWithAnonymousMethod 方法经修改后成为图 1 中所示的 ApmPatternWithLambdaExpression 方法。在此处可以看到语法略为简化,因为编译器能够自动推断出结果参数的类型为 IAsyncResult,而且“=>”要键入的内容比“delegate”少。



foreach 语句

C# 2.0 为 C# 编程语言引入了另一种功能:迭代器。迭代器功能的最初目的是让开发人员能够轻松地编写代码,遍历集合的内部数据结构。要了解迭代器,必须首先好好看一下 C# foreach 语句。编写如下代码时,编译器会将它转化为如图 2 所示的内容:


foreach (String s in collectionObject)
DoSomething(s);


可以看到,foreach 语句提供了遍历集合类中所有项目的简便方法。但是,有很多不同种类的集合类 — 数组、列表、词典、树、链接列表等等 — 每个均使用其内部数据结构来表示项目集合。使用 foreach 语句就是代表要遍历集合中的所有项目,而并不关注集合类内部用来维护其各个项目的数据结构。

foreach 语句使得编译器生成对集合类的 GetEnumerator 方法的调用。此方法创建了一个实现 IEnumerator 接口的对象。此对象知道如何遍历集合的内部数据结构。while 循环的每次迭代都会调用 IEnumerator 对象的 MoveNext 方法。此方法告诉对象前进到集合中的下一个项目,如果成功,则返回 true,如果所有项目均已枚举,则返回 false。在内部,MoveNext 可能只会递增索引,也可能会前进到链接列表的下一个节点,或者它可能会向上向下遍历树。整个 MoveNext 方法的要点是,从执行 foreach 循环的代码中抽象出集合的内部数据结构。

如果 MoveNext 返回 true,则调用 Current 属性的 get 访问器方法会返回 MoveNext 方法抵达项的值,以便 foreach 语句(try 块内部的代码)的主体能处理该项目。当 MoveNext 确定集合的所有项目均已得到处理时,会返回 false。

此时,while 循环退出,并进入 finally 块。在 finally 块中,通过检查来确定 IEnumerator 对象是否实现 IDisposable 接口,如果是,则调用其 Dispose 方法。对记录而言,IEnumerator 接口由 IDisposable 派生而来,因此需要所有 IEnumerator 对象来实现 Dispose 方法。一些枚举器对象在迭代过程中需要附加资源。对象可能会返回文本文件的文本行。foreach 循环退出后(可能会在遍历集合中的所有项目之前发生),finally 块会调用 Dispose,允许 IEnumerator 对象释放这些附加资源 — 例如,关闭文本文件。

posted on 2007-11-22 14:08  simhare  阅读(467)  评论(0)    收藏  举报

导航