C# 3.0语言详解之基本的语言增强

Linq项目简介和开发环境的搭建

  9月份,微软推出了一项名为“Linq项目”的新技术,用于在.NET语言中集成数据查询的功能。您可以从http://msdn.microsoft.com/netframework/future/得到Linq项目的技术预览版,其中包括大量的介绍文章(英文)和C# 3.0以及VB 9.0的编译器。

  Linq项目英文全称为Language Integrated Query,即“语言集成查询”。Linq基于.NET Framework 2.0,通过对语言的改进,实现在直接在语言中通过类似SQL语句的构造对数据源进行查询,可查询的数据源从基本的顺序结构(如数组和链表)延伸到关系型数据库(目前是SQL Server,相信将来可以扩展到几乎所有的关系型数据库)和XML。

  C# 3.0是C#语言再一次升级后的版本,率先实现了Linq的概念;同样实现了Linq的还有VB 9.0。从上面提到的网址,读者可以找到两种语言的编译器的技术预览版。在本文中我们将以C# 3.0为中心讨论其开发环境的搭建和语言改进。下载得到的是一个名为“linq preview.msi”(也可能不同)的安装包,双击之后可以和其他软件一样进行安装,此处不再赘述。该安装包为Visual Studio 2005 Beta 2及以后的版本和Visual C# 2005 Beta 2提供了插件(项目模板)和C#编译器,该编译器生成的IL代码可以直接在.NET Framework 2.0上运行。在安装了该预览版后,在新建项目对话框中项目类型列表的Visual C#节点下,我们可以看到一个Linq Preview的条目,并可以在右边的项目模板中选择一些桌面项目模板(Linq暂时还不支持Web项目),如下图所示:


图1 - 安装完Linq的Visual Studio 2005 新建项目对话框


  只要选中了Linq中的项目模板,我们就可以像编写其他应用程序一样在Visual Studio 2005中开始编写C# 3.0应用程序了,在编译的时候IDE会自动为我们选择C# 3.0的编译器。

  现在我们已经可以开始编写C# 3.0应用程序了,后面的章节中我就将为大家讲解C# 3.0所带来的语言增强。值得说明的一点是,本文是一个系列文章中的一篇,这一系列文章共三个部分。本文为第一部分,讲述C# 3.0种基本的语言增强,这些语言增强其实是其他两个部分的基础;第二部分将讲述C# 3.0中的Lambda表达式,这是匿名方法的自然进化形式,它不仅能将表达式体现为可执行的方法(委托),也能将表达式体现为可以在运行时进行操作的数据结构——表达式树;最后一部分讲述的才是Linq项目中最核心的也是最激动人心的内容——查询表达式,这是Linq在C#中的实现形式。同时,Linq还针对SQL查询和XML查询对.NET Framework基础类库进行了扩充,分别称作DLinq和XLinq,这些内容我将在其他系列文章中为大家讲述。

  在本文的讲述过程中,限于篇幅,仅提供了简短的代码段而不是完整的代码。不过这些代码段是从完整的、可正确编译和运行的代码中抽取出来的,这些完整代码可以从这里下载,并且在本文的第5部分中有关于完整代码的介绍。

  好了,废话太多了,让我们赶快进入C# 3.0的精彩世界。

  具有隐式类型的声明

  在一个带有初始化器的声明语句中,待声明的变量的类型是显而易见的——和初始化表达式的结果类型一致。对于这种情况,在C# 3.0中可以使用新增的关键字var来代替声明中的类型,而由编译器根据初始化表达式来推断出变量的类型。

var i = 5; // int
var d = 9.0; // double
var s = "Hello"; // string
var a = new int[] { 1, 2, 3, 4, 5 }; // int[]

Console.WriteLine("Type of variable <i>: {0}", i.GetType());
Console.WriteLine("Type of variable <d>: {0}", d.GetType());
Console.WriteLine("Type of variable <s>: {0}", s.GetType());
Console.WriteLine("Type of variable <a>: {0}", a.GetType());


  上面的代码在C# 3.0中是符合语法规则的,前四行代码就是使用了隐式类型的声明;而后四行代码用于在运行时验证各个变量是否拥有正确的类型。如果在Visual Studio 2005中运行这段代码(为了能够看到结果,请使用Ctrl+F5来编译并启动程序),会得到下面的结果:

