C#實現(xiàn)接口base調(diào)用示例詳解
背景
在三年前發(fā)布的C#8.0中有一項重要的改進叫做接口默認實現(xiàn),從此以后,接口中定義的方法可以包含方法體了,即默認實現(xiàn)。
不過對于接口的默認實現(xiàn),其實現(xiàn)類或者子接口在重寫這個方法的時候不能對其進行base調(diào)用,就像子類重寫方法是可以進行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#團隊將這個特性列為了下一步的計劃(點此查看細節(jié)),然而三年過去了依然沒有被提上日程。這個特性的缺失無疑是一種很大的限制,有時候我們確實需要接口的base調(diào)用來實現(xiàn)某些需求。本文將介紹兩種方法來實現(xiàn)它。
方法1:使用反射找到接口實現(xiàn)并進行調(diào)用
這種方法的核心思想是,使用反射找到你需要調(diào)用的接口實現(xiàn)的MethodInfo,然后構(gòu)建DynamicMethod使用OpCodes.Call去調(diào)用它即可。
首先我們定義方法簽名用來表示接口方法的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é)的例子就可以改寫成:
public class Service : IService
{
public void Proccess()
{
Console.WriteLine("Before Proccess");
this.Base<IService>(m => m.Proccess());
Console.WriteLine("End Proccess");
}
}于是接下來,我們就需要根據(jù)lambda表達式找到其對應(yīng)的接口實現(xiàn),然后調(diào)用即可。
第一步根據(jù)lambda表達式獲取MethodInfo和參數(shù)。要注意的是,對于屬性的調(diào)用我們也需要支持,其實屬性也是一種方法,所以可以一并處理。
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獲取到需要調(diào)用的接口實現(xiàn)方法。此處注意的要點是,instanceType.GetInterfaceMap(interfaceType).InterfaceMethods 會返回該接口的所有方法,所以不能僅根據(jù)方法名去匹配,因為可能有各種重載、泛型參數(shù)、還有new關(guān)鍵字聲明的同名方法,所以可以按照方法名+聲明類型+方法參數(shù)+方法泛型參數(shù)唯一確定一個方法(即下面代碼塊中IfMatch的實現(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 => 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;
}第三步,用獲取到的接口方法,構(gòu)建DynamicMethod。其中的重點是使用OpCodes.Call,它的含義是以非虛方式調(diào)用一個方法,哪怕該方法是虛方法,也不去查找它的重寫,而是直接調(diào)用它自身。
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轉(zhuǎn)為強類型的委托就完成了??紤]到性能的優(yōu)化,可以將最終的委托緩存起來,下次調(diào)用就不用再構(gòu)建一次了。
方法2:利用函數(shù)指針
這個方法和方法1大同小異,區(qū)別是,在方法1的第二步,即找到接口方法的MethodInfo之后,獲取其函數(shù)指針,然后利用該指針構(gòu)造委托。這個方法其實是我最初找到的方法,方法1是其改進。在此就不多做介紹了
方法3:利用Fody在編譯時對接口方法進行IL的call調(diào)用
方法1雖然可行,但是肉眼可見的性能損失大,即使是用了緩存。于是乎我利用Fody編寫了一個插件InterfaceBaseInvoke.Fody。
其核心思想就是在編譯時找到目標接口方法,然后使用call命令調(diào)用它就行了。這樣可以把性能損失降到最低。該插件的使用方法可以參考項目介紹。
性能測試
| 方法 | 平均用時 | 內(nèi)存分配 |
| 父類的base調(diào)用 | 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 | - |
總結(jié)
本文探討了幾種實現(xiàn)接口的base調(diào)用的方法,其中性能以InterfaceBaseInvoke.Fody最佳,在C#官方支持以前推薦使用,更多關(guān)于C#實現(xiàn)接口base調(diào)用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#遠程發(fā)送和接收數(shù)據(jù)流生成圖片的方法
這篇文章主要介紹了C#遠程發(fā)送和接收數(shù)據(jù)流生成圖片的方法,涉及C#通過數(shù)據(jù)流傳輸圖片的相關(guān)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-07-07
C#零基礎(chǔ)開發(fā)中最重要的概念總結(jié)
這篇文章主要為大家詳細介紹了C#零基礎(chǔ)開發(fā)中最重要的一些概念,文中的示例代碼講解詳細,對我們學(xué)習(xí)C#有一定的幫助,需要的可以參考一下2023-02-02
C#實現(xiàn)窗體中動態(tài)按鈕的設(shè)計方法
在窗體界面中,通常以按鈕來代替菜單欄的功能,這種形式雖然給用戶一種直觀、界面風(fēng)格各異的感覺,但通常按鈕都是以靜止的形式顯示,所以本文給大家介紹了C#實現(xiàn)窗體中動態(tài)按鈕的設(shè)計方法,感興趣的朋友可以參考下2024-04-04
C#把EXCEL數(shù)據(jù)轉(zhuǎn)換成DataTable
這篇文章介紹了C#把EXCEL數(shù)據(jù)轉(zhuǎn)換成DataTable的方法,文中通過示例代碼介紹的非常詳細。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-04-04

