C# 扩展方法之个人理解

参考:

MSDN:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/extension-methods

博客园:http://www.cnblogs.com/suger/archive/2012/05/13/2498248.html

MSDN的开编语句:

扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。 扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用。


开始上代码:

1.首先创建一个控制台应用程序。

在Program.cs中定义一个字符串转换为整数的实例方法。

如何实例方法在同一个类下,则直接strToInt(str) 就可以实现转换了。

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

namespace ExtensionMethodsTest
{
    class Program
    {
        static void Main(string[] args)
        {
            string str = "50";

             Program pm = new Program();

            //实例方法
            int ret = pm.strToInt(str);

            Console.WriteLine("ok");
            Console.ReadKey();
        }

        /// <summary>
        /// 字符串转为整数的实例方法
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public int strToInt(string str)
        {
            int id;
            int.TryParse(str, out id);

            return id;
        }

    }
}

2、静态方法创建

先创建一个类库ExtensionMethodCommon;然后在该类库下创建一个静态类ExtensionMethods;在该类下创建一个静态方法ToInt(this string str,int numberOne ,int numberTwo)。

扩展方法被定义为静态方法,但它们通过实例方法语法进行调用。它们的第一个参数制定该方法作用于哪个类型,并且该参数以this修饰符为前缀。

 

ExtensionMethods里有一个ToInt的静态方法,他接收一个自身参数this,类型为string,this string必须在方法参数的第一个位置,其他参数必须在this string 参数后面。

这句话什么意思,即你需要对string扩展一个ToInt方法,

this是string实例化后的对象(下边第三点的代码中,string str = "50";  int result = str.ToInt(10, 5);  此时this就是str)。

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

namespace ExtensionMethodCommon
{
    
    public static class ExtensionMethods
    {
        //扩展发方法
        //必须是在静态类里边
        public static int ToInt(this string str,int numberOne ,int numberTwo)
        {
            
            int ret = numberOne-numberTwo;

            int id;
            int.TryParse(str, out id);//转换失败是返回0

            return id;
        }
    }
}

3.调用静态方法

必须引用静态方法所在的命名空间:using ExtensionMethodCommon;

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

namespace ExtensionMethodsTest
{
    class Program
    {
        static void Main(string[] args)
        {
            string str = "50";

             Program pm = new Program();

            //实例方法
            int ret = pm.strToInt(str);

            //调用静态方法
            int result = str.ToInt(10, 5);

            Console.WriteLine("ok");
            Console.ReadKey();
        }

        /// <summary>
        /// 字符串转为整数的实例方法
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public int strToInt(string str)
        {
            int id;
            int.TryParse(str, out id);

            return id;
        }

    }
}

 


MSDN上说:在代码中,可以使用实例方法语法调用该扩展方法。 但是,编译器生成的中间语言 (IL) 会将代码转换为对静态方法的调用。 因此,并未真正违反封装原则。

我们来看看IL代码,IL代码对应的是Program里边的代码。

如下图:

静态方法调用对应的IL代码


下边是MSDN上的此处就粘贴过来,纯属为了自己以后方便看:

在编译时绑定扩展方法

可以使用扩展方法来扩展类或接口,但不能重写扩展方法。 与接口或类方法具有相同名称和签名的扩展方法永远不会被调用。 编译时,扩展方法的优先级总是比类型本身中定义的实例方法低。 换句话说,如果某个类型具有一个名为 Process(int i) 的方法,而你有一个具有相同签名的扩展方法,则编译器总是绑定到该实例方法。 当编译器遇到方法调用时,它首先在该类型的实例方法中寻找匹配的方法。 如果未找到任何匹配方法,编译器将搜索为该类型定义的任何扩展方法,并且绑定到它找到的第一个扩展方法。 下面的示例演示编译器如何确定要绑定到哪个扩展方法或实例方法

示例

下面的示例演示 C# 编译器在确定是将方法调用绑定到类型上的实例方法还是绑定到扩展方法时所遵循的规则。静态类 Extensions 包含为任何实现了 IMyInterface 的类型定义的扩展方法。 类 AB 和 C 都实现了该接口。