Type of variable <i>: System.Int32
Type of variable <d>: System.Double
Type of variable <s>: System.String
Type of variable <a>: System.Int32[]


  这表明,编译器在编译的时候,已经正确地推断了每个变量的类型,并将其嵌入到了程序集的元数据中。

  这里有两个限制,一是具有隐式类型的声明只能作用在局部变量上,二是这种声明必须有初始化器(即等号和后面的表达式)。如果我们企图在一个类中声明一个具有隐式类型的域,就会出现一个编译错误:Invalid token ’var’ in class, struct, or interface member declaration;而如果声明中没有出现初始化器,则会导致另外一个编译错误:’=’ expected。

  除了局部变量外,作用域为一个块的数组也可以运用具有隐式类型的声明,例如:

var ia = new [] { 1, 2, 3, 4, 5 }; // int[]
var da = new [] { 1.1, 2, 3, 4, 5 }; // double[]
var sa = new [] { "Hello", "World" }; // string[]

Console.WriteLine("Type of array <ia>: {0}", ia.GetType());
Console.WriteLine("Type of array <da>: {0}", da.GetType());
Console.WriteLine("Type of array <sa>: {0}", sa.GetType());


  在上面例子的数组声明中,在运算符new和表示数组声明的一对方括号之间省略了类型名字,但这在C# 3.0中仍然是符合语法规则的。编译器会通过成员列表中的值的类型来推断数组的类型。编译并运行上面的例子,会得到如下的输出:

Type of array <ia>: System.Int32[]
Type of array <da>: System.Double[]
Type of array <sa>: System.String[]


  除了和具有隐式类型的局部变量具有相同的约束外,具有隐式类型的数组还有必须尊从这样一个规则,即成员列表中的所有值必须是兼容的。也就是说,成员列表中必须存在这样一个值,使得其他值可以隐式地转换为该值的类型。因此,下面的声明是不符合语法规则的:

var aa = new [] { 1, "Hello", 2.0, "World" };


  如果试图编译上面的代码,会得到一个编译错误:No array type can be inferred from the initializers。这是因为编译器无法根据成员列表中的值来推断数组的类型。

  实际上,尽管具有隐式类型的声明使得传统声明的编写方法更加便利,然而引入这种声明方式的真正目的并不在于此,而是为了使局部变量和数组能够访问这样一个新的语言构造:匿名类型。

  对象和集合初始化器

  1、对象初始化器

  在C# 3.0出现之前,如果想在构造一个对象的时候对其成员进行初始化,只有通过调用带参数的构造器来完成。如果一个类型仅提供了无参构造器(默认构造器)或构造器接受的参数和对象的公共属性没有太大关系,对对象的初始化就只能通过两个步骤来完成:首先是声明一个对象,然后对其公共属性进行赋值。

  C# 3.0引入了对象初始化器的语言构造,可以在声明对象的时候对其公共可读写属性进行赋值。例如我们有这样一个类型:

class Book
{
private string m_name;
private string m_isbn;
private float m_price;

public string Name { get { return m_name; } set { m_name = value; } }
public string Isbn { get { return m_isbn; } set { m_isbn = value; } }
public float Price { get { return m_price; } set { m_price = value; } }
}


  那么,我们就可以用下面的简单方式来初始化Book类的对象:

var firstBook = new Book
{
Name = "The First Sample Book",
Isbn = "1-111-11111-1",
Price = 88.0f
};
var secondBook = new Book
{
Name = "The Second Sample Book",
Price = 25.0f,
Isbn = "2-222-22222-2"
};


  也就是说,只要在对象声明语句的末尾添加一对花括号,并在其中对每个属性进行赋值即可。并且我们可以看到,第二个声明中对属性的赋值语句的顺序与Book类中属性定义的顺序并不相同,但这完全是符合语法规则的,只要明确指定了要赋值的属性名字即可。另外,也可以只对一部的属性进行赋值,如只对Isbn进行赋值,而在稍后通过查询等手段为Name和Price属性赋值。甚至可以保留一个空的属性赋值列表,这时每个属性都被赋以相应类型的默认值。

  实际上,对象初始化器完成的工作还是我们以前要完成的那两步,只不过由编译器代劳了,例如上面对firstBook的声明将被编译器翻译为形如下面的代码,之后才进行进一步的编译:

