65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.45/5 (12投票s)

2017 年 10 月 26 日

CPOL

5分钟阅读

viewsIcon

19813

downloadIcon

143

通过 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 个方法(voidint

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 表达式所需的所有数据。
抱歉 - 小修改:存在 refout 参数可能会导致问题(但懒惰使我无法详细检查)。
但可以肯定的是:任何类型的 ActionFunc 的方法签名 MethodInfo 都可以成功编译。

另一个值得关注的点是,**私有** 类成员也可以通过 Reflection 访问 - 借助 LINQ.Expressions,这可以非常高效。

© . All rights reserved.