创建调用泛型方法的可执行表达式
Create executable expression calling generic method
好吧,假设我有这种 class 层次结构:
/// In 3rd party library
public class WidgetBase
{
protected void Register<THandler>(Action<THandler> handler) { /* do something */ }
}
public record Message1();
public record Message2();
public sealed class MyWidget : Base
{
public MyClass()
{
RegisterHandlers(this);
}
[Handler]
private void Handle(Message1 msg) {}
[Handler]
private void Handle(Message2 msg) {}
}
public static class Ext
{
// Would prefer extension or normal static method
// and not impose inheritance by putting this
// in an intermediatery base class.
public static void RegisterHandlers<T>(this T t)
{
// Discovers methods with 'Handler' attribute and calls t.Register()
}
}
所以 objective 是实现 RegisterHandlers
,它会自省对象的方法,然后生成一个可执行文件 Expression
,它调用基础 classes 注册方法。考虑 Asp.Net 核心控制器处理程序。
我只是不知道该怎么做。表达式的要点是提高性能,尽管即使是基于纯反射的解决方案也可以。
我可以发现这些方法,甚至可以生成像 t => this.Handle(t)
这样的表达式,但无法理解如何在没有类型的情况下调用通用基础 class 方法。
SO里有很多类似的问题,但找不到确切的解决方案。
[编辑]
使示例更加清晰。
您可以做如下事情:
public static void RegisterHandlers<T>(T t) where T : Base
{
var methods = typeof(T).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute<HandlerAttribute>() != null);
var registerMethod = typeof(Base).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance);
var blockItems = new List<Expression>();
foreach (var method in methods)
{
if (method.IsGenericMethod || method.GetParameters().Length != 1 || method.ReturnType != typeof(void))
throw new Exception($"Invalid method signature for method {method}");
// The type of the Handle method's parameter (e.g. SomeType1 or SomeType2)
var parameterType = method.GetParameters()[0].ParameterType;
// MethodInfo for e.g. the Register<SomeType1> method
var typedRegisterMethod = registerMethod.MakeGenericMethod(parameterType);
// The type of delegate we'll pass to Register, e.g. Action<SomeType1>
var delegateType = typeof(Action<>).MakeGenericType(parameterType);
// Construct the x => Handle(x) delegate
var delegateParameter = Expression.Parameter(parameterType);
var delegateConstruction = Expression.Lambda(delegateType, Expression.Call(Expression.Constant(t), method, delegateParameter), delegateParameter);
// Construct the Register(delegate) call
var methodCall = Expression.Call(Expression.Constant(t), typedRegisterMethod, new[] { delegateConstruction });
// Add this to the list of expressions we'll put in our block
blockItems.Add(methodCall);
}
var compiled = Expression.Lambda<Action>(Expression.Block(blockItems)).Compile();
compiled();
}
请注意,与仅使用反射相比,这样做并没有特别的优势。您没有缓存生成的 compiled
或 blockItems
,这是为此类事情使用编译表达式节省的地方。
不过您可以稍微扩展它,并添加这样的缓存:
private static class Cache<T> where T : Base
{
public static readonly Action<T> Instance = CreateInstance();
private static Action<T> CreateInstance()
{
var methods = typeof(T).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute<HandlerAttribute>() != null);
var registerMethod = typeof(Base).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance);
var instanceParameter = Expression.Parameter(typeof(T));
var blockItems = new List<Expression>();
foreach (var method in methods)
{
if (method.IsGenericMethod || method.GetParameters().Length != 1 || method.ReturnType != typeof(void))
throw new Exception($"Invalid method signature for method {method}");
// The type of the Handle method's parameter (e.g. SomeType1 or SomeType2)
var parameterType = method.GetParameters()[0].ParameterType;
// MethodInfo for e.g. the Register<SomeType1> method
var typedRegisterMethod = registerMethod.MakeGenericMethod(parameterType);
// The type of delegate we'll pass to Register, e.g. Action<SomeType1>
var delegateType = typeof(Action<>).MakeGenericType(parameterType);
// Construct the x => Handle(x) delegate
var delegateParameter = Expression.Parameter(parameterType);
var delegateConstruction = Expression.Lambda(delegateType, Expression.Call(instanceParameter, method, delegateParameter), delegateParameter);
// Construct the Register(delegate) call
var methodCall = Expression.Call(instanceParameter, typedRegisterMethod, new[] { delegateConstruction });
// Add this to the list of expressions we'll put in our block
blockItems.Add(methodCall);
}
var compiled = Expression.Lambda<Action<T>>(Expression.Block(blockItems), instanceParameter).Compile();
return compiled;
}
}
public static void RegisterHandlers<T>(T t) where T : Base
{
Cache<T>.Instance(t);
}
请注意我们现在如何将 T
实例作为参数,这让我们可以缓存生成的 Action<T>
.
到目前为止,您的代码存在一些问题,主要是关于未正确定义扩展方法。此外,当您编写使用多个不同泛型的代码时,这些泛型并非都接受相同的类型,这有助于为类型参数提供比 T
.
更具描述性的名称
另一点是您的扩展可能应该作为受保护的方法进入基 class。您的实际用例可能与上述不同,足以保证扩展形式,但请考虑一下您是否真的需要这种方式。扩展是否与 Base
class 紧密耦合?如果是这样,除非基数 class 不在您的控制范围内,否则它不是扩展的好选择。即便如此,代理基地 class 可能是更好的选择。
总之,进入直接解决方案:反射。
基本流程是这样的:
- 获取
Register<>
方法的通用方法模板。
- 对于派生的 class 中具有
Handler
属性的每个方法:
- 使用正确的参数类型专门化
Register<>
方法。
- 创建正确类型的委托以调用当前实例上的方法。
- 以委托作为参数调用专用方法。
- 利润。
(好的,我只是假设最后一点。)
最有趣的部分是从方法中获取“正确类型的委托”。虽然 MethodInfo
包含 CreateDelegate
方法,但您必须传入正确的专用 Action<T>
类型。幸运的是,您的专用 Register<>
方法的参数类型正是我们在这里想要的。
让我们尝试一个简单的实现:
static class Ext
{
public static void RegisterHandlers<T>(this T instance)
{
// Get the generic method template for `Register<T>`
var registerTemplate = typeof(T).GetMethod("Register", BindingFlags.Instance | BindingFlags.NonPublic);
// Locate all handler methods
var handlerQuery =
from m in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
where m.ReturnType == typeof(void) && m.GetCustomAttribute<HandlerAttribute>() != null
let parms = m.GetParameters()
where parms.Length == 1
select (m, parms[0].ParameterType);
foreach (var (handler, parmType) in handlerQuery)
{
// Specialize the Register<> method
var registerMethod = registerTemplate.MakeGenericMethod(parmType);
// Create Action<T> delegate for method
var actionType = registerMethod.GetParameters()[0].ParameterType;
object action = handler.CreateDelegate(actionType, instance);
// Call the specialized Register<> method
registerMethod.Invoke(instance, new[] { action });
}
}
}
您需要添加适当的错误处理等,但这是基本思想。
虽然它在这个特定案例中有效,但我使用了一个非常简单的 GetMethod
调用来获取通用方法模板。验证您是否拥有正确的方法 - 它是一个通用方法模板(提示:IsGenericMethodDefinition
)并且参数是正确的类型 - 有点困难。常见问题可以这样解决:
var registerTemplate =
(
from m in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
where m.Name == "Register" && m.IsGenericMethodDefinition
let args = m.GetGenericArguments()
where args.Length == 1
let actionTemplate = typeof(Action<>).MakeGenericType(args[0])
let parms = m.GetParameters()
where parms.Length == 1 && parms[0].ParameterType == actionTemplate
select m
).FirstOrDefault();
if (registerTemplate is null)
return;
如果定义了不兼容的 Register
方法,或者匹配不明确,或者...等等,这至少可以避免令人尴尬的崩溃。
虽然这很有趣,但我会认真考虑如何找到一种非反射方法。反射可能有点混乱,而且速度很慢。如果你不能消除反射,至少尽量减少它。您可以让 Register
方法采用 Delegate
和 Type
而不是需要 Action<T>
.
的通用方法
好吧,假设我有这种 class 层次结构:
/// In 3rd party library
public class WidgetBase
{
protected void Register<THandler>(Action<THandler> handler) { /* do something */ }
}
public record Message1();
public record Message2();
public sealed class MyWidget : Base
{
public MyClass()
{
RegisterHandlers(this);
}
[Handler]
private void Handle(Message1 msg) {}
[Handler]
private void Handle(Message2 msg) {}
}
public static class Ext
{
// Would prefer extension or normal static method
// and not impose inheritance by putting this
// in an intermediatery base class.
public static void RegisterHandlers<T>(this T t)
{
// Discovers methods with 'Handler' attribute and calls t.Register()
}
}
所以 objective 是实现 RegisterHandlers
,它会自省对象的方法,然后生成一个可执行文件 Expression
,它调用基础 classes 注册方法。考虑 Asp.Net 核心控制器处理程序。
我只是不知道该怎么做。表达式的要点是提高性能,尽管即使是基于纯反射的解决方案也可以。
我可以发现这些方法,甚至可以生成像 t => this.Handle(t)
这样的表达式,但无法理解如何在没有类型的情况下调用通用基础 class 方法。
SO里有很多类似的问题,但找不到确切的解决方案。
[编辑] 使示例更加清晰。
您可以做如下事情:
public static void RegisterHandlers<T>(T t) where T : Base
{
var methods = typeof(T).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute<HandlerAttribute>() != null);
var registerMethod = typeof(Base).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance);
var blockItems = new List<Expression>();
foreach (var method in methods)
{
if (method.IsGenericMethod || method.GetParameters().Length != 1 || method.ReturnType != typeof(void))
throw new Exception($"Invalid method signature for method {method}");
// The type of the Handle method's parameter (e.g. SomeType1 or SomeType2)
var parameterType = method.GetParameters()[0].ParameterType;
// MethodInfo for e.g. the Register<SomeType1> method
var typedRegisterMethod = registerMethod.MakeGenericMethod(parameterType);
// The type of delegate we'll pass to Register, e.g. Action<SomeType1>
var delegateType = typeof(Action<>).MakeGenericType(parameterType);
// Construct the x => Handle(x) delegate
var delegateParameter = Expression.Parameter(parameterType);
var delegateConstruction = Expression.Lambda(delegateType, Expression.Call(Expression.Constant(t), method, delegateParameter), delegateParameter);
// Construct the Register(delegate) call
var methodCall = Expression.Call(Expression.Constant(t), typedRegisterMethod, new[] { delegateConstruction });
// Add this to the list of expressions we'll put in our block
blockItems.Add(methodCall);
}
var compiled = Expression.Lambda<Action>(Expression.Block(blockItems)).Compile();
compiled();
}
请注意,与仅使用反射相比,这样做并没有特别的优势。您没有缓存生成的 compiled
或 blockItems
,这是为此类事情使用编译表达式节省的地方。
不过您可以稍微扩展它,并添加这样的缓存:
private static class Cache<T> where T : Base
{
public static readonly Action<T> Instance = CreateInstance();
private static Action<T> CreateInstance()
{
var methods = typeof(T).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute<HandlerAttribute>() != null);
var registerMethod = typeof(Base).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance);
var instanceParameter = Expression.Parameter(typeof(T));
var blockItems = new List<Expression>();
foreach (var method in methods)
{
if (method.IsGenericMethod || method.GetParameters().Length != 1 || method.ReturnType != typeof(void))
throw new Exception($"Invalid method signature for method {method}");
// The type of the Handle method's parameter (e.g. SomeType1 or SomeType2)
var parameterType = method.GetParameters()[0].ParameterType;
// MethodInfo for e.g. the Register<SomeType1> method
var typedRegisterMethod = registerMethod.MakeGenericMethod(parameterType);
// The type of delegate we'll pass to Register, e.g. Action<SomeType1>
var delegateType = typeof(Action<>).MakeGenericType(parameterType);
// Construct the x => Handle(x) delegate
var delegateParameter = Expression.Parameter(parameterType);
var delegateConstruction = Expression.Lambda(delegateType, Expression.Call(instanceParameter, method, delegateParameter), delegateParameter);
// Construct the Register(delegate) call
var methodCall = Expression.Call(instanceParameter, typedRegisterMethod, new[] { delegateConstruction });
// Add this to the list of expressions we'll put in our block
blockItems.Add(methodCall);
}
var compiled = Expression.Lambda<Action<T>>(Expression.Block(blockItems), instanceParameter).Compile();
return compiled;
}
}
public static void RegisterHandlers<T>(T t) where T : Base
{
Cache<T>.Instance(t);
}
请注意我们现在如何将 T
实例作为参数,这让我们可以缓存生成的 Action<T>
.
到目前为止,您的代码存在一些问题,主要是关于未正确定义扩展方法。此外,当您编写使用多个不同泛型的代码时,这些泛型并非都接受相同的类型,这有助于为类型参数提供比 T
.
另一点是您的扩展可能应该作为受保护的方法进入基 class。您的实际用例可能与上述不同,足以保证扩展形式,但请考虑一下您是否真的需要这种方式。扩展是否与 Base
class 紧密耦合?如果是这样,除非基数 class 不在您的控制范围内,否则它不是扩展的好选择。即便如此,代理基地 class 可能是更好的选择。
总之,进入直接解决方案:反射。
基本流程是这样的:
- 获取
Register<>
方法的通用方法模板。 - 对于派生的 class 中具有
Handler
属性的每个方法:- 使用正确的参数类型专门化
Register<>
方法。 - 创建正确类型的委托以调用当前实例上的方法。
- 以委托作为参数调用专用方法。
- 使用正确的参数类型专门化
- 利润。
(好的,我只是假设最后一点。)
最有趣的部分是从方法中获取“正确类型的委托”。虽然 MethodInfo
包含 CreateDelegate
方法,但您必须传入正确的专用 Action<T>
类型。幸运的是,您的专用 Register<>
方法的参数类型正是我们在这里想要的。
让我们尝试一个简单的实现:
static class Ext
{
public static void RegisterHandlers<T>(this T instance)
{
// Get the generic method template for `Register<T>`
var registerTemplate = typeof(T).GetMethod("Register", BindingFlags.Instance | BindingFlags.NonPublic);
// Locate all handler methods
var handlerQuery =
from m in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
where m.ReturnType == typeof(void) && m.GetCustomAttribute<HandlerAttribute>() != null
let parms = m.GetParameters()
where parms.Length == 1
select (m, parms[0].ParameterType);
foreach (var (handler, parmType) in handlerQuery)
{
// Specialize the Register<> method
var registerMethod = registerTemplate.MakeGenericMethod(parmType);
// Create Action<T> delegate for method
var actionType = registerMethod.GetParameters()[0].ParameterType;
object action = handler.CreateDelegate(actionType, instance);
// Call the specialized Register<> method
registerMethod.Invoke(instance, new[] { action });
}
}
}
您需要添加适当的错误处理等,但这是基本思想。
虽然它在这个特定案例中有效,但我使用了一个非常简单的 GetMethod
调用来获取通用方法模板。验证您是否拥有正确的方法 - 它是一个通用方法模板(提示:IsGenericMethodDefinition
)并且参数是正确的类型 - 有点困难。常见问题可以这样解决:
var registerTemplate =
(
from m in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
where m.Name == "Register" && m.IsGenericMethodDefinition
let args = m.GetGenericArguments()
where args.Length == 1
let actionTemplate = typeof(Action<>).MakeGenericType(args[0])
let parms = m.GetParameters()
where parms.Length == 1 && parms[0].ParameterType == actionTemplate
select m
).FirstOrDefault();
if (registerTemplate is null)
return;
如果定义了不兼容的 Register
方法,或者匹配不明确,或者...等等,这至少可以避免令人尴尬的崩溃。
虽然这很有趣,但我会认真考虑如何找到一种非反射方法。反射可能有点混乱,而且速度很慢。如果你不能消除反射,至少尽量减少它。您可以让 Register
方法采用 Delegate
和 Type
而不是需要 Action<T>
.