本书翻译目的为个人学习和知识共享,其版权属原作者所有,如有侵权,请告知本人,本人将立即对发帖采取处理。
允许转载,但转载时请注明本版权声明信息,禁止用于商业用途!
博客园:韩现龙
Introducing to Microsoft LINQ目录
不必通过学习LINQ来全面理解C# 3.0语言的增强部分。例如,(LINQ)这个新的语言特性中没有一项涉及到CLR变更。LINQ需要新的编译器(C# 3.0 或是 Microsoft Visual Basic 9.0),这些编译器生成的中间代码可以很好的在Microsoft .NET 2.0 下运行,给您提供可以使用的LINQ库。
尽管如此,在这一章里,我们整理出您需要清楚理解并且在LINQ中要用到的那些C#的特性进行一个简洁的描述(从C# 1.x 到 C# 3.0)。如果您打算跳过这一章,您也可以在想理解LINQ语法内部究竟是怎么回事的时候回过头来查看本章。
重温C# 2.0
C# 2.0 在很多方面改进了原有的C#语言。例如,泛型(Generics)的使用让开发人员可以用C#定义方法和类拥有一个或多个类型参数。泛型(Generics)是LINQ的一个支柱。
在本章中,我们将描述一些对LINQ来说很重要的C# 2.0语法:泛型(Generics),匿名方法(Anonymous methods,它们是C# 3.0中lambda表达式的基础),yield关键字以及可以枚举的接口。若您要对LINQ想要有一个较好的理解,您须对这些概念有一个好的理解。
泛型(Generics)
很多编程语言通过定义具体的类型和严格的转换规则来处理变量和对象。用强类型语言写的代码缺少泛化的条件。请思考如下代码:
int Min( int a, int b ) { if (a < b) return a; else return b; }
为了使用这个代码,我们需要一个包含每种类型参数的不同的Min版本来进行比较。习惯于使用对象做为泛型(Generics)的默认类型的开发人员可能会写出如下方法:
object Min( object a, object b ) { if (a < b) return a; else return b; }
不幸的是,通用的对象类型之间(<)操作符无法使用。我们需要使用一个通用的接口(Interface)来处理:
IComparable Min( IComparable a, IComparable b ) { if (a.CompareTo( b ) < 0) return a; else return b; }
然而,即便解决了这个问题,我们将面对一个更大的问题:这个Min方法不确定的结果类型。调用Min可以把IComparable类型转换成int类型,但是可能会导致异常且一定会提高CPU的使用成本。
int a = 5, b = 10; int c = (int) Min( a, b );
C# 2.0 是用泛型(Generics)来解决这个问题的。泛型(Generics)的基本原理就是把类型转换从C#编译器处理转移到CLR的Jitter进行处理。下面是Min方法的泛型版:
T Min<T>( T a, T b ) where T : IComparable<T> { if (a.CompareTo( b ) < 0) return a; else return b; }
【小贴士】
Jitter是.Net运行时的实时编译部分。它把IL代码翻译成机器码。当您编译.Net代码时,编译器把它们成生IL代码,然后在第一次处理前,通过Jitter把IL代码编译成机器码。
把类型处理转到Jitter进行是个不错方法:Jitter可以生成同一段代码的多种版本,其中总有一种可以使用。这个办法类似一个宏的扩展,不同之处在于,这种优化是用来避免代码扩散,即泛型方法(Generics Method)的所有版本都使用参考类型作为泛型(Generics)类型且共用相同的编译好的代码,仅仅是调用的方法不同。
用泛型(Generics)的方法来写如下的代码:
int a = 5, b = 10;
int c = (int) Min( a, b );
您可以写出如下代码:
int a = 5, b = 10;
int c = Min<int>( a, b );
之前的问题没了,代码运行比以前更快。并且,编译器可以从参数推断使用T类型泛型(Generics)的Min方法,所以我们可以写如下的代码:
int a = 5, b = 10;
int c = Min( a, b );
类型推断(Type Inference)
类型接口(Type Inference)是一个关键特性。它允许你写出更多的抽象代码,写这些抽象的代码让编译器处理关于类型的细节。然而,C#的类型转换机制在编译时不能保证类型都正确,也不能拦截错误代码(例如,调用完全不相容的类型的时候)。
泛型(Generics)不仅可以定义泛型方法(Generics Method),而且和类以及接口一样可以使用类型声明。正如前面所说,这本书的目的不是详细地解释泛型(Generics),而是想提醒您泛型(Generics)和LINQ的结合将会用着非常舒服。
委托(Delegates)
委托(Delegate)是封装了一个或多个方法的类。在其内部,一个代理保存了一些方法的指针列表,每个指针都对应于一个含有实例方法的类。
一个委托(Delegate)可以包含若干个方法,但是本章我们只讨论包含一个方法的委托(Delegate)。抽象点看,这个委托(Delegate)类型象一个代码容器。容器中的代码是不可更改的,但是它可以独立的被栈调用或是存储一个变量。它存储一个实例对象,这样就可以延长对象的生命周期直到委托被有效使用。
委托(Delegate)的语法演进是匿名方法(Anonymous Method)的基础,这部分内容我们下一章会提到。声明一个委托(Delegate)其实是定义一个可以实例化本身的类型。委托(Delegate)声明需要一个完整的方法签名。在Listing 2-1中,我们声明了3种不同的类型:有着相同方法签名的它们中的每一种都只能通过外部的方法进行实例化。
Listing 2-1: Delegate declaration
delegate void SimpleDelegate();
delegate int ReturnValueDelegate();
delegate void TwoParamsDelegate( string name, int age );
委托(Delegate)是以前C函数指针的升级安全版。使用C# 1.x,委托(Delegate)可以仅通过明确的创建对象来生成,如Listing 2-2所示:
Listing 2-2: Delegate instantiation (C# 1.x)
public class DemoDelegate {
void MethodA() { … }
int MethodB() { … }
void MethodC( string x, int y ) { … }
void CreateInstance() {
SimpleDelegate a = new SimpleDelegate( MethodA );
ReturnValueDelegate b = new ReturnValueDelegate ( MethodB );
TwoParamsDelegate c = new TwoParamsDelegate( MethodC );
// …
}
}
使用最原始的语法来创建委托(Delegate)是十分乏味的:即使当前的类型强于需要被委托的类型,您仍然不得不去了解委托类(Delegate Class)的名字,因为不允许使用别的任何类型。这样,委托(Delegate)类型才能安全的使用。.
C# 2.0考虑到这点,并允许您跳过那部分语法。之前我们看到的委托实例不使用新的关键字也可以创建。您只需要知道方法名即可。编译器会推断出委托的类型。如果您指定了一个可变的简单委托(SimpleDelegate)类型,那么C#编译器会自动的生成新的简单委托(SimpleDelegate)代码,这一点适用于任何委托(Delegate)类型。Listing 2-3中展示的C# 2.0版和C# 1.x版的代码会生成相同的IL代码。
Listing 2-3: Delegate instantiation (C# 2.0)
public class DemoDelegate {
void MethodA() { … }
int MethodB() { … }
void MethodC( string x, int y ) { … }
void CreateInstance() {
SimpleDelegate a = MethodA;
ReturnValueDelegate b = MethodB;
TwoParamsDelegate c = MethodC;
// …
}
// …
}
您也可以自定义一个泛型的委托类型,显然在一个泛型类(Generic Class)中定义一个委托对于LINQ来说是一个很重要很有用的的功能。
委托的一个常用点是把一些代码注入到一个现存的方法中。在Listing 2-4中,我们假定存在一个我们不想改变的名叫Repeat10Times的方法。
Listing 2-4: Common use for a delegate
public class Writer {
public string Text;
public int Counter;
public void Dump() {
Console.WriteLine( Text );
Counter++;
}
}
public class DemoDelegate {
void Repeat10Times( SimpleDelegate someWork ) {
for (int i = 0; i < 10; i++) someWork();
}
void Run1() {
Writer writer = new Writer();
writer.Text = "C# chapter";
this.Repeat10Times( writer.Dump );
Console.WriteLine( writer.Counter );
}
// …
}
当前用的回调函数被定义为SimpleDelegate,我们想通过注入一个字符串统计这个方法被调用了多少次。定义一个Writer类,把实例化的数据作为Dump方法的参数。正如您看见的,我们需要定义一个单独的类,只用来存放需要用的代码和数据。一条捷径是采用类似的方式但是使用匿名方法(Anonymous Method)。
匿名方法(Anonymous Methods)
在前面的章节中,我们提到了委托的共用。C# 2.0中有一种代码写法,即在Listing 2-4 中言简意赅的使用匿名方法(Anonymous Method)。如Listing 2-5 例子所示。
Listing 2-5: Using an anonymous method
public class DemoDelegate {
void Repeat10Times( SimpleDelegate someWork ) {
for (int i = 0; i < 10; i++) someWork();
}
void Run2() {
int counter = 0;
this.Repeat10Times( delegate {
Console.WriteLine( "C# chapter" );
counter++;
} );
Console.WriteLine( counter );
}
// …
}
在代码中,我们不再声明Writer类。编译器自动的为我们声明一个类并为其命了名。相反,我们定义一个调用Repeat10Times的方法,看上去好像我们把这部分代码作为一个参数在使用。然而,编译器把代码转换为一种类似有明确类使用的共同委托(Common Delegate)的形式。在代码中这种转换的唯一证明就是代码块前的关键字。这个语法被称为匿名方法(Anonymous Method)。
【小贴士】
记住你无法将代码传入变量中,能传入的只是一个指针。在您继续操作前请在确定一遍您没有弄错。
在代码段前标明作为委托关键字的匿名方法(Anonymous Method)。当我们有一个包含一个或多个参数的委托方法时,这个语法允许我们把参数的名字定义为委托(delegate)。Listing 2-6 中的代码定义了一个作为TwoParamsDelegate委托(delegate)类型的匿名方法。
Listing 2-6: Parameters for an anonymous method
public class DemoDelegate {
void Repeat10Times( TwoParamsDelegate callback ) {
for (int i = 0; i < 10; i++) callback( "Linq book", i );
}
void Run3() {
Repeat10Times( delegate( string text, int age ) {
Console.WriteLine( "{0} {1}", text, age );
} );
}
// …
}
我们用2个明确的参数作为委托(delegate)来代替Repeat10Times方法。可以这样理解:如果您移除text和age两个参数的声明,委托(delegate)将产生2个名称为定义的错误。
要点
您将在C# 3.0中(间接地)使用委托和匿名方法,基于这个原因,深刻地理解这些概念是非常重要的。只有这样,您在面对越来越复杂的情况时能从抽象的高度来把握它。
枚举(Enumerators) 和 Yield关键字
C# 1.x 定义两个接口来支持枚举。命名空间System.Collections包含这些声明,如Listing 2-7所示:
Listing 2-7: IEnumerator and IEnumerable declarations
public interface IEnumerator { bool MoveNext(); object Current { get; } void Reset(); } public interface IEnumerable { IEnumerator GetEnumerator(); }
接口就可以枚举实例化IEnumerator接口的对象。这个枚举可以调用MoveNext方法直到它返回False。
Listing 2-8中的代码就是按这种方式定义了一个类。如您所见,CountdownEnumerator这个类更复杂些,且它单独的实现了枚举逻辑。在这个例子里,并不是真的要枚举什么,只是简单的把Countdown类中定义的StartCountdown这个数字开始的降序数字返回出来,Countdown类也是一个枚举类。
public class Countdown : IEnumerable { public int StartCountdown; public IEnumerator GetEnumerator() { return new CountdownEnumerator( this ); } } public class CountdownEnumerator : IEnumerator { private int _counter; private Countdown _countdown; public CountdownEnumerator( Countdown countdown ) { _countdown = countdown; Reset(); } public bool MoveNext() { if (_counter > 0) { _counter--; return true; } else { return false; } } public void Reset() { _counter = _countdown.StartCountdown; } public object Current { get { return _counter; } } }
CountdownEnumerator在委托真的发生时才被使用。例如,Listing 2-9中显示了一种可能的使用情况。
Listing 2-9: Sample enumeration code
public class DemoEnumerator { public static void DemoCountdown() { Countdown countdown = new Countdown(); countdown.StartCountdown = 5; IEnumerator i = countdown.GetEnumerator(); while (i.MoveNext()) { int n = (int) i.Current; Console.WriteLine( n ); } i.Reset(); while (i.MoveNext()) { int n = (int) i.Current; Console.WriteLine( "{0} BIS", n ); } } // … }
调用GetEnumerator就提供了枚举对象。我们用两个循环来显示这个重置方法的用法。我们不得不把当前的返回值转换为int型是因为我们使用的是非通用枚举接口。
【小贴士】 C# 2.0中介绍了支持泛型的枚举。命名空间System.Collections.Generic包含了IEnumerable<T>和IEnumerator<T>的声明。这些接口无需转换即可把数据转成object类型。对于枚举值类型来说,这种能力显得非常重要,因为这样可能不会有装箱或是拆箱操作带来的影响。
自从 C# 1.x起,枚举可以方便地使用foreach来进行操作。Listing 2-10中代码所示的结果和之前例子相同。
Listing 2-10: Enumeration using a foreach statement
public class DemoEnumeration { public static void DemoCountdownForeach() { Countdown countdown = new Countdown(); countdown.StartCountdown = 5; foreach (int n in countdown) { Console.WriteLine( n ); } foreach (int n in countdown) { Console.WriteLine( "{0} BIS", n ); } } // … }
使用foreach,编译器初始化GetEnumerator后,在每一个循环之前调用MoveNext。真正的不同在于,每次循环都生成了代码且没有调用重置方法:即产生了2个CountdownEnumerator对象而不是1个对象。
【小贴士】 foreach也可以用于那些不带枚举接口(IEnumerable interface)但是有一个公共枚举方法(GetEnumerator method)的类。
C# 2.0通过编译器自动生成一个从枚举接口(IEnumerator interface)继承且返回枚举方法(GetEnumerator method)的类来说明yield。Yield仅可以用在return或是break这两个关键字之前。Listing 2-11代码中生成了一个相当于前面介绍的CountdownEnumerator的类。
Listing 2-11: Enumeration using a yield statement
public class CountdownYield : IEnumerable { public int StartCountdown; public IEnumerator GetEnumerator() { for (int i = StartCountdown - 1; i >= 0; i--) { yield return i; } } }
从逻辑上看,yield return等价于在下一个MoveNext被调用前暂缓执行(suspending execution)。回忆一下,GetEnumerator方法在整个枚举中只被调用一次,它继承自IEnumerator接口且返回一个类。那个类从包含yield的方法中继承了那些声明。
一个包含了yield的方法称为一个迭代器(iterator):迭代器(iterator)可以包含多个yield。Listing 2-12中的代码绝对有效且在功能上等效于之前的StartCountdown值为5的CountdownYield类。
Listing 2-12: Multiple yield statements
public class CountdownYieldMultiple : IEnumerable { public IEnumerator GetEnumerator() { yield return 4; yield return 3; yield return 2; yield return 1; yield return 0; } }
通过使用IEnumerator泛型,可以定义一种CountdownYield强类型,如Listing 2-13所示。
Listing 2-13: Enumeration using yield (typed)
public class CountdownYieldTypeSafe : IEnumerable<int> { public int StartCountdown; IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } public IEnumerator<int> GetEnumerator() { for (int i = StartCountdown - 1; i >= 0; i--) { yield return i; } } }
这个强类型版本中包含两个GetEnumerator方法:一个是和非泛型代码兼容的(返回IEnumerable),另一个是强类型(返回IEnumerator<int>)。
LINQ广泛的使用枚举(Enumeration)和yield。即使它们被封装,当你调试代码时也应当想到它们。
校稿:博客园 韩现龙
上一篇:微软免费图书《Introducing Microsoft LINQ》翻译-Chapter1.5 and Chapter1.6:LINQ的现状及前景
下一篇:C#3.0特性