在以往的編程模式裡,一旦一個類型(可以是類、接口、結構、枚舉或者委托等等)定義完畢並編譯完成後,它就基本上被確定了。唯一的修改方法就是打開代碼,更新之後重新編譯。當然現在有些很好很強大的方法比如使用反射(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
{
{
// 該方法允許任意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的代碼,我們甚至連它的內部結構都不知道,但仍然可以向它們的數據類型“注入”我們自己想要的函數。