在某些情况下,我们需要为一些类添加方法,对于我们自己编写的类或接口的,我们很容易实现这一点,但是对于一些第三方的类库,我们却很难做到,虽然可以采用继承等方式实现,但这些方法都需要编写更多的代码,而且不够灵活。所以无论是第三方类库,还是我们自己编写的代码,如果我们想要在不破坏原有类型的基础上,添加一些其他的方法,这时候就可以使用C#的扩展方法实现。

1. 基本使用

// Extension method allow us to extend new method for existing class or interface and we do not need to rewrite them.

public class Person
{
    public string FirstName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

public static class PersonHelper
{
    public static string GetFullName(this Person person)
    {
        return person.FirstName + person.LastName;
    }
}

...
var person = new Person("Rob", "Li");
var extensionFullName = person.GetFullName();
var staticFullName = PersonHelper.GetFullName(person);
Console.WriteLine(extensionFullName);
Console.WriteLine(staticFullName);

比如我们要实现一个获取全名的方法,只需要编写一个静态类,以及编写相应的方法就可以了。

这里需要注意的是person.GetFullName()在实际使用的时候,会被转化成普通的静态方法调用,即PersonHelper.GetFullName(person)

2. 链式调用与命名空间引入

namespace DefineMain
{
    using ExtensionMethod;
    public class MainClass
    {
        static void Main()
        {
            var name = "Name";
            var doubleName = name.GetDoubleString(); 
            Console.WriteLine(doubleName); // NameName
            var result = name.GetDoubleString().GetDoubleString();
            Console.WriteLine(result); //NameNameNameName
        }
    }
}

namespace ExtensionMethod
{
    public static class StringHelper
    {
        public static string GetDoubleString(this string s)
        {
            return s + s;
        }
    }
}

使用扩展方法时,必须导入扩展方法所在的命名空间,这时候才能使用。这和使用相应的类时,导入对应的命名空间是一样的。

此外扩展方法支持链式调用,比如name.GetDoubleString()的返回类型依旧是 string,所以依旧可以继续使用扩展方法。

3. 优先级

如果扩展方法方法名与实例的方法名一样,在参数数量一致的情况下,总是会优先调用实例的方法,即使扩展方法参数的类型比实例方法的参数类型更加具体

public class Person
{
    public string GetName() => "MyName";
    public string GetLastName(object lastName) => $"{lastName}";
}

public static class PersonHelper
{
    public static string GetName(this Person person) => "No Param";
    public static string GetName(this Person person,string param) => $"No Param {param}";
    public static string GetLastName(this Person person,string param) => $"LastName {param}";
}
...
var person = new Person();
Console.WriteLine(person.GetName()); // MyName
Console.WriteLine(person.GetName("Lucy")); // No Param Lucy
Console.WriteLine(person.GetLastName("Jack")); // Jack

但是,如果多个扩展方法方法签名一样,参数类型更加具体的方法优先级更高

public static class StringHelper
{
    public static string GetName(this string s) => $"{s} string";
}
public static class StringHelperCopy
{
    public static string GetName(this object s) => $"{s} object";
}
...
Console.WriteLine("Rob".GetName()); // Rob string

4. 总结

1. 扩展方法形式

  • 必须被定义在非嵌套、非泛型静态类的内部;

  • 方法的第一个参数为this,第一个参数的类型为要扩展的类型;

2. 注意事项

  • 使用时,必须使用using 引入扩展方法所在的命名空间;
  • 实例方法与扩展方法,方法签名,参数一致时,即使扩展方法的参数类型更加具体,也会优先调用实例的方法;
  • 扩展方法与扩展方法,方法签名,参数一致时,优先调用参数更加具体的扩展方法。

3. 理解

对于实例方法,即使方法没有参数,实际上也会有一个隐藏的参数,也就是this,即实例本身;扩展方法之所以用this加对象类型作为方法的第一次参数,实际上也是为了引入实例对象,只不过相当于是显示指定的this这个默认的参数。

5. 参考资料

  • 《C# 8.0核心技术指南》