本书翻译目的为个人学习和知识共享,其版权属原作者所有,如有侵权,请告知本人,本人将立即对发帖采取处理。
允许转载,但转载时请注明本版权声明信息,禁止用于商业用途!
博客园:韩现龙
Introducing to Microsoft LINQ目录
C# 3.0 使 C# 走向函数式语言的方向,支持更多声明式编码。 LINQ 使得能广泛地使用所有新特性,它也让您在除了LINQ之外的其它代码中使用更高级的抽象。
小贴士:您可以阅读Mads Torgersen 所写的关于 C# 和功能语言有趣的帖子,他是C# 语言的项目管理者,他的博客是:http://blogs.msdn.com/madst/archive/2007/01/23/is-c- becoming-a-functional-language.aspx
本地类型推断
类型推断对于任何一种语言来说都是一个完美的特性。它保护类型安全且允许您编写更"自由"的代码。换句话说,你可以定义变量且使用它而不用过多担心它们的类型,留给编译器去从给变量自身赋值的表达式中推断变量的正确类型。
对比于你想使用的类型而言,使用类型推断可能会产生一些不甚明确的代码,但是,在我们看来,当精确类型定义不是特别有意义时,这个特性就简化了local 变量的代码维护。
C# 3.0 提供了类型推断,它允许您用 var 关键字取代特定类型来定义一个变量。这也许像等同于定义一个 object 类型的变量,但它不是。下面的代码给您演示 object 类型需要对值类型进行装箱操作(看 b 的声明),并且无论如何,当你想对特定类型操作时,都需要一个转换操作符(看 d 的赋值):
var a = 2; // a is declared as int
object b = 2; // Boxing an int into an object
int c = a; // No cast, no unboxing
int d = (int) b; // Cast is required, an unboxing is done
使用 var 时,编译器从初始化变量的表达式中推断类型。编译后的 IL 代码只包含推断出的类型。换句话说,请考虑这些代码:
int a = 5;
var b = a;
它完全等同于下面的例子:
int a = 5;
int b = a;
这些为什么重要?关键字 var 让人们联想到组建对象模型 Component Object Model (COM) 的类型 VARIANT,它在 Visual Basic 6.0 中普遍采用,但事实上它们完全不同,因为关键字 var是一个类型安全的声明。实际上,当你运用它时便推断出类型。
从某种程度上说,var 可能是懒惰程序员的工具。然而,var 是定义匿名类型变量的唯一方式,这我们将在后面讨论。
小贴士:变量 Variants 是 COM 实现变量类型后期绑定的一种方式。使用 variants 没有编译时检测,这使得运行代码时(经常是代码被最终用户运行时)出现大堆讨厌的bug。
关键字 var 只能在本地范围内使用。换句话说,可以用这种方式去定义本地的变量,可是成员或者参数却不能。下面的代码演示正确使用 var 的例子:x、y 和 r 都是 double 类型; d 和 w 是 decimal 类型;s 和 p 是 string 类型;l 是 int 类型。请注意:常量 2.3 定义由 3 个变量推断出的类型,关键字 default 是一个推断出p 的正确类型的"有类型"的 null。
public void ValidUse( decimal d ) {
var x = 2.3; // double
var y = x; // double
var r = x / y; // double
var s = "sample"; // string
var l = s.Length; // int
var w = d; // decimal
var p = default(string); // string
}
下面例子演示关键字 var 不允许使用的一些场合:
class VarDemo {
// invalid token 'var' in class, struct or interface member declaration
var k =0;
// type expected in parameter list
public void InvalidUseParameter( var x ){}
// type expected in result type declaration
public var InvalidUseResult() {
return 2;
}
public void InvalidUseLocal() {
var x; // Syntax error, '=' expected
var y = null; // Cannot infer local variable type from 'null'
}
// …
}
k 的类型能从常量初始化器中推断,但是 var 不允许作为成员的类型。 InvalidUseResult 返回值的类型虽然可以从内部的return语句声明中推断出来,但这种用法依然是不允许的。
这种简单的语言特性允许我们编写实际上几乎排除所有的 local 变量的类型定义的代码。虽然这样简化了代码编写,但是它降低了代码的可读性。例如,您调用一个重载方法,而方法的各个版本仅是参数类型不同,阅读这代码将不清楚哪个版本的方法被调用。总之,差劲地重载方法会产生类似的问题:当方法的行为(和意义)不同时您可以使用不同的方法名。
Lambda 表达式(Lambda Expressions)
C# 2.0 通用使用匿名方法引入了"传递指针到特定代码"作为参数的功能。这是一个功能强大的概念,但是这种方式您实际传递的是方法的一个指针,而不是代码块。那个引用指向编译时生成的强类型代码。使用泛型,您可获得更大灵活性,但是对泛型类型难以应用标准操作符
C# 3.0 引入 lambda 表达式,它允许使用更简练的语法来定义匿名方法。Lambda 表达式还可以通过创建表达式树 express tree,可选择地推迟代码生成,在代码实际生成前允许更多操作,这发生在运行时。表达式树仅能从特定表达式"代码块"生成。
下面代码是演示使用匿名方法的例子:
public class AggDelegate {
public List<int> Values;
delegate T Func<T>( T a, T b );
static T Aggregate<T>( List<T> l, Func<T> f ) {
T result = default(T);
bool firstLoop = true;
foreach( T value in l ) {
if (firstLoop) {
result = value;
firstLoop = false;
}
else {
result = f( result, value );
}
}
return result;
}
public static void Demo() {
AggDelegate l = new AggDelegate();
int sum;
sum = Aggregate(
l.Values,
delegate( int a, int b ) { return a + b; }
);
Console.WriteLine( "Sum = {0}", sum );
}
// …
}
下面的例子,我们使用Aggregate 方法的类似版本,因而我们不需每次都重新创建它。作为参数传递给 Aggregate 的匿名方法,定义了 List 对象的每个元素都要执行的聚合操作。
使用 lambda 表达式语法,我们可以编写如 Listing 2-14 的Aggregate 调用。
Listing 2-14: 精确类型参数列表
sum = Aggregate(
l.Values,
( int a, int b ) => { return a + b; }
);
您可以将这规则读作:"给定的整型变量 a 和 b,返回 a 和 b 的和 a+b。"
我们去掉了参数列表前面的关键字 delegate,而在参数列表和方法代码之间增加了=> 标记。眼下,区别只在于语法,因为其编译后的代码与匿名方法语法编译后的结果相同。然而,lambda 表达式语法允许您编写如 Listing2-15 所示的代码。
Listing 2-15: 隐示的类型参数列表
sum = Aggregate(
l.Values,
( a, b ) => { return a + b; }
);
小贴士: "=>"标记没有官方定义。,一些开发人员在lambda 表达式是谓词时读作"such that", 而当它是介词时读作"becomes"。另外一些开发人员一般读作"goes to"。
您可以把这规则读作:"给定 a 和 b, 返回 a+b, 凡是'+'表示 a 和 b 的类型。"(从上下文推断,a 和 b 的具体类型必需存在"+"操作符,否则代码将不能被编译。)
尽管我们从参数列表中去掉了参数类型,编译器会从 Aggregate 的调用中推断参数类型。我们调用的是范型方法,但是范型类型 T 由参数 l.Values 决定,而它是 List<int> 类型。在这个调用中,T 是 int 型;因而,委托 Func<T> 是 Func<int>,a 和 b 都是 int 型。
您可能认为这些语法更像 var 声明,而不是范型用法。类型推断在编译时进行,如果一个参数的类型是范型,您不能访问操作符和成员,除了那些经类型约束而允许的。如果是一个普通类型,您可以完全访问该类型最终定义的操作符(例如我们使用的"+"操作符)和成员。
Lambda表达式可以通过两种方式定义一个主体。我们已经看到了这个声明主体,它需要像其他的代码块中使用的括号,和一个在表达式之前必需的return声明语句。别一种形式就是表达式主体,这个主体可以在代码块只是跟随一个表达式之后的return语句时使用。如Listing2-16所示,你可以忽略掉括号和那个return语句:
sum = Aggregate(
l.Values,
( a, b ) => a + b
);
在首次使用Lambda表达式时,在我们意识到它只是一些能够用来写匿名方法的更加强大的语法之前可能会感到迷惑。这个概念很重要,你必须记住,因为你经常会在参数列表中遇到一些标识字符。也就是说,参数列表定义了匿名方法的参数。在lambda表达式主体中的其他一些标识符(声明语句、或者表达式)必须在匿名方法中得到体现。如下的代码展示了这种事例(AggregateSingle<T>方法为第二个参数使用了一个稍微不同的代理,该代理声明为delegate T FuncSinlgle<T)(Ta)).
int sum = 0;
sum = AggregateSingle(
l.Values,
( x ) => sum += x
);}
该lambda表达式仅有一个x参数;sum仅是一个本地方法中的一个变量,它的生存期是扩展于代理的指向由lambda表达式本身定义的一个匿名方法的生存期。相对应的return sum += x的声明语句将会是对x相加之后的sum的值。
当lambda表达式只有一个参数时,圆括号可以在参数列表中被忽略,如下面这个例子:
int sum = 0;
sum = AggregateSingle(
l.Values,
x => sum += x
);}
如果在lambda表达式中没有参数时,在=>之前必须有两个圆括号().Listing2-17的代码展示了一些可能的语法情况:
Listing 2-17: Lambda expression examples
( int a, int b ) => { return a + b; } // Explicitly typed, statement body
( int a, int b ) => a + b; // Explicitly typed, expression body
( a, b ) => { return a + b; } // Implicitly typed, statement body
( a, b ) => a + b // Implicitly typed, expression body
( x ) => sum += x // Single parameter with parentheses
x => sum += x // Single parameter no parentheses
() => sum + 1 // No parameters
Predicate and Projection
一些lambda表达式有一些基于它们的用途的一些特定的名字:
- "判断语句"是一个表示在一个组中的某个元素的Boolean表达式。例如,它可以被用来定义如何在一个循环中进行过滤其他项:
// Predicate
( age ) => age > 21
- "投影"是指返回和它的仅有的一个参数的类型不同的类型的一个表达式:
// Projection: takes a string and returns an int
( s ) => s.Length
Lambda表达式的一个实际的应用就是在调用一个方法时在该方法的参数列表中写一些小的代码片断。正在的代码展示的是,将"判断语句"作为一个参数传入Display方法(该方法是遍历数组中的所有元素),并且只显示那些使这个"判断语句"为true的元素。"判断语句"和它的用法有代码中加粗显示, Listing 2-18中的Func代理在下一页中有解释。
Listing 2-18: Lambda expression as a predicate
public static void Demo() {
string[] names = { "Marco", "Paolo", "Tom" };
Display( names, s => s.Length > 4 );
}
public static void Display<T>( T[] names, Func<T, bool> filter ){
foreach( T s in names) {
if (filter( s )) Console.WriteLine( s );
}
}
这段代码的执行将生成一个多于四个字母的名字列表。这种语法的简明性是在LINQ中使用lambda表达式的原因之一,另外一个原因是lambda能够创建表达式树。
在这一点上,我们已经考虑到将在声明主体和表达式主体之间的不同仅作为用来对同样的代码进行检索的不同语法,但是其他不止是这些。Lambda表达式也可以被用来指定为代理类型变量。
public delegate T Func< T >();
public delegate T Func< A0, T >( A0 arg0 );
public delegate T Func<A0, A1, T> ( A0 arg0, A1 arg1 );
public delegate T Func<A0, A1, A2, T >( A0 arg0, A1 arg1, A2 arg2 );
public delegate T Func<A0, A1, A3, T> ( A0 arg0, A1 arg1, A2 arg2, A3 arg3 );
定义这些代理没有什么要求。LINQ在System.Linq命名空间下定义这些代理,但是lambda表达式在功能上并不依赖于这些声明。你可以定义你自己的代理,即便你不使用Func这个名。不过有一种情况是特殊的:如果你将一个lambda表达式转换为一个表达式树,编译器将会生成一个可以被操作并且在执行时转换为可执行代码的二进制的表达式。表达式树是System.Linq.Expressions.Expression<T>类的一个实例,在这里T是表达式树所代表的代理。
在许多情况下,通过使用lambda表达式生成表达式树使得lambda表达式和代理方法有了相似之处。不同的是,代理方法在编译时已经被描述为IL代码(仅使用的类型参数未被完全指明),而表达式树仅在执行时才被描述为IL代码。只有lambda表达式和表达式主体才能被转换为一个表达式树,而若lambda表达式如果包括一个声明主体的话就行不通了。
Listing 2-19 显示了同一个lambda表达式如何被转换为代理或者一个表达式树。加粗显示的行展示了表达式树的声明和它的使用。
Listing 2-19: Use of an expression tree
class ExpressionTree {
delegate T Func<T>( T a, T b );
public static void Demo() {
Func<int> x = (a, b) => a + b;
Expression<Func<int>> y = (a, b) => a + b;
Console.WriteLine( "Delegate" );
Console.WriteLine( x.ToString() );
Console.WriteLine( x( 29, 13 ) );
Console.WriteLine( "Expression tree" );
Console.WriteLine( y.ToString() );
Console.WriteLine( y.Compile()( 29, 13 ) );
}
}
这里输入的是Demo方法执行的结果。调用的结果同样是42,但是对ToString()的调用输出却不同:
Delegate ExpressionTree+Func`1[System.Int32]
42
Expression tree (a, b) => (a + b)
42
表达式树维护了在内存中的对一个表达式的描述。你不能像我们在x代理语法上那样来对一个代理对表达式树调用。当你使用表达式时,你需要对它进行编译。对Compile方法的调用返回一个代理,这个代理可以通过Invoke方法进行调用(或者像我们在上一个例子中使用的那种语法进行调用)。在此不对此进行深入探讨,但是对LINQ的请多部分来说都是非常重要的基础。例如,LINQ to SQL中有能够定位到表达式树并且将它转换为SQL语句的方法。转换是在运行时进行,而不是在编译时进行。
扩展方法(Extension Methods)
C# is an object-oriented programming language that allows the extension of a class through inheritance. Nevertheless, designing a class that can be inherited in a safe way and maintaining that class in the future is hard work. A safe way to write such code is to declare all classes as sealed, unless they are designed as inheritable. In that case, safety is set against agility.
C#是一个面向对象的编程语言,它允许通过继承对一个类进行扩展。然而,设计一个能够被安全的扩展的类并且在将来对它进行维护却不是一件易事。写这代码的一个比较安全的方法是将所有不需要继承的类声明为sealed类型的。,在那种情况下,安全和灵活就需要进行权衡了。
更多信息 Microsoft .NET允许在X.DLL中的类A被在Y.DLL中的类B继承。这意味着X.DLL的新版本应该设计为和Y.DLL相兼容。C#和.NET有在这方面上有许多帮助工具。然而,如果你想允许某个类被派生的话,你必须将该类设计为可被继承的;否则,在基类的一点变化可能会影响到在派生类中的代码运行。如果你未将类设计为可被继承的,你就最好将它设计为sealed,或者至少设计为private或internal的。
C#3.0引入了一个可以在不对一个类生成一个新类型的情况下通过添加一个新的方法而对现在的类型进行扩展(无论是通过引用还是值)。一些人可能会认为这样做的结果只是在语法上的一些小变化,但是这种功能却使LINQ变得更易读,也更易写。对一个类型进行扩展的方法只能对一个类型它本身的公有成员进行扩展,正如你可以在目标类型外部可以使用任何的代码块。
下面的代码展示了用传统的方法写出两个方法(FormattedUS and FormattedIT),将一个decimal类型的值转根据特定culture格式化为string类型:
static class Traditional {
public static void Demo() {
decimal x = 1234.568M;
Console.WriteLine( FormattedUS( x ) );
Console.WriteLine( FormattedIT( x ) );
}
public static string FormattedUS( decimal d ) {
return String.Format( formatIT, "{0:#,0.00}", d );
}
public static string FormattedIT( decimal d ) {
return String.Format( formatUS, "{0:#,0.00}", d );
}
static CultureInfo formatUS = new CultureInfo( "en-US" );
static CultureInfo formatIT = new CultureInfo( "it-IT" );
}
除了方法的参数之间有联系之外,在方法和decimal类型之间并无联系。我们可以将这段代码写成对decimal类型的扩展。它(decimal)是一个值类型的,并且不可继承,但是我们在第一个参数之前添加上this关键字,而且通过这种方式我们就可以像使用deimal类型内部定义的方式一样使用这种方法。在Listing 2-20与上面代码不同的部分用高亮显示:
Listing 2-20: Extension methods declaration
static class ExtensionMethods {
public static void Demo() {
decimal x = 1234.568M;
Console.WriteLine( x.FormattedUS() );
Console.WriteLine( x.FormattedIT() );
Console.WriteLine( FormattedUS( x ) ); // Traditional call allowed
Console.WriteLine( FormattedIT( x ) ); // Traditional call allowed
}
static CultureInfo formatUS = new CultureInfo( "en-US" );
static CultureInfo formatIT = new CultureInfo( "it-IT" );
public static string FormattedUS( this decimal d ){
return String.Format( formatIT, "{0:#,0.00}", d );
}
public static string FormattedIT( this decimal d ){
return String.Format( formatUS, "{0:#,0.00}", d );
}
}
扩展方法(extension method)必须是static和public的,必须定义在一个static的类中,并且必须在第一个参数类型之前有this关键字,第一个参数就是这个方法扩展的。扩展方法是public的因为它们(并且通常是)在声明它们的类的外部进行调用。
虽然这并不一个较大的变革,其一个优点可能是Microsoft的智能感知支持,可以一个给定的标识符的可见的扩展方法进行智能感知。然后,扩展方法的产生的类型可能是被扩展的类型本身。在这种情况下,我们可以对一个类型进行许多的扩展,这些扩展均工作在同样的数据上。LINQ经常这样使用扩展方法:
在Listing 2-21的代码中我们对decimal类型写了一组扩展:
Listing 2-21: Extension methods for native value types
static class ExtensionMethods {
public static decimal Double( this decimal d ) {
return d + d;
}
public static decimal Triple( this decimal d ) {
return d * 3;
}
public static decimal Increase( this decimal d ) {
return ++d;
}
public static decimal Decrease( this decimal d ) {
return --d;
}
public static decimal Half( this decimal d ) {
return d / 2;
}
// …
}
在Listing2-22中,我们可以比较一下两个调用的语法,一个是精典的调用(y)和新的调用方法(x)。
Listing 2-22: Extension methods call order
decimal x = 14M, y = 14M;
x = Half( Triple( Decrease( Decrease( Double( Increase( x ) ) ) ) ) );
y = y.Increase().Double().Decrease().Decrease().Triple().Half();
小贴士 意识到扩展方法的重要性是很重要的。当你调用一个类型的实例方法时,你可能会希望这些实例的状态可以通过你的调用被改变。但是不要忘记,就像我们已经说过的那样,你只能通过调用扩展类型的公有成员来实现它的状态的改变。当扩展方法返回一个和它扩展的类型一样的类型时,你可以假定该类型的的实例状态是不应该被改变的。对于扩展值类型的类型是推荐的,而对于一个引用类型的扩展可能会花费(对所有的调用都要创建一个对象的拷贝)比较高的代价。
扩展方法并不是自动被考虑的。对于它的解决方案要遵循几个原则。下面是用来为一个标识符解决其方法的一个求值顺序:
- 实例方法(Instance method):如果一个实例方法存在的话,它就有优先权。
- 扩展方法(Extension method):对于扩展方法的查找是通过在当前命名空间和在所有的当前using块中引入的命名空间下所有静态类(static classes)。(当前命名空间(current namespace)最近的声明的命名空间,该命名空间包容了静态类和扩展方法的声明),如果两个类型存在相同的扩展方法的话,编译器将会抛出一个异常来。
扩展方法最平常的使用是在特定的命名空间下的静态类中定义它们,通过在模块中声明一个或多个using指令来引入它们。
在首次看来,这些用来解决一个方法调用的规则定义了一个不是太透明的特性。当你在一个类上调用一个扩展方法时,它总是会为一个为特殊类型定义的成员方法的一个方法的特定版本所替换。也就是说,扩展方法代表了一个方法的"默认"实现,这个默认实现总会为特定的类的特定版本重写。
我们通过一些事例来看一下这个特点。第一个代表事例包括了一个object类型的扩展方法;通过这种方式,我们可以为任意类型的实例上调用Display方法。在Customer类实例上来调用它:
public class Customer {
protected int Id;
public string Name;
public Customer( int id ) {
this.Id = id;
}
}
static class Visualizer {
public static void Display( this object o ) {
string s = o.ToString();
Console.WriteLine( s );
}
}
static class Program {
static void Main() {
Customer c = new Customer( 1 );
c.Name = "Marco";
c.Display();
}
}
对这段代码的执行结果是类名Customer。
我们可以为Customer类自定义这个Display方法,定义一个如Listing2-23重载的扩展方法(我们可以在另外一个命名空间下定义一个重载的方法,如果该命名方法优先级高于原来的命名空间)。
Listing 2-23: Extension methods overload
static class Visualizer {
public static void Display( this object o ) {
string s = o.ToString();
Console.WriteLine( s );
}
public static void Display( this Customer c ) {
string s = String.Format( "Name={0}", c.Name );
Console.WriteLine( s );
}
}
通过结果我们可以看出,这次"更精确的版本"被执行了:
Name=Marco
在不移除这些扩展方法的情况下,我们可以通过将它作为Customer类实例方法的一个实现来为Display添加另一个行为。在Listing2-24中对Display的实现,会高于其他任何等同于或者从Customer过来的扩展方法。
Listing 2-24: Instance method over extension methods
public class Customer {
protected int Id;
public string Name;
public Customer( int id ) {
this.Id = id;
}
public void Display() {
string s = String.Format( "{0}-{1}", Id, Name );
Console.WriteLine( s );
}
}
在这儿显示的执行结果,说明了这个实例方法被调用了:
1-Marco
在第一眼看到时,这种变化看起来像是和虚方法巧合了。但实际上并非如此,因为扩展方法是在编译阶段被确定下来的,而虚方法是在执行阶段被确定的。这意味着如果你调用一个被作为基类的对象的扩展方法时,其包含的对象的实例类型就无关。如果一个可兼容的扩展方法存在的话(即使它是一个派生类),它会被用在实例方法上。Listing 2-25说明了这一点:
Listing 2-25: Extension methods resolution
public class A {
public virtual void X() {}
}
public class B : A {
public override void X() {}
public void Y() {}
}
static public class E {
static void X( this A a ) {}
static void Y( this A b ) {}
public static void Demo() {
A a = new A();
B b = new B();
A c = new B();
a.X(); // Call A.X
b.X(); // Call B.X
c.X(); // Call B.X
a.Y(); // Call E.Y
b.Y(); // Call B.Y
c.Y(); // Call E.Y
}
}
X方法在实例方法中是被提到。它是一个虚方法,因此c.X()调用了B.X方法的实现。在这些对象上,扩展方法E.X从来没被调用过。
Y方法仅定义在B类中。它是对A类的一个扩展方法,因此只有b.Y()调用了B.Y()。注意c.Y()调用了E.Y是因为标识符c被定义为A类型,即使它存在了B类型的一个实例,因为Y在类A中没有被定义。
对泛型的扩展方法的最终观点是当你使用一个泛型类型作为一个参数时,你应该以this关键字来标识它,你要知道并非仅对此类进行了扩展,扩展的是这一组类。我们发现在设计组件词典时这种操作不是太直观,但是在写使用它们的代码时却非常舒服。下面的代码对上面的lambda表达式的事例进行了一点小小的发动,我们向names参数添加上了this关键字,将Display方法的调用做了修改。一些重要的修改在Listing 2-26上用高亮字显示出来了。
Listing 2-26: Lambda expression as predicate
public static void Display<T>( this T[] names, Func<T, bool> filter ) {…}
public static void Demo() {
string[] names = { "Marco", "Paolo", "Tom" };
names.Display( s => s.Length > 4 );
// It was: Display( names, s => s.Length > 4 );
}
Display方法可以和一个不同的类一起使用(比如int类型的数组),并且它会要求你有一个和array相同类型的参数。下面的代码使用了同样的Display方法,仅显示出偶数:
int[] ints = { 19, 16, 4, 33 };
ints.Display( i => i % 2 == 0 );
随着你对扩展方法理解的不断加深,你会发现它这语言更灵活,而且是强类型的。
新特性待续……两个小时译了这么多,累死我了。