使类的扩展更简单——扩展方法
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)}");这并不是真正地在空引用上调用方法,所以也就不存在异常的问题。