溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

C#如何實(shí)現(xiàn)接口base調(diào)用

發(fā)布時(shí)間:2022-06-13 14:05:19 來(lái)源:億速云 閱讀:143 作者:iii 欄目:開(kāi)發(fā)技術(shù)

今天小編給大家分享一下C#如何實(shí)現(xiàn)接口base調(diào)用的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來(lái)了解一下吧。

    背景

    在三年前發(fā)布的C#8.0中有一項(xiàng)重要的改進(jìn)叫做接口默認(rèn)實(shí)現(xiàn),從此以后,接口中定義的方法可以包含方法體了,即默認(rèn)實(shí)現(xiàn)。

    不過(guò)對(duì)于接口的默認(rèn)實(shí)現(xiàn),其實(shí)現(xiàn)類或者子接口在重寫(xiě)這個(gè)方法的時(shí)候不能對(duì)其進(jìn)行base調(diào)用,就像子類重寫(xiě)方法是可以進(jìn)行base.Method()那樣。例如:

    public interface IService
    {
        void Proccess()
        {
            Console.WriteLine("Proccessing");
        }
    }
    public class Service : IService
    {
        public void Proccess()
        {
            Console.WriteLine("Before Proccess");
            base(IService).Proccess(); // 目前不支持,也是本文需要探討的部分
            Console.WriteLine("End Proccess");
        }
    }

    當(dāng)初C#團(tuán)隊(duì)將這個(gè)特性列為了下一步的計(jì)劃(點(diǎn)此查看細(xì)節(jié)),然而三年過(guò)去了依然沒(méi)有被提上日程。這個(gè)特性的缺失無(wú)疑是一種很大的限制,有時(shí)候我們確實(shí)需要接口的base調(diào)用來(lái)實(shí)現(xiàn)某些需求。本文將介紹兩種方法來(lái)實(shí)現(xiàn)它。

    方法1:使用反射找到接口實(shí)現(xiàn)并進(jìn)行調(diào)用

    這種方法的核心思想是,使用反射找到你需要調(diào)用的接口實(shí)現(xiàn)的MethodInfo,然后構(gòu)建DynamicMethod使用OpCodes.Call去調(diào)用它即可。

    首先我們定義方法簽名用來(lái)表示接口方法的base調(diào)用。

    public static void Base<TInterface>(this TInterface instance, Expression<Action<TInterface>> selector);
    public static TReturn Base<TInterface, TReturn>(this TInterface instance, Expression<Func<TInterface, TReturn>> selector);

    所以上一節(jié)的例子就可以改寫(xiě)成:

    public class Service : IService
    {
        public void Proccess()
        {
            Console.WriteLine("Before Proccess");
            this.Base&lt;IService&gt;(m =&gt; m.Proccess());
            Console.WriteLine("End Proccess");
        }
    }

    于是接下來(lái),我們就需要根據(jù)lambda表達(dá)式找到其對(duì)應(yīng)的接口實(shí)現(xiàn),然后調(diào)用即可。

    第一步根據(jù)lambda表達(dá)式獲取MethodInfo和參數(shù)。要注意的是,對(duì)于屬性的調(diào)用我們也需要支持,其實(shí)屬性也是一種方法,所以可以一并處理。

    private static (MethodInfo method, IReadOnlyList&lt;Expression&gt; args) GetMethodAndArguments(Expression exp) =&gt; exp switch
    {
        LambdaExpression lambda =&gt; GetMethodAndArguments(lambda.Body),
        UnaryExpression unary =&gt; GetMethodAndArguments(unary.Operand),
        MethodCallExpression methodCall =&gt; (methodCall.Method!, methodCall.Arguments),
        MemberExpression { Member: PropertyInfo prop } =&gt; (prop.GetGetMethod(true) ?? throw new MissingMethodException($"No getter in propery {prop.Name}"), Array.Empty&lt;Expression&gt;()),
        _ =&gt; throw new InvalidOperationException("The expression refers to neither a method nor a readable property.")
    };

    第二步,利用Type.GetInterfaceMap獲取到需要調(diào)用的接口實(shí)現(xiàn)方法。此處注意的要點(diǎn)是,instanceType.GetInterfaceMap(interfaceType).InterfaceMethods 會(huì)返回該接口的所有方法,所以不能僅根據(jù)方法名去匹配,因?yàn)榭赡苡懈鞣N重載、泛型參數(shù)、還有new關(guān)鍵字聲明的同名方法,所以可以按照方法名+聲明類型+方法參數(shù)+方法泛型參數(shù)唯一確定一個(gè)方法(即下面代碼塊中IfMatch的實(shí)現(xiàn))

    internal readonly record struct InterfaceMethodInfo(Type InstanceType, Type InterfaceType, MethodInfo Method);
    private static MethodInfo GetInterfaceMethod(InterfaceMethodInfo info)
    {
        var (instanceType, interfaceType, method) = info;
        var parameters = method.GetParameters();
        var genericArguments = method.GetGenericArguments();
        var interfaceMethods = instanceType
            .GetInterfaceMap(interfaceType)
            .InterfaceMethods
            .Where(m =&gt; IfMatch(method, genericArguments, parameters, m))
            .ToArray();
        var interfaceMethod = interfaceMethods.Length switch
        {
            0 =&gt; throw new MissingMethodException($"Can not find method {method.Name} in type {instanceType.Name}"),
            &gt; 1 =&gt; throw new AmbiguousMatchException($"Found more than one method {method.Name} in type {instanceType.Name}"),
            1 when interfaceMethods[0].IsAbstract =&gt; throw new InvalidOperationException($"The method {interfaceMethods[0].Name} is abstract"),
            _ =&gt; interfaceMethods[0]
        };
        if (method.IsGenericMethod)
            interfaceMethod = interfaceMethod.MakeGenericMethod(method.GetGenericArguments());
        return interfaceMethod;
    }

    第三步,用獲取到的接口方法,構(gòu)建DynamicMethod。其中的重點(diǎn)是使用OpCodes.Call,它的含義是以非虛方式調(diào)用一個(gè)方法,哪怕該方法是虛方法,也不去查找它的重寫(xiě),而是直接調(diào)用它自身。

    private static DynamicMethod GetDynamicMethod(Type interfaceType, MethodInfo method, IEnumerable&lt;Type&gt; argumentTypes)
    {
        var dynamicMethod = new DynamicMethod(
            name: "__IL_" + method.GetFullName(),
            returnType: method.ReturnType,
            parameterTypes: new[] { interfaceType, typeof(object[]) },
            owner: typeof(object),
            skipVisibility: true);
        var il = dynamicMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        var i = 0;
        foreach (var argumentType in argumentTypes)
        {
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Ldc_I4, i);
            il.Emit(OpCodes.Ldelem, typeof(object));
            if (argumentType.IsValueType)
            {
                il.Emit(OpCodes.Unbox_Any, argumentType);
            }
            ++i;
        }
        il.Emit(OpCodes.Call, method);
        il.Emit(OpCodes.Ret);
        return dynamicMethod;
    }

    最后,將DynamicMethod轉(zhuǎn)為強(qiáng)類型的委托就完成了??紤]到性能的優(yōu)化,可以將最終的委托緩存起來(lái),下次調(diào)用就不用再構(gòu)建一次了。

    han12345/c42de446a23aa9a17fb6abf905479f25" rel="external nofollow" target="_blank">完整的代碼點(diǎn)這里 

    方法2:利用函數(shù)指針

    這個(gè)方法和方法1大同小異,區(qū)別是,在方法1的第二步,即找到接口方法的MethodInfo之后,獲取其函數(shù)指針,然后利用該指針構(gòu)造委托。這個(gè)方法其實(shí)是我最初找到的方法,方法1是其改進(jìn)。在此就不多做介紹了

    方法3:利用Fody在編譯時(shí)對(duì)接口方法進(jìn)行IL的call調(diào)用

    方法1雖然可行,但是肉眼可見(jiàn)的性能損失大,即使是用了緩存。于是乎我利用Fody編寫(xiě)了一個(gè)插件InterfaceBaseInvoke.Fody。

    其核心思想就是在編譯時(shí)找到目標(biāo)接口方法,然后使用call命令調(diào)用它就行了。這樣可以把性能損失降到最低。該插件的使用方法可以參考項(xiàng)目介紹。

    性能測(cè)試

    方法平均用時(shí)內(nèi)存分配
    父類的base調(diào)用0.0000 ns-
    方法1(DynamicMethod)691.3687 ns776 B
    方法2(FunctionPointer)1,391.9345 ns1,168 B
    方法3(InterfaceBaseInvoke.Fody)0.0066 ns-

    以上就是“C#如何實(shí)現(xiàn)接口base調(diào)用”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

    向AI問(wèn)一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI