C# 3.0 之 扩展方法

2009年3月22日星期日

C# 3.0 之 扩展方法

在以往的编程模式里,一旦一个类型(可以是类、接口、结构、枚举或者委托等等)定义完毕并编译完成后,它就基本上被确定了。唯一的修改方法就是打开代码,更新之后重新编译。当然现在有些很好很强大的方法比如使用反射(System.Reflection.Emit),但C# 3.0有一种叫做“扩展方法”(extension method)的功能。即保持现有Type原封不动的情况下对其进行扩展,用户可以在对Type的定义不做任何变动的情况下,为之添加所需的方法成员。

通过这种方法,我们可以将新功能“注入”到已有的程序集中,而我们并不需要拥有那个程序集的源代码,也可以用来实现在不修改原始类型声明的条件下,强迫一个类型来支持一组成员(在多态性中很有用)。

定义扩展方法的第一个限制,就是他们必须被定义在静态类中,并且每个扩展方法也必须被声明为静态的;第二个限制,所有的扩展方法的第一个参数都使用this关键字;第三个限制,无论是直接访问内存中的实例还是静态地通过静态类的定义,都可以调用任意一个扩展方法。

呃,其实我看到这里也有些晕,还是用实例来说话吧 :-)

定义扩展方法

下面是一个叫做MyExtensions的类,定义了两个扩展方法。第一个方法使用了System.Reflection命名空间,可以使得.NET基类库的所有对象有一个DisplayDefiningAssembly(),第二个方法是个翻转int型数据的函数,比如说可以把124变成421。

static class MyExtensions

{

public static void DisplayDefiningAssembly(this object obj)

{

// 该方法允许任意object显示它被定义的程序集

Console.WriteLine("{0} lives here:\n\t->{1}\n", obj.GetType().Name, Assembly.GetAssembly(obj.GetType()));

}



public static int ReverseDigits(this int i)

{

char[] digits = i.ToString().ToCharArray(); // 先转换为string,再存到数组中



Array.Reverse(digits); // 逆序排列

string newDigits = new string(digits);



return int.Parse(newDigits); // 以int类型返回

}



public static void Foo(this int i)

{

//Int32 类型拥有的Foo()方法

Console.WriteLine("{0} rings the bell.", i);

}



public static void Foo(this int i, string message)

{

//重载的 Foo() 方法

Console.WriteLine("{0} rings the bell and said: {1}", i, message);

}

}




可以看到所有方法的第一个参数具有一个this关键字。第一个方扩展的是System.Object,而第二个方法则只能扩展整型,如果一个非整型试图调用这个方法,将会得到一个编译时错误。一个扩展方法可以拥有多个参数,但只有第一个需要加this,这在后面被重载的方法里可以看到。



另外一个要注意的问题是,带有扩展方法的类不能嵌套在另一个类中,它必须是顶层类。



从实例级别调用扩展方法

好,现在有了这两个扩展方法,我们来看一看任意一个对象(当然,必须是在.NET基类库中的)是怎么执行DisplayDefiningAssembly()的,而一个System.Int32类型是如何执行ReverseDigits()和Foo()的。

static void Main(string[] args)

{

// int 类型被指定了一个新方法

int myInt = 12345678;

myInt.DisplayDefiningAssembly();



// DataSet也是

System.Data.DataSet ds = new System.Data.DataSet();

ds.DisplayDefiningAssembly();



// SoundPlayer也一样

System.Media.SoundPlayer sp = new System.Media.SoundPlayer();

sp.DisplayDefiningAssembly();



// 为 int 类型新加的功能

Console.WriteLine("Value of myInt: {0}", myInt);

Console.WriteLine("After reversed: {0}", myInt.ReverseDigits());



// 测试重载的扩展方法

myInt.Foo();

myInt.Foo("This is the sample provided by SpadeQ!");



// 下面这个就不行了

bool b = true;

// b.Foo();



Console.ReadLine();

}


将上面这个Main方法添加到上面的类中,作为程序的入口方法,然后执行,可以看到下面的结果:






静态地调用扩展方法

回想一下,所有扩展方法的第一个参数都被添加了一个this关键字,如果我们想一想在这背后发生了些什么(可以使用ildasm.exe或者Lutz Roeder's Reflector),我们将看到编译器简单地调用了normal静态方法,将调用这些方法的变量当作一个参数来传送。将上面Main方法中的语句替换如下:

static void Main(string[] args)

{

int myInt = 12345678;

MyExtensions.DisplayDefiningAssembly(myInt);



System.Data.DataSet ds = new System.Data.DataSet();

MyExtensions.DisplayDefiningAssembly(ds);



System.Media.SoundPlayer sp = new System.Media.SoundPlayer();

MyExtensions.DisplayDefiningAssembly(sp);



Console.WriteLine("Value of myInt: {0}", myInt);

Console.WriteLine("After reversed: {0}", MyExtensions.ReverseDigits(myInt));



MyExtensions.Foo(myInt);

MyExtensions.Foo(myInt, "This is the sample provided by SpadeQ!");



Console.ReadLine();

}

运行结果仍然是一样的。也就是说,从某个对象调用扩展方法,看起来像是实例级别的调用,其实是编译器在忽悠我们而已~~~


生成以及使用扩展库



最后一个有关扩展方法的话题就是建立扩展库。一切有用的东西都可以封装到库里以供复用,这是现代编程的王道。为了演示这个过程,重新建立一个.NET Library工程,然后将代码转移进去,看起来应该如下所示:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Reflection;



namespace MyExtensionLibrary

{

public static class MyExtensions

{

public static void DisplayDefiningAssembly(this object obj)

{

// 该方法允许任意object显示它被定义的程序集

Console.WriteLine("{0} lives here:\n\t->{1}\n", obj.GetType().Name, Assembly.GetAssembly(obj.GetType()));

}



public static int ReverseDigits(this int i)

{

char[] digits = i.ToString().ToCharArray(); // 先转换为string,再存到数组中



Array.Reverse(digits); // 逆序排列

string newDigits = new string(digits);



return int.Parse(newDigits); // 以int类型返回

}

}

}

注意,如果想在库外调用这些方法,必须在类名前面加上public,因为C#默认的访问级别是private。在这里,还可以显示指明扩展应用,即在public static ...前面加上一个[Extension],当然现在并不必须这么做。

编译完成之后回到我们原来的那个工程,添加对这个新编译出来的MyExtensionLibrary.dll的引用,修改代码如下:

using System;

using MyExtensionLibrary;



namespace ConsoleApplication1

{

class Program

{

static void Main(string[] args)

{

int myInt = 12345678;

myInt.DisplayDefiningAssembly();



System.Data.DataSet ds = new System.Data.DataSet();

ds.DisplayDefiningAssembly();



System.Media.SoundPlayer sp = new System.Media.SoundPlayer();

sp.DisplayDefiningAssembly();



Console.WriteLine("Value of myInt: {0}", myInt);

Console.WriteLine("After reversed: {0}", myInt.ReverseDigits());



myInt.Foo();

myInt.Foo("This sample is provided by SpadeQ");



Console.ReadLine();

}

}

}



可以得到同样的结果,就像使用普通的方法一样。所有object类型都已经被加上了DisplayDefiningAssembly()扩展,所有的int类型还被加上了ReverseDigits()扩展,前提是我们无需分拆出object和int的代码,我们甚至连它的内部结构都不知道,但仍然可以向它们的数据类型“注入”我们自己想要的函数。

0 评论:

发表评论