Keep on going never give up.

Let's Go

C# 学习笔记(59)扩展方法

C#Lonely2019-10-04 14:51:4370次2条

什么是扩展方法?

扩展方法,首先是一种方法,它可以用来扩展已定义类型中的方法成员。


扩展方法的定义规则

1、扩展方法必须在一个非嵌套、非泛型的静态类中定义;

2、扩展方法至少要有一个参数;

3、扩展方法第一个参数必须加上this关键字作为前缀(第一个参数类型也称为扩展类型,即方法对这个类型进行扩展);

4、扩展方法第一个参数不能使用任何其他的修饰符(如不能使用ref、out等修饰符);

5、扩展方法第一个参数的类型不能是指针类型;

这些规则都是硬性规定,无论违反了哪一条,编译器都可能报错,或认为它不是一个扩展方法。


下面简单演示一下扩展方法的定义,代码示例如下:

//扩展方法必须在非泛型静态类中定义
public static class Extensions
{
    /// <summary>
    /// 获取格式化字符串,带时分秒,格式:"yyyy-MM-dd HH:mm:ss"
    /// </summary>
    /// <param name="dateTime">日期</param>
    /// <param name="isRemoveSecond">是否移除秒</param>
    public static string ToDateTimeString(this DateTime dateTime, bool isRemoveSecond = false)
    {
        if (isRemoveSecond)
            return dateTime.ToString("yyyy-MM-dd HH:mm");
        return dateTime.ToString("yyyy-MM-dd HH:mm:ss");
    }

    /// <summary>
    /// 获取格式化字符串,不带时分秒,格式:"yyyy-MM-dd"
    /// </summary>
    /// <param name="dateTime">日期</param>
    public static string ToDateString(this DateTime dateTime)
    {
        return dateTime.ToString("yyyy-MM-dd");
    }

    /// <summary>
    /// 获取格式化字符串,不带时分秒,格式:"yyyy-MM-dd"
    /// </summary>
    /// <param name="dateTime">日期</param>
    public static string ToDateString(this DateTime? dateTime)
    {
        if (dateTime == null)
            return string.Empty;
        return ToDateString(dateTime.Value);
    }
}

成功定义了扩展方法后,接下来就该去调用它了,具体演示代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime dt_1 = DateTime.Now;
            DateTime? dt_2 = DateTime.Now;
            DateTime? dt_3 = null;

            //以下代码中,调用了我们定义的扩展方法,从调用过程来看扩展方法的调用与普通方法没什么不同。
            string strDt_1 = Extensions.ToDateTimeString(dt_1);
            string strDt_2 = Extensions.ToDateString(dt_2);          
            string strDt_3 = Extensions.ToDateString(dt_3);

            Console.WriteLine(strDt_1);
            Console.WriteLine(strDt_2);
            Console.WriteLine(strDt_3);

            //然而,扩展方法还有另外一种调用方式,代码如下:
            string strDt_4 = dt_1.ToDateTimeString();
            string strDt_5 = dt_2.ToDateString();
            string strDt_6 = dt_3.ToDateString();

            Console.WriteLine(strDt_4);
            Console.WriteLine(strDt_5);
            Console.WriteLine(strDt_6);

            Console.ReadKey();
        }
    }
}

从以上代码中,可以看出扩展方法调用的独特性,即可以直接通过DateTime类型来调用扩展方法。大家可能会认为我的代码写错了,因为在DateTime类型中根本没有定义这些实例方法,然而事实确是,上面的调用方式完全正确,运行结果如下:

image.png


编译器如何发现扩展方法

刚接触扩展方法的朋友肯定会对以上代码的调用有所疑惑。而对于C# 3.0编译器而言,当它看到某个类型的变量在调用方法时,它会首先去该对象的实例方法中进行查找,如果没有找到与调用方法同名并参数一致的实例方法,编译器就会去查找是否存在合适的扩展方法。编译器会检查所有导入的命名空间和当前命名空间中的扩展方法,并将变量类型匹配到扩展类型。

你可以通过前面介绍的扩展方法规则和编译器的智能提示来识别扩展方法,在VS中,扩展方法前面都有一个向下的箭头标识,如下所示:

image.png

但是这只是人们识别扩展方法的方式,编译器则会根据System.Runtime.CompilerServices.ExtensionAttribute属性来识别扩展方法。如果我们定义的方法是扩展方法的话,该属性就会被自动绑定到方法上。我们可以通过反编译工具来查看之前定义的扩展方法,如下所示:

image.png

从编译器发现扩展方法的过程来看,方法的调用顺序应为:类型的实例方法 -> 当前命名空间下的扩展方法 -> 导入命名空间的扩展方法。要想使用不同命名空间的扩展方法,要先引入该命名空间。


空引用也可以调用扩展方法

在C#中,在空引用(即null)上调用实例方法是会引发NullReferenceException异常的,但在空引用上却可以调用扩展方法。你可能会说:“这怎么可能呢?”然而事实上确是如此。为了证明“代码在空引用上调用扩展方法是可以正常运行的”这个结论,我们来看如下代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {

            Console.WriteLine("空引用上调用扩展方法演示:");

            string str = null; //str为空引用

            //在空引用上调用扩展方法,不会发生NullReferenceException异常
            bool myBool = str.IsNull();

            Console.WriteLine("字符串str为空字符串:{0}",myBool);

            Console.ReadKey();
        }
    }

    //扩展方法必须在非泛型静态类中定义
    public static class Extensions
    {
        //不规范的定义扩展方法的方式
        public static bool IsNull(this object obj)
        {
            return obj == null;
        }

    }

}

以上代码在空引用上调用扩展方法时确实没有出现NullReferenceException异常,运行结果如下所示:

image.png

在前面这段代码中,扩展方法的定义方式是不规范的。代码扩展了object类型,所有继承于object的类型都将具有该扩展方法,这就对其他的子类型产生了“污染”。之所以叫它污染,是因为我们定义的扩展方法本来只是想扩展某个子类(如前面的代码只想扩展string类型),却意外的造成了原本不想造成的后果。更好的实现方式应该如下代码所示:

public static bool IsNull(this string str)
{
    return str == null;
}

所以当我们为一个类型定义扩展方法时,应尽量扩展具体的类型,而不要扩展其基类。

在空引用上调用扩展方法之所以不会出现NullReferenceException异常,是因为对于编译器而言,这个过程只是把空引用“str”当成参数传入静态方法而已,即str.IsNull的调用等效于下面的代码:

bool myBool = Extensions.IsNull(str);

Console.WriteLine("字符串str为空字符串:{0}",myBool);

这并不是真正地在空引用上调用方法,所以也就不存在出现NullReferenceException异常的问题了。

为了证实前面的结论,我们来看一下前面例子所对应的IL代码:

image.png

以上红框就是str.IsNull相对应的IL代码,由此可以看出此时确是是把空引用str当作参数传入了静态类Extensions,从而调用了静态方法IsNull,这也证实了前面的结论。

暗锚,解决锚点偏移

文章评论

    嘿,来试试登录吧!