通过 LINQ 表达式而非反射进行通用方法访问






4.45/5 (12投票s)
通过 Reflection.MethodInfo 进行访问速度提高 10 倍(已更新)
抱歉 - 附加的代码已过时
请先参考Dismembers 文章评论。他建议,我自行开发的、使用 LINQ 表达式“编译”MethodInfos 为匿名方法的方法,其实 MethodInfo 类本身已经提供了该功能 - 即 MethodInfo.CreateDelegate<TDelegate>()
。
现在我将保留这篇文章,也许您可以从中了解一些 LINQ 表达式的操作方法,并瞥见其一些概念。
但请不要将附加的源文件视为解决实际问题的方案。而是使用我的
MethodInfo.MakeCompiledMethod<TDelegate>()
- 扩展方法
命令式地直接使用内置的
MethodInfo.CreateDelegate<TDelegate>()
- 实例方法。
也许我现在应该删除这篇文章,但从读者的兴趣来看,很少有程序员知道 MethodInfo.CreateDelegate<TDelegate>()
因此,尽管这篇文章在某种程度上失去了可以分享的代码,但它仍然有助于提高对 MethodInfo.CreateDelegate()
的认识度 - 这可以被视为对社区知识的宝贵贡献。
但现在,这篇文章将回归到我了解 .CreateDelegate()
之前的样子
先看代码 ;-)
public static Delegate MakeCompiledMethod(this MethodInfo mtd) {
if (mtd == null) throw new ArgumentNullException("ReflectionX.MakeCompiledMethod(MethodInfo mtd): mtd mustn't be null");
var prams = mtd.GetParameters().Select(p => Expression.Parameter(p.ParameterType, p.Name)).ToList();
Expression methodCall;
if (mtd.IsStatic) methodCall = Expression.Call(null, mtd, prams);
else { // on instance-Methods the ownerInstance must be included
var ownerInstance = Expression.Variable(mtd.DeclaringType, "ownerInstance");
methodCall = Expression.Call(ownerInstance, mtd, prams);
prams.Insert(0, ownerInstance);
}
return Expression.Lambda(methodCall, false, prams).Compile();
}
然后是基准测试
测试类 Foo
,包含 2 个方法(void
和 int
)
class Foo {
public void Nop() { }
public int Mult3(int x) { return x * 3; }
}
基准测试代码:先是空运行,然后调用 Foo.Nop()
/ Foo.Mult3()
10000000 次,采用三种不同的调用方式
- DelegateExec - 通过硬编码的匿名方法调用
- MethodInfoExec - 通过
MethodInfo.Invoke()
进行通用调用 - CompiledInfExec - 通过编译
MethodInfo
的匿名方法进行通用调用
const int _LoopCount = 9999999;
Type _tpFoo = typeof(Foo);
for (var i = _LoopCount; i-- > 0; ) ; // EmptyRun - #0
var anonymous = (Func<Foo, int, int>)((foo, x) => foo.Mult3(x));
for (var i = _LoopCount; i-- > 0; ) { var y = anonymous(_Foo, 3); } // DelegateExec Mult3 #1
var inf = _tpFoo.GetMethod("Mult3"); // MethodInfo of Mult3()
for (var i = _LoopCount; i-- > 0; ) { var y = (int)inf.Invoke(_Foo, new object[] { 3 }); } // MethodInfoExec Mult3 #2
anonymous = inf.MakeCompiledMethod<Func<Foo, int, int>>(); // inf compiled
for (var i = _LoopCount; i-- > 0; ) { var y = anonymous(_Foo, 3); } // CompiledInfExec Mult3 #3
var anonymous = (Action<Foo>)(foo => foo.Nop());
for (var i = _LoopCount; i-- > 0; ) anonymous(_Foo); // DelegateExec Nop #4
var inf = _tpFoo.GetMethod("Nop"); // MethodInfo of Nop()
for (var i = _LoopCount; i-- > 0; ) inf.Invoke(_Foo, null); // MethodInfoExec Nop #5
anonymous = inf.MakeCompiledMethod<Action<Foo>>(); // inf compiled
for (var i = _LoopCount; i-- > 0; ) anonymous(_Foo); // CompiledInfExec Nop #6
基准测试结果:执行 Foo.Nop()
/ Foo.Mult3()
10000000 次的毫秒数
#0 - Executing EmptyRun: 48 #1 - Executing DelegateExec Mult3: 432 #2 - Executing MethodInfoExec Mult3: 5706 #3 - Executing CompiledInfExec Mult3: 419 #4 - Executing DelegateExec Nop: 153 #5 - Executing MethodInfoExec Nop: 3030 #6 - Executing CompiledInfExec Nop: 275
您可以看到:使用反射 - “MethodInfoExec”最慢。使用编译后的 MethodInfo - “CompiledInfExec” - 速度提高了 10 倍。
应用于带有返回值的方法时,“CompiledInfExec”的速度比硬编码的匿名方法 - “DelegateExec” - 还要快(非常微小)! - 我不知道为什么。
(这里我可以结束了,推荐附加的源文件,我认为这不会是 CodeProject 上最糟糕的技巧)
System.Linq.Expressions
但我仍然想借此机会对所使用的技术进行一个简短、简化且不完全准确的介绍 - 即 System.Linq.Expressions.Expression
类及其派生类。
它们共同构成了一个模拟完整编程语言的系统 - 也许它模拟的是通用中间语言(CIL)本身。
为了让您对提供的强大功能有所想象,请看一些(方法)名称
Add(); AddAssign(); And(); AndAlso(); ArrayAccess(); ArrayIndex(); ArrayLength(); Assign(); Block(); Break(); Call(); Catch(); ClearDebugInfo(); Coalesce(); Condition(); Constant(); Continue(); Convert(); DebugInfo(); Decrement(); Default(); Divide(); DivideAssign(); Dynamic(); ElementInit(); Empty(); Equal(); ExclusiveOr(); Field(); Goto(); GreaterThan(); GreaterThanOrEqual(); IfThen(); IfThenElse(); Increment(); Invoke(); IsFalse(); IsTrue(); Lambda(); LeftShift(); LessThan(); LessThanOrEqual(); ListInit(); Loop(); MakeBinary(); MakeCatchBlock(); MakeDynamic(); MakeGoto(); MakeIndex(); MakeMemberAccess(); MakeTry(); MakeUnary(); Modulo(); Multiply(); Negate(); ewArrayBounds(); Not(); NotEqual(); Or(); OrElse(); Parameter(); PostIncrementAssign(); Power(); Property(); PropertyOrField(); ReferenceEqual(); ReferenceNotEqual(); Rethrow(); Return(); RightShift(); Subtract(); Switch(); Throw(); TryCatch(); TypeAs(); TypeEqual(); TypeIs(); UnaryPlus(); Variable();
您肯定能认出几乎所有 C# 关键字和运算符的对应项 - 这并非偶然。
不用担心:您不必处理所有这些,甚至不必理解它们 - 你们大多数人永远不会接触到这些。
但在某些特定情况下,提取其中一小部分并使其工作可能很有用。
例如,我的“表达式练习”(请参见“先看代码”列表)从任意 MethodInfo
获取信息,以构建一个调用由 MethodInfo
描述的方法的匿名方法,但无需反射,意味着:速度快。
正如基准测试所示,它的速度可能仍比硬编码的匿名方法慢 2 倍,但比使用反射快约 10 倍。
表达式的一些关注点
- 树形结构
Expression(及其派生类)实例旨在组合成任意大小的复杂树 - 表达式树 - 无构造函数
Expression 实例不能使用new
关键字创建。而是使用许多静态生成方法之一 - 如上列表所示(还有一些,以及许多重载)。 - 构建表达式树很慢
需要几毫秒,尤其是编译。快速的是使用结果委托。因此,对于一两次方法调用,这种方法非常不合适 - 最好还是坚持使用反射。
代码详解
第一个列表只说了一半。它的缺点是它返回一个任意的 Delegate
类型的匿名方法,而 Delegate
是抽象的,无法用于任何操作。您必须将其转换为具体的委托类型,例如 Action<T>
、Func<T1, TResult>
- 或其他任何类型。
问题显而易见:不知道具体的签名,您就无法调用方法(好吧 - 您可以 - 但不会成功)。
为此,我将核心方法封装在另一个方法中,该方法仅负责从抽象委托转换为具体委托,并特别确保在失败时引发有意义的 InvalidCastException
。
信不信由你:那个封装方法比被封装的核心方法更难开发 - 请看整体
public static Delegate MakeCompiledMethod(this MethodInfo mtd) {
if (mtd == null) throw new ArgumentNullException("ReflectionX.MakeCompiledMethod(MethodInfo mtd): mtd mustn't be null");
var prams = mtd.GetParameters().Select(p => Expression.Parameter(p.ParameterType, p.Name)).ToList();
Expression methodCall;
if (mtd.IsStatic) methodCall = Expression.Call(instance: null, method: mtd, arguments: prams);
else { // on instance-Methods the ownerInstance must be included
var ownerInstance = Expression.Variable(mtd.DeclaringType, "ownerInstance");
methodCall = Expression.Call(ownerInstance, mtd, prams);
prams.Insert(0, ownerInstance);
}
return Expression.Lambda(methodCall, false, prams).Compile();
}
/// <summary> generates an anonymous Method to call the MethodInfo-method nearly as fast as direct calls.
/// Eg an (Instance-) MethodInfo "bool StringCollection.Contains(<string>)" would compile to "Func<StringCollection, string, bool>".
/// On the other hand "static bool String.IsNullOrEmpty(<string>)" would compile to "Func<string, bool>".
/// You must specify the correct Delegate-Typparam T to make this method work.</summary>
public static T MakeCompiledMethod<T>(this MethodInfo mtd) {
object dlg = mtd.MakeCompiledMethod();
try { return (T)dlg; }
catch (InvalidCastException x) {//build a proper Exception is more difficult than the function itself
var stpParams = string.Join(", ", mtd.GetParameters().Select(p => p.ParameterType.FriendlyName()));
var sMtd = string.Format("{0} {1}.{2}({3})", mtd.ReturnType.FriendlyName(), mtd.DeclaringType.FriendlyName(), mtd.Name, stpParams);
var sIsStatic = mtd.IsStatic ? "static " : "";
var msg = @"The given MethodInfo ""{0}{1}"" would compile to ""{2}"", but my TypeParameter requests ""{3}""";
msg = string.Format(msg, sIsStatic, sMtd, dlg.GetType().FriendlyName(), typeof(T).FriendlyName());
throw new InvalidOperationException("ReflectionX.MakeCompiledMethod<T>(MethodInfo mtd): " + msg, x);
}
}
/// <summary> a helpful helper </summary>
public static string FriendlyName(this Type tp) {
var rgx = new Regex(@"(?<=(^|[\.\+]))\b\D\w+($|[,`])|\[\[|\]\]");
return string.Concat(rgx.Matches(tp.FullName).Cast<Match>()).Replace("`[[", "<").Replace(",]]", ">").Replace(",", ", ");
}
但我只解释核心部分(第 1-12 行)- 作为“LINQ 表达式练习” - 以识别上述“关注点”中提到的内容。
#3:获取方法参数并将其转换为 ParameterExpression
列表
#4:methodCall
表达式必须预先声明 - 因为有两种不同的选项来构建它
#5:对于 static
方法,第一个参数 - “instance”必须是 null
#7:对于实例方法,我们需要一个额外的表达式来限定实例方法的 ownerInstance
#8:将参数传递给 .Call
调用
#9:我们的 prams
参数列表将在构建可编译的 Lambda 表达式时重用。由于实例方法需要额外的 ownerInstance
信息,我们必须将其插入到我们的参数列表的第一个位置。
#11:创建一个 Lambda 表达式(无论这意味着什么 - 但它是可编译的),编译并返回它。
您可以看到:我们构建了一个小树
根是 Lambda
,它包含一个 methodCall
表达式和一个 ParameterExpression
列表methodCall
包含一个作为实例的 Variable
表达式,以及一些 Param 表达式
Lambda - pram1, pram2, ... \ MethodCall - pram2, pram3, ... \ ownerInstance (==pram1)
当您使用断点等检查附加的示例代码时,这可能会更清楚。
结论
我最初的目标只是分享我精美的 MethodInfo 编译器。但随后我想,了解其背后的技术至少能有所帮助。
如果您想为其他类似问题开发自己的解决方案,这可以提供一个“切入点”。
例如,您可以将我的“MethodInfo 编译器”作为一种模板,为 Reflection.FieldInfo
或 .PropertyInfo
开发类似的东西
请注意,核心方法非常稳定:只要存在有效的 MethodInfo
(不存在无效的 MethodInfo
),它就包含创建有效的 LINQ 表达式所需的所有数据。
抱歉 - 小修改:存在 ref
或 out
参数可能会导致问题(但懒惰使我无法详细检查)。
但可以肯定的是:任何类型的 Action
或 Func
的方法签名 MethodInfo
都可以成功编译。
另一个值得关注的点是,**私有** 类成员也可以通过 Reflection
访问 - 借助 LINQ.Expressions
,这可以非常高效。