本书翻译目的为个人学习和知识共享,其版权属原作者所有,如有侵权,请告知本人,本人将立即对发帖采取处理。
允许转载,但转载时请注明本版权声明信息,禁止用于商业用途!
博客园:韩现龙
Introducing to Microsoft LINQ目录
对象初始化表达式(Object Initialization Expressions)
C#1.x允许我们在单独的声明上对字段或者本地的变量进行初始化。这儿展示的语法可以初始化一个单独的标识符:
int i = 3;
string name = 'Unknown';
Customer c = new Customer( "Tom", 32 );
当这种初始化表达式用于引用类型时,它需要调用类的构造函数,构造函数可能含有标识如何对该类进行实例化的参数。你可以对引用类型和值类型都使用对象初始化器。
当你想初始化一个对象(无论是引用类型还是值类型)时,你需要有含有足够参数的构造函数来指明该对象的初始状态如何被初始化:思考如下代码:
2 public int Age;
3 public string Name;
4 public string Country;
5 public Customer( string name, int age ) {
6 this.Name = name;
7 this.Age = age;
8 }
9 // …
10}
11
Customer实例是通过Customer类的构造函数初始化的,但是我们仅设定了它的Name和Age字段。如果我们想设定Country而并且Age,我们要写如Listing 2-27这样的代码:
Listing 2-27: Standard syntax for object initialization
customer.Name = "Marco";
customer.Country = "Italy";
C#3.0为对象初始化语法引入了更简洁的模式,如Listing 2-28:
Listing 2-28: Object initializer
Customer customer = new Customer { Name = "Marco", Country = "Italy" };
小贴士 用来初始化对象(标准的对象初始化器)的语法在代码编译后是相同的。对象初始化器产生了一个为特定类型进行的构造函数的调用(无论是引用类型还是值类型):无论何时在类型名称和开放括号之前你没有用括号闭合时它都是默认的构造函数。如果该构造函数对成员字段成功地进行了初始化,编译器还是会做那个工作,即使这些声明可能没有被用到。如果被初始化类型的构造函数是空的话,对象初始化器是不会有额外的花销的。
这些在初始化列表中指定的名字和被初始化对象的公用的字段或者属性有关。若默认的构造函数对一个类型不可用的话,语法也允许对非默认构造函数进行调用。在Listing2-29中展示了这个例子:
Listing 2-29: Explicit constructor call in object initializer
2Customer c1 = new Customer() { Name = "Marco", Country = "Italy" };
3
4// Explicitly specify nondefault constructor
5Customer c2 = new Customer( "Paolo", 21 ) { Country = "Italy" };
6
The c2 assignment above is equivalent to this one:
c2的声明和下面的代码是一样的:
c2.Country = "Italy";
小贴士 对象初始化器的实现其实是创建并初始化对象为一个临时的变量,并且仅在最后才将该对象的引用拷贝到目标变量。通过这种方法,在对象完全被初始化之前对另外一个线程是不可见的。
这种对象初始化器的好处之一是它允许你以函数的形式写出一个完整的初始化函数:你可以在不用另外声明的情况下将它写在表达式中。因此,也可以进行语法嵌套,为一个成员变量的初始值写入初始化对象中。经典的Point和Rectangle类的事例说明了这一点(Listing 2-30)
Listing 2-30: Nested object initializers
int x, y;
public int X { get { return x; } set { x = value; } }
public int Y { get { return y; } set { y = value; } }
}
public class Rectangle {
Point tl, br;
public Point TL { get { return tl; } set { tl = value; } }
public Point BR { get { return br; } set { br = value; } }
}
// Possible code inside a method
Rectangle r = new Rectangle {
TL = new Point { X = 0, Y = 1 },
BR = new Point { X = 2, Y = 3 }
};
这个对r变量的初始化和下面代码是等价的:
Point point1 = new Point();
point1.X = 0;
point1.Y = 1;
rectangle2.TL = point1;
Point point2 = new Point();
point2.X = 2;
point2.Y = 3;
rectangle2.BR = point2;
Rectangle rectangle1 = rectangle2;
如上面代码如示,用最短的代码来实现的语法对于程序的可读性来说是非常有帮助的。在对象初始化器中,两个临时变量point1和point2依然是被创建了,但是我们却没有显示的对它们进行定义。
前面的事例通过引用类型使用了嵌套的对象初始器。同样的语法也适用于值类型,但是你必须明白,在TL和BR变量在初始化时,一个临时的Point对象的拷贝被创建了。
小贴士在对于大的值类型进行值复制时可能会有性能影响。但是这影响并不是因为使用对象初始化器产生的。
对象初始化语法仅可以用在在对字段或变量的值进行初始化时。关键字new仅在最终声明时才是必须的。在初始化器中,在对象的成员初始化时你可以不使用new关键字。在这种情况下,代码就使用了通过构造函数而创建的对象实例。如Listing2-31所示:
Listing 2-31: Initializers for owned objects
TL和BR成员实例通过Rectangle类的构造函数被显示地创建。对象初始化器不需要使用new关键字。这样,初始化器就对已经存在的实例TL和BR进行操作。
public class Rectangle {
Point tl = new Point();
Point br = new Point();
public Point TL { get { return tl; } }
public Point BR { get { return br; } }
}
// Possible code inside a method
Rectangle r = new Rectangle {
TL = { X = 0, Y = 1 },
BR = { X = 2, Y = 3 }
};
到现在为止,该事例中我们在对象初始化器中使用了一些常量。你也可以使用其他的计算值,如下所示:
Name = c1.Name, Country = c2.Country, Age = c2.Age };
C#1.x已经有了和初始化器的概念,并且语法也和这个相类型,但是它仅限于数组:
string[] customers = { "Jack", "Paolo", "Marco" };
同样的新对象初始化器语法也可以对集合(collections)使用。内部列表可以由常数,表达式或者其他的初始化值组成,就像我们刚才展示的其他的对象初始化器一样。如果集合类实现了System.Collections.Generic.ICollection<T>接口,对于在初始化器中的每个元素来说,对于ICollection<T>.Add(T)的调用是和元素的顺序相同的。Add()方法在初始化器中为每个元素调用。在Listing2-32中展示了使用集合的事例。
Listing 2-32: Collection initializers
总的来说,对象和集合初始化器允许在一个单独的函数中对一组对象(即便是嵌套的)进行创建和初始化。LINQ对这种特性进行了扩展,特别是通过匿名方法(anonymous types)。
// Collection classes that implement ICollection<T>
List<int> integers = new List<int> { 1, 3, 9, 18 };
List<Customer> list = new List<Customer> {
new Customer( "Jack", 28 ) { Country = "USA"},
new Customer { Name = "Paolo" },
new Customer { Name = "Marco", Country = "Italy" },
};
// Collection classes that implement IEnumerable
ArrayList integers = new ArrayList() { 1, 3, 9, 18 };
ArrayList list = new ArrayList {
new Customer( "Jack", 28 ) { Country = "USA"},
new Customer { Name = "Paolo" },
new Customer { Name = "Marco", Country = "Italy" },
};
匿名方法(Anonymous Types)
对象初始化器也可以在不指明类的情况下使用。若那样做的话,一个新类-匿名类型-就被创建了。请思考Listing 2-33所示的代码:
Listing 2-33: Anonymous types definition
c1 和 c2两个变量是Customer类型的,但是c3, c4, c5,和 c6就不能通过代码轻易地读出来它们的类型了。关键字var应该从一个指定的表达式中去推断变量的类型,但是这里它有一个没有指明类型的new关键字。像你想象的那样,这种类型的对象初始化器将生成一个新类。
Customer c1 = new Customer { Name = "Marco" };
var c2 = new Customer { Name = "Paolo" };
var c3 = new { Name = "Tom", Age = 31 };
var c4 = new { c2.Name, c2.Age };
var c5 = new { c1.Name, c1.Country };
var c6 = new { c1.Country, c1.Name };
生成的新类有公共有属性和在初始化器中存在的各个参数的隐藏的私有字段:它的名字和类型是从对象初始化器本身推断出来的。当名字不太明确时,它将从初始化表达式却推断,如c4,c5和c6的定义。这种较短的语法是叫做初始化器的投影,因为它不仅投影了一个值,还投影了该值的名字。
对于所有可能的属性有相同名称和类型的匿名类型,那个类同样适用。用下面的代码我们可以看到自动生成的类型的名称:
Console.WriteLine( "c2 is {0}", c2.GetType() );
Console.WriteLine( "c3 is {0}", c3.GetType() );
Console.WriteLine( "c4 is {0}", c4.GetType() );
Console.WriteLine( "c5 is {0}", c5.GetType() );
Console.WriteLine( "c6 is {0}", c6.GetType() );
下面是输出的内容:
c1 is Customer
c2 is Customer
c3 is <>f__AnonymousType0`2[System.String,System.Int32]
c4 is <>f__AnonymousType0`2[System.String,System.Int32]
c5 is <>f__AnonymousType5`2[System.String,System.String]
c6 is <>f__AnonymousTypea`2[System.String,System.String]
匿名类型不可以通过代码推测(因为你并不知道它生成的名称),但是它可以在对象实例上进行查询。变量c3和c4是相同的匿名类型,因为它们有相同的字段和属性。即便c5和c6有相同的属性(类型和名称),但是因为它们的顺序不同,仅此一点,编译器就生成两个不同类型的匿名类型。
重要通常来说,类型中的成员的顺序并不重要,即使标准对象的初始化器是基于成员的名称而非它们的顺序。LINQ为两个仅在成员变量的顺序上不同的类获取不同类型的需要源自于一个有序的字段组,比如在SElECT语句中。
初始化一个有类型数组的语法在C#3.0中已经得到了加强。现在你可以声明一个数组初始化器,并且从初始化器内容中引用该类型。这种结构可以和匿名类型和对象初始化器关联起来,如Listing2-34所示:
Listing 2-34: Implicitly typed arrays
小贴士: C#1.x中的语法需要指定的变量为一个确定的类型。C#3.0的语法允许使用var关键字来以这种方式初始化的变量。
var ints = new[] { 1, 2, 3, 4 };
var ca1 = new[] {
new Customer { Name = "Marco", Country = "Italy" },
new Customer { Name = "Tom", Country = "USA" },
new Customer { Name = "Paolo", Country = "Italy" }
};
var ca2 = new[] {
new { Name = "Marco", Sports = new[] { "Tennis", "Spinning"} },
new { Name = "Tom", Sports = new[] { "Rugby", "Squash", "Baseball" } },
new { Name = "Paolo", Sports = new[] { "Skateboard", "Windsurf" } }
};
ints是一个int的数组,ca1是Customers的数组,ca2是一个匿名类型的数组,每一个都包括一个字符串类型(Name)和一个字符串数组(Sports)。在ca2的定义中你看不到类型的定义,因为所有的类型都是从初始化表达式中推断出来的。重新看一下ca2,注意ca2的声明是一个单独的表达式,它可以被嵌入到另外一个中。
查询表达式(Query Expressions)
C#3.0引入了查询表达式(query expressions)的概念,它和SQL语法相类似,用来对数据进行操作。这个语法被转换成C#3.0中的常规语法,用来对作为LINQ字典的一部分的类,方法和数组进行操作。我们不能对所有的关键字都一一进行作详细的介绍,它超出了本章的范围。在第四章中“LINQ Syntax Fundamentals”中将对查询表达式的语法作更进一步的介绍。
在本小节中,我们想简要地介绍一下编译器对查询表达式进行的转换,描述一下代码是如何被解释的。
下面是一个典型的LINQ查询:
var customers = new []{
new { Name = "Marco", Discount = 4.5 },
new { Name = "Paolo", Discount = 3.0 },
new { Name = "Tom", Discount = 3.5 }
};
var query =
from c in customers
where c.Discount > 3
orderby c.Discount
select new { c.Name, Perc = c.Discount / 100 };
foreach( var x in query ) {
Console.WriteLine( x );
}
查询表达式以from关键字开始(在C#中,所有的查询表达式都是区分大小写的),以select或者group关键字结束。from关键字表明了LINQ将操作于哪个对象,该对象必须是一个实现了IEnumerable<T>接口的类的实例。
该代码的运行结果如下:
{ Name = Tom, Perc = 0.035 }
{ Name = Marco, Perc = 0.045 }
.Where( c => c.Discount > 3)
.OrderBy( c => c.Discount )
.Select( c=> new { c.Name, Perc = c.Discount / 100 } );
每个查询语法都和一个泛型方法相关联,通过适用于扩展方法的规则来解决关联问题。因此,即使它因为可以推断多种定义比如在lambda表达式中的参数名而显得更加智能,查询语法同宏扩展相类似。
在这一点上,必须清楚为什么C#3.0允许你将一个复杂的查询写入一个简单的表达式的特性对于LINQ来说如此重要。一个查询表达式调用了如此多的方法,每个调用都将前一个调用的结果作为一个参数。扩展方法将语法更加简单化,避免嵌套调用。Lambda表达式定义了一些操作的逻辑(比如where,orderby等)。匿名方法和对象初始化器定义了如何存储查询的结果。本地类型推断是将这些部分结合在一起的粘合剂。
本章小结
在本章中,我们重温了C#1.x和2.0中的一些概念,比如泛型,匿名方法和迭代器以及yield关键字。这些概念对于理解C#3.0的扩展是非常重要的。我们还涉及了C#3.0的一些新特性,这些特性是LINQ的基础:本地类型推断,lambda表达式,扩展方法,对象初始化器和匿名类型。
在C#3.0中更多的变化是查询表达式。我们将在第四章中对它及LINQ架构进一步进行阐述。
马上熄灯了。