var firstBook = new Book;
firstBook.Name = "The First Sample Book";
firstBook.Isbn = "1-111-11111-1";
firstBook.Price = 88.0f;


  2、集合初始化器

  我们知道,在C# 1.x和C# 2.0中,可以通过一个包含在花括号之间的值列表来初始化一个刚刚声明的数组,如:

int[] intArray = new int[] { 1, 2, 3};


  然而,对于和数组有着类似功能的其他集合(如List)就不能享受这种方便的语法了,只能在声明完集合对象后将值一个一个地添加到集合中。而C# 3.0提出了集合初始化器的语言构造,可以用初始化数组的便捷语法来初始化任何一个实现了ICollection<T>接口的对象。这里以C# 2.0中引入的著名的泛型类型List<T>为例:

var intList = new List<int> { 1, 2, 3, 4, 5 };

Console.WriteLine("A list of int:");
foreach(var i in intList)
Console.WriteLine("{0} ", i);


  运行上面这段代码,可以得到下面的结果:

A list of int:
1
2
3
4
5


  前面提到了,这种语法可以用来初始化任何实现了ICollection<T>接口的对象,这实际上是编译器的功劳。编译器针对这种语法自动生成了循环调用ICollection.Add方法的代码,将列表中的值一个一个地添加到正在声明的集合中。例如:

var intList = new List<int>();
intList.Add(1);
intList.Add(2);
intList.Add(3);
intList.Add(4);
intList.Add(5);


  另外,也必须是实现了ICollection<T>的对象才能用这种语法进行初始化,其他具备Add方法但未实现ICollection<T>的对象不能这样初始化;同时在声明中还必须明确指定类型参数T所代表的实际类型。

  匿名类型

  在很多情况下,我们需要一种能够临时将一批具有一定关联的数据存放起来的对象;或者在某些情况下,我们对仅一个对象的“形状”(如属性的名字和类型等)比较感兴趣。例如前面我们提到的Book,当它和其他商品放在一起进行查询时,我们可能仅对其名称和价格感兴趣,并且希望将这两种属性放在另外一个单独的临时对象中以备今后使用。这时,我们关注的仅仅是这个临时对象具有Name和Price的属性感兴趣,至于它究竟是什么类型就无关紧要了。然而,为了使这样一个对象得以存在,我们不得不为这个无关紧要的类型写上一大堆“样本代码”,无非就是定义一个如BookAsGood的类,其中无非也就是形如m_name和m_price的私有域和名为Name与Price的公共可读写方法。

  而在C# 3.0中,我们无须为这些无关紧要的类型浪费时间。通过使用“匿名类型”,只要在需要一个这样的对象时使用没有类型名字的new表达式,并用前面提到的对象初始化器进行初始化即可。如:

var b1 = new { Name = "The First Sample Book", Price = 88.0f };
var b2 = new { Price = 25.0f, Name = "The Second Sample Book" };
var b3 = new { Name = "The Third Sample Book", Price = 35.00f };

Console.WriteLine(b1.GetType());
Console.WriteLine(b2.GetType());
Console.WriteLine(b3.GetType());


  首先,前面三行声明并初始化了三个具有匿名类型的对象,它们都将具有公共可读写属性Name和Price。我们可以看到,匿名类型的属性连类型都省掉了,完全是由编译器根据相应属性的初始化表达式推断出来的。这三行称作“匿名类型对象初始化器”,编译器在遇到这样的语句时,首先会创建一个具有内部名称的类型(所谓的“匿名”只是源代码层面上的匿名,在最终编译得到的元数据中还是会有这样一个名字的),这个类型拥有两个可读写属性,同时有两个私有域用来存放属性值;然后,和对待对象初始化器一样,编译器产生对象声明代码,并依次为每个属性赋值。

  上面代码的最后三行用来检验匿名类型在运行时的类型,如果尝试编译并运行上述代码,会得到类似下面的输出:

lover_P.CSharp3Samples.Ex03.Program+<Projection>f__0
lover_P.CSharp3Samples.Ex03.Program+<Projection>f__1
lover_P.CSharp3Samples.Ex03.Program+<Projection>f__0


  这表明编译器的确为匿名类型对象创建了实际的类型,并且该类型在代码中是不可访问的,因为类型的名字不符合C#语言命名规则(其中出现了+、<、>等非法字符)。

  另外,我们还发现一个有趣的现象,由于b1和b2在初始化的时候其属性的顺序和推断出来的类型完全一致,它们的运行时类型也是一样的;而b2因为属性出现的顺序不同于另外两个对象,因此具有不同的运行时类型。通过下面的代码,我们可以验证这一事实:

