一、序
拓展方法是C# 3.0中添加的一个新特性,虽然C# 3.0的推出已经过去很久了,但我还是想把这个拓展方法单独提出来和大伙讲讲——因为它真的真的太棒了!
虽然C#的拓展方法可能在您日常开发中很少会需要自己去写拓展方法,但您在日常的开发工作中,一定会多多少少用到一些库提供的拓展方法,因为有很多库的功能就是基于拓展方法提供的,比如说像.net自带的System.Linq命名空间下,提供的一系列关于集合查询的方法,以及Polly中Policy的定义等也会使用到拓展方法。可见拓展方法已经深深的融入到我们的开发生活中。
首先看一下微软官方对拓展方法的介绍:
扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。 扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用。
从上面的介绍可以看出一个拓展方法最重要的一个特点:不需要创建任何新的派生类,即可对指定的原始类型添加方法。
下面我会举一个简单的例子,来解释这个特性有多么的方便。
二、情景
现在我有这么一个函数GetString(),它用于从一个字符串中,提取从start字符(串)开始,到end字符(串)结束的子串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/// <summary> /// 截取字符串 /// </summary> /// <param name="text">源字符串</param> /// <param name="start">起始字符串</param> /// <param name="end">结束字符串</param> /// <param name="defaultResult">匹配失败返回</param> /// <param name="ignoreCase">忽略大小写</param> /// <returns>截取结果</returns> public static string GetString(string text, string start, string end, string defaultResult = null, bool ignoreCase = true) { if (text == null) throw new ArgumentNullException("text"); if (start == null) throw new ArgumentNullException("start"); if (end == null) throw new ArgumentNullException("end"); string result = defaultResult; string textUpper; if (ignoreCase) { textUpper = text.ToUpper(); start = start.ToUpper(); end = end.ToUpper(); } else { textUpper = text; } int p3 = start.Length; int p1 = textUpper.IndexOf(start); if (p1 != -1) { int p2 = textUpper.IndexOf(end, p1 + p3); if ((p2 != -1) && ((p2 - p1 - p3) >= 0)) { result = text.Substring(p1 + p3, p2 - p1 - p3); result = result.Trim(); } } return result; } |
在实际情景中,这个方法我经常会使用到,但每次使用的时候都必须这么去调用它:
1 |
string result = StringHelper.GetString("<p1>hello world!</p1>", "<p1>", "</p1>") |
可以看到写起来非常的麻烦,每次都要使用“类名.方法名”的方式去调用它。
不过在这里我想插一句题外话,从C# 6 起,我们可以使用using static语法简化调用:
1 2 3 4 5 6 7 |
using static Chunfeng.Common.StringHelper; static void Main(string[] commandLineArgs) { string result = GetString("<p1>hello world!</p1>", "<p1>", "</p1>"); Console.WriteLine(result); } |
虽然在上面使用了C# 6的using static语法,但我们还是要单独的去使用一个函数“GetString”,我们能不能让这个“GetString”函数成为内置的string类型的实例方法呢?
三、以往的解决方法:继承
如果我们要对一个已有的类型添加新的实例方法和属性,通常会创建一个新类,并使这个新类继承于我们想要拓展的已有类,然后再在此新类中添加新的方法,比如下面有个Animal类,我想为Animal类添加一个新的实例方法sleep,我会这么做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/// <summary> /// 动物类 /// </summary> public class Animal { /// <summary> /// 吃东西 /// </summary> public void Eat() { throw new NotImplementedException(); } } /// <summary> /// 人类 /// </summary> public class Person : Animal { /// <summary> /// 睡觉 /// </summary> public void Sleep() { throw new NotImplementedException(); } } |
可以看到,通过继承并创建新类的方式有很多的问题,随便提两个:
1.创建的新类并不等同于父类,在使用上会有限制:比如赋值给父类类型变量,就不能使用新增的方法了;
1 2 3 4 5 6 7 |
//以下代码是可以的 Person person = new Person(); person.Sleep(); //以下代码是有问题的 Animal animal = new Person(); animal.Sleep(); |
2.由于密封类不能被继承,所以此方法不能对密封类进行拓展。
四、C# 3带来的福音:拓展方法
在C# 3.0中,微软为C#带来了拓展方法这一特性。
拓展方法可以为所有的现有类型添加新的方法,并不需要去使用继承,从而绕过了密封类这一限制。
那么该怎么做呢?
非常简单,只要将需要新增的方法放到一个非嵌套、非泛型的静态类中,成作为一个静态方法,同时将需要拓展的类型作为函数的第一个参数,且此参数定义前加一个“this”就可以了。
拿我们刚刚第二章中的GetString为例,我们只需要将函数定义从
1 |
public static string GetString(string text, string start, string end, string defaultResult = null, bool ignoreCase = true) |
改为:
1 |
public static string GetString(this string text, string start, string end, string defaultResult = null, bool ignoreCase = true) |
就可以了。注意到了吗,在新的函数定义中,第一个参数的定义前多了一个this,就这么简单!
现在我们来看一下效果:
StringHelper类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
using System; namespace Chunfeng.Common { /// <summary> /// 字符串帮助类 /// <para>可以对字符串进行常见的操作</para> /// </summary> public static class StringHelper { /// <summary> /// 截取字符串 /// </summary> /// <param name="text">源字符串</param> /// <param name="start">起始字符串</param> /// <param name="end">结束字符串</param> /// <param name="defaultResult">匹配失败返回</param> /// <param name="ignoreCase">忽略大小写</param> /// <returns>截取结果</returns> public static string GetString(this string text, string start, string end, string defaultResult = null, bool ignoreCase = true) { if (text == null) throw new ArgumentNullException("text"); if (start == null) throw new ArgumentNullException("start"); if (end == null) throw new ArgumentNullException("end"); string result = defaultResult; string textUpper; if (ignoreCase) { textUpper = text.ToUpper(); start = start.ToUpper(); end = end.ToUpper(); } else { textUpper = text; } int p3 = start.Length; int p1 = textUpper.IndexOf(start); if (p1 != -1) { int p2 = textUpper.IndexOf(end, p1 + p3); if ((p2 != -1) && ((p2 - p1 - p3) >= 0)) { result = text.Substring(p1 + p3, p2 - p1 - p3); result = result.Trim(); } } return result; } } } |
调用:
1 2 3 4 5 6 7 |
using Chunfeng.Common; static void Main(string[] args) { string result = "<p1>hello world!</p1>".GetString("<p1>", "</p1>"); Console.WriteLine(result); } |
看,现在我们能在string类型后面直接点出GetString方法了!是不是非常的方便?
五、其实还是语法糖
拓展方法这么强大,内部是怎么实现的呢?
其实,这一切还是语法糖,我们用反编译工具反编译一下刚刚的程序,可以看到刚刚的那句
1 |
"<p1>hello world!</p1>".GetString("<p1>", "</p1>"); |
编译后的IL代码:
1 2 3 4 5 6 |
IL_0001: ldstr "<p1>hello world!</p1>" IL_0006: ldstr "<p1>" IL_000b: ldstr "</p1>" IL_0010: ldnull IL_0011: ldc.i4.1 IL_0012: call string Chunfeng.Common.StringHelper::GetString(string, string, string, string, bool) |
追根究底,其实就是一个静态函数调用【摊手】。
六、结语:
其实我们常用那些Linq方法,其实都是位于System.Linq命名空间中的拓展方法:
拓展方法这个特性一直默默无闻,很多人可能都不知道这个概念,但不要忘记,它们可支起了C#世界的小半边天啊。
参考资料:《扩展方法(C# 编程指南)》(https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/extension-methods)
小柊
2018年7月31日 22:26:54