C#實現接口base調用示例詳解
背景
在三年前發佈的C#8.0中有一項重要的改進叫做接口默認實現,從此以後,接口中定義的方法可以包含方法體瞭,即默認實現。
不過對於接口的默認實現,其實現類或者子接口在重寫這個方法的時候不能對其進行base調用,就像子類重寫方法是可以進行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"); } }
當初C#團隊將這個特性列為瞭下一步的計劃(點此查看細節),然而三年過去瞭依然沒有被提上日程。這個特性的缺失無疑是一種很大的限制,有時候我們確實需要接口的base調用來實現某些需求。本文將介紹兩種方法來實現它。
方法1:使用反射找到接口實現並進行調用
這種方法的核心思想是,使用反射找到你需要調用的接口實現的MethodInfo
,然後構建DynamicMethod
使用OpCodes.Call
去調用它即可。
首先我們定義方法簽名用來表示接口方法的base調用。
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);
所以上一節的例子就可以改寫成:
public class Service : IService { public void Proccess() { Console.WriteLine("Before Proccess"); this.Base<IService>(m => m.Proccess()); Console.WriteLine("End Proccess"); } }
於是接下來,我們就需要根據lambda表達式找到其對應的接口實現,然後調用即可。
第一步根據lambda表達式獲取MethodInfo和參數。要註意的是,對於屬性的調用我們也需要支持,其實屬性也是一種方法,所以可以一並處理。
private static (MethodInfo method, IReadOnlyList<Expression> args) GetMethodAndArguments(Expression exp) => exp switch { LambdaExpression lambda => GetMethodAndArguments(lambda.Body), UnaryExpression unary => GetMethodAndArguments(unary.Operand), MethodCallExpression methodCall => (methodCall.Method!, methodCall.Arguments), MemberExpression { Member: PropertyInfo prop } => (prop.GetGetMethod(true) ?? throw new MissingMethodException($"No getter in propery {prop.Name}"), Array.Empty<Expression>()), _ => throw new InvalidOperationException("The expression refers to neither a method nor a readable property.") };
第二步,利用Type.GetInterfaceMap獲取到需要調用的接口實現方法。此處註意的要點是,instanceType.GetInterfaceMap(interfaceType).InterfaceMethods 會返回該接口的所有方法,所以不能僅根據方法名去匹配,因為可能有各種重載、泛型參數、還有new關鍵字聲明的同名方法,所以可以按照方法名+聲明類型+方法參數+方法泛型參數唯一確定一個方法(即下面代碼塊中IfMatch
的實現)
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 => IfMatch(method, genericArguments, parameters, m)) .ToArray(); var interfaceMethod = interfaceMethods.Length switch { 0 => throw new MissingMethodException($"Can not find method {method.Name} in type {instanceType.Name}"), > 1 => throw new AmbiguousMatchException($"Found more than one method {method.Name} in type {instanceType.Name}"), 1 when interfaceMethods[0].IsAbstract => throw new InvalidOperationException($"The method {interfaceMethods[0].Name} is abstract"), _ => interfaceMethods[0] }; if (method.IsGenericMethod) interfaceMethod = interfaceMethod.MakeGenericMethod(method.GetGenericArguments()); return interfaceMethod; }
第三步,用獲取到的接口方法,構建DynamicMethod
。其中的重點是使用OpCodes.Call
,它的含義是以非虛方式調用一個方法,哪怕該方法是虛方法,也不去查找它的重寫,而是直接調用它自身。
private static DynamicMethod GetDynamicMethod(Type interfaceType, MethodInfo method, IEnumerable<Type> 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
轉為強類型的委托就完成瞭。考慮到性能的優化,可以將最終的委托緩存起來,下次調用就不用再構建一次瞭。
han12345/c42de446a23aa9a17fb6abf905479f25″ rel=”external nofollow” target=”_blank”>完整的代碼點這裡
方法2:利用函數指針
這個方法和方法1大同小異,區別是,在方法1的第二步,即找到接口方法的MethodInfo
之後,獲取其函數指針,然後利用該指針構造委托。這個方法其實是我最初找到的方法,方法1是其改進。在此就不多做介紹瞭
方法3:利用Fody在編譯時對接口方法進行IL的call調用
方法1雖然可行,但是肉眼可見的性能損失大,即使是用瞭緩存。於是乎我利用Fody編寫瞭一個插件InterfaceBaseInvoke.Fody。
其核心思想就是在編譯時找到目標接口方法,然後使用call命令調用它就行瞭。這樣可以把性能損失降到最低。該插件的使用方法可以參考項目介紹。
性能測試
方法 | 平均用時 | 內存分配 |
父類的base調用 | 0.0000 ns | – |
方法1(DynamicMethod) | 691.3687 ns | 776 B |
方法2(FunctionPointer) | 1,391.9345 ns | 1,168 B |
方法3(InterfaceBaseInvoke.Fody) | 0.0066 ns | – |
總結
本文探討瞭幾種實現接口的base調用的方法,其中性能以InterfaceBaseInvoke.Fody最佳,在C#官方支持以前推薦使用,更多關於C#實現接口base調用的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- c# 在Emit代碼中如何await一個異步方法
- C#值類型、引用類型、泛型、集合、調用函數的表達式樹實踐
- C#使用反射機制實現延遲綁定
- .NET中創建對象的幾種方式和對比
- C# IQueryable<T>揭開表達式樹的神秘面紗