MethodB 扩展方法永远不会被调用,因为它的名称和签名与这些类已经实现的方法完全匹配。

如果编译器找不到具有匹配签名的实例方法,它会绑定到匹配的扩展方法(如果存在这样的方法)

// Define an interface named IMyInterface.
namespace DefineIMyInterface
{
    using System;

    public interface IMyInterface
    {
        // Any class that implements IMyInterface must define a method
        // that matches the following signature.
        void MethodB();
    }
}


// Define extension methods for IMyInterface.
namespace Extensions
{
    using System;
    using DefineIMyInterface;

    // The following extension methods can be accessed by instances of any 
    // class that implements IMyInterface.
    public static class Extension
    {
        public static void MethodA(this IMyInterface myInterface, int i)
        {
            Console.WriteLine
                ("Extension.MethodA(this IMyInterface myInterface, int i)");
        }

        public static void MethodA(this IMyInterface myInterface, string s)
        {
            Console.WriteLine
                ("Extension.MethodA(this IMyInterface myInterface, string s)");
        }

        // This method is never called in ExtensionMethodsDemo1, because each 
        // of the three classes A, B, and C implements a method named MethodB
        // that has a matching signature.
        public static void MethodB(this IMyInterface myInterface)
        {
            Console.WriteLine
                ("Extension.MethodB(this IMyInterface myInterface)");
        }
    }
}


// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
namespace ExtensionMethodsDemo1
{
    using System;
    using Extensions;
    using DefineIMyInterface;

    class A : IMyInterface
    {
        public void MethodB() { Console.WriteLine("A.MethodB()"); }
    }

    class B : IMyInterface
    {
        public void MethodB() { Console.WriteLine("B.MethodB()"); }
        public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
    }

    class C : IMyInterface
    {
        public void MethodB() { Console.WriteLine("C.MethodB()"); }
        public void MethodA(object obj)
        {
            Console.WriteLine("C.MethodA(object obj)");
        }
    }

    class ExtMethodDemo
    {
        static void Main(string[] args)
        {
            // Declare an instance of class A, class B, and class C.
            A a = new A();
            B b = new B();
            C c = new C();

            // For a, b, and c, call the following methods:
            //      -- MethodA with an int argument
            //      -- MethodA with a string argument
            //      -- MethodB with no argument.

            // A contains no MethodA, so each call to MethodA resolves to 
            // the extension method that has a matching signature.
            a.MethodA(1);           // Extension.MethodA(IMyInterface, int)
            a.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

            // A has a method that matches the signature of the following call
            // to MethodB.
            a.MethodB();            // A.MethodB()

            // B has methods that match the signatures of the following
            // method calls.
            b.MethodA(1);           // B.MethodA(int)
            b.MethodB();            // B.MethodB()

            // B has no matching method for the following call, but 
            // class Extension does.
            b.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

            // C contains an instance method that matches each of the following
            // method calls.
            c.MethodA(1);           // C.MethodA(object)
            c.MethodA("hello");     // C.MethodA(object)
            c.MethodB();            // C.MethodB()
        }
    }
}
/* Output:
    Extension.MethodA(this IMyInterface myInterface, int i)
    Extension.MethodA(this IMyInterface myInterface, string s)
    A.MethodB()
    B.MethodA(int i)
    B.MethodB()
    Extension.MethodA(this IMyInterface myInterface, string s)
    C.MethodA(object obj)
    C.MethodA(object obj)
    C.MethodB()
 */

通用准则

通常,建议你只在不得已的情况下才实现扩展方法,并谨慎地实现。 只要有可能,必须扩展现有类型的客户端代码都应该通过创建从现有类型派生的新类型来达到这一目的。 有关详细信息,请参阅继承

在使用扩展方法来扩展你无法更改其源代码的类型时,你需要承受该类型实现中的更改会导致扩展方法失效的风险。

