1、什麼是擴展方法?
擴展方法,首先是一種方法,它可以用來擴展已定義類型中的方法成員。
在擴展方法誕生之前,如果想為一個已有類型自定義含有特殊邏輯的新方法時,你必須重新定義一個類型來繼承已有類型,以這種方式來添加方法。如果基類有抽象方法,則還要重新去實現這個抽象方法。
這樣,為了擴展一個方法,需要承擔更多的因繼承而產生的開銷。使用繼承來擴展現有類型總有點大材小用的感覺,並且值類型或密封類(不能被繼承的類)等也不能被繼承,不能由此獲得擴展。
於是,C#3.0提出了擴展方法。
2、擴展方法的使用
2.1 定義擴展方法
1 public static class ListExtern 2 { 3 public static int JSum(this IEnumerable<int> source) 4 { 5 if (source == null) 6 { 7 throw new ArgumentException("輸入數組為空"); 8 } 9 int jsum = 0; 10 bool flag = false; 11 12 foreach (var i in source) 13 { 14 if (!flag) 15 { 16 jsum += i; 17 flag = true; 18 } 19 else 20 { 21 flag = false; 22 } 23 } 24 return jsum; 25 } 26 }
在以上代碼中,JSum方法就是一個擴展方法,它的功能是計算數組中小標為奇數的數組成員之和。並不是所有的方法都可以用作擴展方法。下列是符合擴展方法的定義規則:
(1)擴展方法必須在一個非嵌套、非泛型的靜態類中定義;
(2)它至少要有一個參數;
(3)第一個參數必須加上this關鍵字作為前綴(第一個參數類型也稱為擴展類型,即指方法對這個類型進行擴展);
(4)第一個參數不能使用任何其他的修飾符(如不能使用ref、out等修飾符);
(5)第一個參數的類型不能是指針類型。
這些規則都是硬性規定,無論方法違反了哪一條,編譯器都可能會報錯,或認為它不是一個擴展方法。
2.2 調用擴展方法
成功定義了一個擴展方法後,接下來就該去調用它。
1 static void Main(string[] args) 2 { 3 List<int> source=new List<int>() {1,2,3,4,5,6,3}; 4 int jsum = source.JSum(); 5 Console.WriteLine("數組的奇數和為:"+jsum); 6 Console.ReadKey(); 7 }
成功調用,說明了擴展方法調用的獨特性,即這裡可以直接通過List<int>類型來調用擴展方法。
3、編譯器如何發現擴展方法
對於C# 3.0編譯器而言,當它看到某個類型的變量在調用方法時,它會首先去該對象的實例方法中進行查找,如果沒有找到與調用方法同名並參數一致的實例方法,編譯器就回去查找存在合適的擴展方法。
編譯器會檢查所有導入的命名控件和當前命名控件中的擴展方法,並將變量類型匹配到擴展類型,這裡存在一個隱式轉換的擴展方法。如在前面代碼中,從List<T>到我們擴展的類型IEnumerable<int>就存在一個隱式轉換。
從編譯器發現擴展方法的過程來看,方法調用的優先級順序應為:類型實例方法-當前命名空間下的擴展方法-導入命名控件的擴展方法。下面就用代碼來演示一下編譯器發現方法的過程:
1 namespace 擴展方法2 2 { 3 using 擴展方法3; 4 class Program 5 { 6 static void Main(string[] args) 7 { 8 Person p = new Person() {Name = "哈哈"}; 9 p.Print(); 10 p.Print("Hello"); 11 } 12 } 13 14 public class Person 15 { 16 public string Name { get; set; } 17 } 18 19 public static class Extensionclass 20 { 21 public static void Print(this Person per) 22 { 23 Console.WriteLine($"調用的是當前命名空間下的擴展方法輸出,姓名為:{per.Name}"); 24 } 25 } 26 } 27 28 namespace 擴展方法3 29 { 30 using 擴展方法2; 31 32 public static class CustomExtensionClass 33 { 34 public static void Print(this Person per) 35 { 36 Console.WriteLine($"調用的是CustomNamaspace命名空間下的擴展方法暑促:姓名為:{per.Name}"); 37 } 38 39 public static void Print(this Person per,string s) 40 { 41 Console.WriteLine($"調用的是CustomNamaspace命名空間下的擴展方法暑促:姓名為:{per.Name},附加字符串{s}"); 42 } 43 } 44 45 }
在以上代碼中,存在兩個不同的命名控件,她們都定義了帶一個參數的擴展方法Print。根據前面對編譯器調用方法的優先級的分析,編譯器首先查看Person類型中是否定義了無參的Print實例方法。如果有,則停止查找;否則繼續查找當前命名空間下,即CurrentNamespace下是否定義了帶一個參數的擴展方法Print。
注意:(1)如果擴展的類型中定義了無參數的Print的實例方法,則在p後面鍵入“.”運算符時,VS的智能提示將不會給出擴展方法。
(2)如果同一個命名空間下的兩個類中含有擴展類型相同的方法,編譯器便不知道該調用哪個方法了,就會出現編譯錯誤。
4、空引用也可調用擴展方法
4.1 拿例子說話
1 namespace 擴展方法3 2 { 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 Console.WriteLine("空引用上調用擴展方法演示:"); 8 string s = null; 9 Console.WriteLine($"字符串S為空字符串:{s.IsNull()}"); 10 Console.ReadKey(); 11 } 12 } 13 14 public static class NullExtern 15 { 16 public static bool IsNull(this object obj) 17 { 18 return obj == null; 19 } 20 } 21 }
以上的代碼沒有報異常,可以正常運行。不過在上面的代碼中,代碼擴展了object類型,所有繼承於object的類型都將具有該擴展方法,這就對其他子類型產生了“污染”。
更好的實現方式應該是:
1 public static bool isNull(this string str) 2 { 3 return str==null; 4 }
所以當我們為某一個類型定義擴展方法時,應盡量擴展具體的類型,而不要擴展其基類。在空引用上調用擴展方法之所以不會出現NullReferenceException異常,是因為對於編譯器而言,這個過程只是把空引用"S"當成參數傳入靜態方法而已,即s.IsNull的調用等效於下面代碼:Console.WriteLine($"字符串s為空字符串{NullExten.IsNull(s)}");這並不是真正地在空引用上調用方法,所以也就不存在異常的問題。