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

来源:lover_p.cstc.net.cn  作者:lover_p
C#3.0语言详解之基本的语言增强
摘要:Linq项目简介和开发环境的搭建9月份,微软推出了一项名为“Linq项目”的新技术,用于在.NET语言中集成数据查询的功能。您可以从http://msdn.microsoft.com/netframework/future/得到 Linq项……

匿名类型

在很多情况下,我们需要一种能够临时将一批具有一定关联的数据存放起来的对象;或者在某些情况下,我们对仅一个对象的“形状”(如属性的名字和类型等)比较感兴趣。例如前面我们提到的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表达式和查询表达式奠定了实现基础。

【相关文章】好搜一下
使用Visual Studio 2010空白解决方案的三个理由

使用Visual Studio 2010空白

在Visual Studio 2010(包括以前的版本中)都提供了很多现成的解决…