// 正确的赋值,b1和b3具有相同的类型
b1 = b3;

// 错误的赋值,b1和b2的类型不同
b1 = b2;


  如果尝试编译这段代码,对于第二个赋值我们会得到一条编译错误:Cannot implicitly convert type ’lover_P.CSharp3Samples.Ex03.Program.<Projection>f__1’ to ’lover_P.CSharp3Samples.Ex03.Program.<Projection>f__0’。

  这实际上是C# 3.0编译器固有的特性,在同一个程序集中,编译器将为属性出现顺序和类型完全相同的匿名类型对象生成唯一的一个类型。而一旦属性的出现顺序或类型有所不同,编译器就会生成不同的类型。另外,在两个程序集之中,即使属性出现的顺序和类型一致,编译器也可能会生成不同的类型,因此具有匿名类型的对象是不能跨程序集访问的。

  扩展方法

  扩展方法是一种特殊的静态方法,它定义在一个静态类中,但可以在其他类的对象上像调用实例方法那样进行调用。因此,通过扩展方法,我们就可以在不修改一个类型的前提下对一个类型进行功能上的扩充;同时,也可以将一些近似的类型中近似的功能同一实现在一个类中,便于阅读和维护。

  另外,扩展方法的引入并非只是简单地为了扩展现有类型,扩展方法的使用还是有一定限制的(这将在稍后谈到)。扩展方法更大的意义在于它为以后将要介绍的查询表达式、查询表达式模式和标准查询运算符的实现奠定了基础,而这些实现正是Linq项目的核心所在。

  1、扩展方法的定义和调用

  扩展方法和一般静态方法的定义方法类似,唯一的区别是在第一个参数的前面要加上关键字this作为修饰符;同时,第一个参数的类型也决定了扩展方法可以扩展的类型。

  为了介绍扩展方法的定义和使用方法,首先我们定义下面这样一个简单的类作为被扩展对象:

class SampleClass
{
int m_val = 10;

public int Val { get { return m_val; } set { m_val = value; } }

public void Func()
{
Console.WriteLine("Hey! I’m myself, and my value is {0}.", m_val);
}
}


  这个类拥有一个公共可读写属性Val,并有一个私有域m_val用于存放这个属性的值。另外,这个类自身还拥有一个公共方法Func,用来在屏幕上显示以行信息,说明该方法被调用了。

  然后,我们定义一个静态类型SampleExtensions(这个名字是随意的,只有将扩展方法作为普通的静态方法进行调用时才会用到这个名字),其中定义一个用于扩充SampleClass类型的扩展方法ExFunc:

static class SampleExtensions
{
public static void ExFunc(this SampleClass s)
{
Console.WriteLine("Aha! I’m going to modify the SampleClass!");
s.Val = 20;
s.Func();
}
}


  注意这个方法的第一个参数(也是仅有的一个参数)的类型前面多了一个修饰符this,这表明该方法用来扩展SampleClass类型,也就是说可以在SampleClass类型的对象上像调用实例方法那样调用ExFunc方法。该方法首先告诉用户它正在被调用,然后修改SampleClass类型的对象的属性,并调用它的实例方法。

  接下来,我们在Main方法中创建SampleClass类型的一个实例,并尝试调用其实例方法和上面定义的扩展方法:

SampleClass s = new SampleClass();

Console.WriteLine("Calling the instance method:");
s.Func();
Console.WriteLine();

Console.WriteLine("Calling the extension method:");
s.ExFunc();


  我们可以看到,对ExFunc的调用形式和对Func方法完全一样,然而从上面的类型定义可以明确地知道,Func是定义在SampleClass类型中的实例方法而ExFunc则是定义在SampleExtensions类型中的扩展(静态)方法。尝试编译和运行上面的代码,可以得到下面的结果:

Calling the instance method:
Hey! I’m myself, and my value is 10.

Calling the extension method:
Aha! I’m going to modify the SampleClass!
Hey! I’m myself, and my value is 20.


  当然,由于扩展方法只是静态方法的一种特例,我们同样可以像用调用一般静态方法那样来调用扩展方法:

SampleExtensions.ExFunc(s);


  这会得到完全一样的结果。而且事实上,编译器也正是将扩展方法的调用翻译为了一般形式的静态方法调用,然后才进行进一步的编译。

  扩展方法不仅能扩展同一个程序集中的类型,同时也能扩展不同程序集甚至是已经发布了的程序集中的类型。下面我们就在SampleExtensions中再添加一个扩展方法,用来扩展.NET Framework的内建类型String(这个例子摘录自C# 3.0语言规范,版权归微软公司所有。):

public static int ToInt32(this string s)
{
return Int32.Parse(s);
}


  然后,我们就可以象下面这样方便地将一个字符串转换为一个整型了:

string sval = "20";
Console.WriteLine("String ’20’ means integer: {0}.", sval.ToInt32());


  尝试运行这段代码,会得到如下结果:

String ’20’ means integer: 20.


  简单地浏览一下.NET Framework的文档就会发现,System.String类型中的确没有定义ToInt32方法,这说明我们的扩展方法在.NET Framework内建类型上仍然有效。

  2、扩展方法的导入和权限

  前面我们探讨了如何在同一个程序集中定义和调用扩展方法,那么如果一个扩展方法是定义在其他程序集中,我们又如何享用这些扩展方法所带来的功能呢?事实非常简单,C# 3.0语言规范中规定,当我们使用using语句导入一个名命名空间时,就会同时导入该命名空间中所有静态类型中定义的所有匿名方法。

  3、重载抉择问题

  看了上面的介绍我们不难发现一个问题:如果一个类型中的某个实例方法与扩展方法的签名等价(这里说“等价”是因为扩展方法与调用形式一样的实例方法相比,要多一个表示被扩展类型的参数,也就是第一个有this修饰符的参数),那么当在被扩展类型的对象上调用方法时,就会产生冲突。我们将这种冲突称为重载抉择问题。C# 3.0语言规范扩展了重载抉择,将对扩展方法的调用也纳入到重载抉择的范畴之内,并且规定扩展方法拥有最低的优先级。也就是说,对于一组特性类型、特定顺序的参数列表,只有当被扩展类型中没有得以匹配的方法时,才考虑从扩展方法中选择一个最合适的方法进行调用。

  现在,我们为上面的SampleExtensions类再添加一个用于扩展SampleClass类型的扩展方法Func:

public static void Func(this SampleClass s)
{
s.Val = -1;
Console.WriteLine("Am I appearing?");
}


  如果用调用实例方法的语法调用这个扩展方法,则其调用形式与调用无参的实例方法Func完全一致。再次编译并运行原来的程序,输出的结果并没有改变,也就是说这个扩展方法根本没有被调用,实际被调用的方法是实例方法Func。当然,如果将这个扩展方法作为普通的静态方法进行调用是没有问题的。

  另外如果两个静态类中为同一个类型定义了签名一致的静态方法,则最后定义的静态方法具有较高的优先级;而同一程序集中定义的静态方法优先级高于用using语句从其他命名空间中导入的扩展方法;最后,如果两个命名空间中包含签名一致的扩展方法,则最后引入的命名空间中的扩展方法优先级较高。

  示例代码简介和小结

  本文的代码包括一个解决方案”CSharp3Sample1“,其中包括4个项目Ex01~Ex04,分别对应于第1~4小节中的示例。您可以从这里下载本文的源代码。如果需要运行某一个示例,请在Visual Studio 2005的Solution Explorer窗口中右击对应的项目,并选择”Set as Startup Project“菜单项;然后按Ctrl+F5键运行示例,这里建议按Ctrl+F5而不是只按F5键来运行示例,是因为这样能够在运行结束后暂停,方便观察结果(由于个人原因我不愿意在代码中加入类似Console.ReadLine这样的代码来暂停程序的运行)。 另外请注意要运行这些代码需要首先正确安装Visual Studio 2005 Beta2和Linq Preview。

  本文通过一系列可以执行并能够看到结果的简单代码介绍了C# 3.0中基本的语言增强——具有隐式类型的对象和数组声明、对象和集合初始化器、匿名类型和扩展方法。与C# 2.0之于C# 1.x不同,C# 3.0的这些语言增强不仅仅是为了是语言变得更加强大和优雅,更重要的是为了后面的Lambda表达式和查询表达式奠定了实现基础。

posted on 2005-11-04 11:51  井泉  阅读(384)  评论(0编辑  收藏  举报

导航