如果确实为给定类型实现了扩展方法,请记住以下几点:

  • 如果扩展方法与该类型中定义的方法具有相同的签名,则扩展方法永远不会被调用。

  • 在命名空间级别将扩展方法置于范围中。 例如,如果你在一个名为 Extensions 的命名空间中具有多个包含扩展方法的静态类,则这些扩展方法将全部由 using Extensions; 指令置于范围中。

 

一般情况下,由于项目架构都已经确定,为了在不修改源代码的情况下会选择使用扩展方法。


CLR Via 中的一些知识:

编译器如何识别是扩展方法?

       在C#中,一旦调用this关键字标记了某个静态方法的第一个参数,编译器就会在内部向该方法应用一个定制特性,该特性会在最终生成的文件的元数据中持久性地存储下来。该特性在System.Core.dll程序集中定义,它看起来想下面这样:

//在System.Runtime.CompilerServices命名空间中定义
[AttributeUsage(AttributeTargets.Method|AttributeTargets.Class|AttrubuteTargets.Assembly)]
public sealed calss ExtensionAttribute:Attribute{

}

除此之外,任何静态类只要包含至少一个扩展方法,它的元数据中也会应用这个特性。类似地,任何程序集只要包含了至少一个符合上述特点的静态类,它的元数据中也会应用这个特性。这样一来,如果代码调用了一个不存在的实例方法,编译器就能快速扫描引用的所有程序集,判断他们哪些包含了扩展方法。然后,在这些程序集中,可以只扫描包含了扩展方法的静态类。在每个这样的静态类中,可以只扫描扩展方法来查找匹配。利用这个技术,代码能以最快速度编译完毕。

规则和原则:

1、C#只支持扩展方法,不支持扩展属性、扩展事件、扩展操作符等。

2、扩展方法(第一个参数前面有this的方法)必须在非泛型的静态类中声明。然而,类名没有限制,可以随便叫什么名字。当然,扩展方法至少要有一个参数,而且只有第一个参数能用this关键字标记。

3、C#编译器在静态类中查找扩展方法时,要求静态类本身必须具有文件作用域(类要具有整个文件的作用域,而不能嵌套在某个类中而只具有该类的作用域)。如果静态类嵌套在另一个类中,C#编译器显示以下信息:error CS1109:扩展方法必须在顶级静态类中定义;StringBuilderExtensions是嵌套(StringBuilderExtensions是静态类,即包含要扩展的扩展方法)。

4、由于静态类可以取任何名字,所以C#编译器要花一定时间来寻找扩展方法,它必须检查文件作用域中的所有静态类,并扫描他们的所有静态方法来查找一个匹配。为了增强性能,并避免找到非你所愿的扩展方法,C#编译器要求“导入”扩展方法。例如,如果有人在Wintellect命名空间中定义一个StringBuilderExtensions类,那么程序员为了访问这个类的扩展方法,必须在他的源代码文件顶部写一条using Wintellect;指令。

5、多个静态类可以定义相同的扩展方法。如果编译器检测到存在两个或者多个扩展方法,就会显示以下信息: error CS0121:在以下方法或属性之间的调用不明确:“StringBuilderExtensions.IndexOf(string,char)” 和 “AnotherStringBuilderExtensions.IndnxOf(string,char)”。修正这个错误必须修改源代码。具体的说,不能再用这个实例方法语法来调用这个静态方法。相反,必须使用静态方法语法。换言之,必须显示指定静态类的名称,明确告诉编译器要调用哪个方法。

6、使用这个功能须谨慎,一个原因是并非所有程序员都熟悉它。例如,用一个扩展方法扩展一个类型时,同时也扩展了派生类型。所以,不要经System.Object作为扩展方法的第一个参数,否则这个方法在所有表达式类型上 都能调用,造成Visual Studio的“智能感知”窗口被填充太多垃圾信息。

7、扩展方法可能存在版本控制问题。如果Microsoft 未来为他们的StringBuilder类添加了IndexOf实例方法,而且和我的代码调用的原形一样,那么在重新编译我的代码时,编译器会绑定到Microsoft 的IndexOf实例方法,而不是我的静态IndexOf方法。这样我的程序就会有不同的行为。版本控制问题是使用扩展方法须谨慎的另一个原因。