【问题标题】:Dynamically create a class by interface通过接口动态创建类
【发布时间】:2016-11-15 16:13:06
【问题描述】:

当我能够动态生成方法时,我对.Net Expressions 有一些经验。很好,很好。

但是现在我需要生成一个完整的类,而且似乎唯一的方法是发射整个 IL,这是完全不可接受的(不可能支持)。

假设我们有以下界面:

public interface IFoo
{
    [Description("5")]
    int Bar();
    [Description("true")]
    bool Baz();
}

应该转换成:

public class Foo : IFoo
{
    public int Bar() => 5;
    public bool Baz() => true;
}

我怎样才能实现它?如果没有 3rd 方工具和库,它甚至可能吗?我知道 GitHub 上有很多有用的工具,但我真的不想导入整个 MVVM 框架来生成一些代码。

如果我可以使用Expressions,并使用我已经用它生成的方法创建一个类。但现在我不知道该怎么做。

【问题讨论】:

  • 您想在编译时还是运行时生成类?
  • 使用 .NET CodeDOM。 Waaay 比在 Reflection Emit 中胡闹要容易:)
  • @SamAxe 在运行时,当然。我可以使用 T4 在编译时生成类:)
  • @AlexZhukovskiy 我见过有人用 Roslyn 做类似的事情,但这只是一个模糊的建议;你现在有一个正确的答案,所以我不会再浪费你的时间了
  • @TheBeardedLlama 最后你的答案似乎是最好的,请看下面我的答案。几年后,我找到了适合我的需求。

标签: c# .net reflection code-generation expression-trees


【解决方案1】:

首先,由于您正在处理远程处理,我不得不提一下,这是 .NET 最初从头开始设计支持的(从 .NET 作为 COM 2.0 的根源回来)。您最直接的解决方案是实现一个透明的远程代理 - 只需创建您自己的(可能是通用的)类派生自 System.Runtime.Remoting.Proxies.RealProxy,然后您可以通过覆盖 Invoke 方法提供实现所需任何功能所需的所有逻辑。使用GetTransparentProxy,您将获得实现接口的代理,一切顺利。

显然,这在运行时,每次调用期间都会产生成本。但是,与您进行任何 I/O 的事实相比,它通常完全不重要,尤其是在您处理网络时。事实上,除非你处于一个紧密的循环中,否则即使不进行 I/O 也是相当不重要的——只有性能测试才能真正判断你是否可以接受成本。

如果您真的想预生成所有方法体,而不是在运行时保持逻辑动态,您可以利用LambdaExpression 为您提供CompileToMethod 的事实。与Compile 不同,您没有可以直接调用的漂亮小委托,但它为您提供了使用 lambda 表达式显式构建方法主体的选项 - 这反过来又允许您创建整个类而无需诉诸委托调用。

一个完整(但简单)的例子:

void Main()
{
  var ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("TestAssembly"), AssemblyBuilderAccess.Run);
  var mb = ab.DefineDynamicModule("Test");

  var tb = mb.DefineType("Foo");
  tb.AddInterfaceImplementation(typeof(IFoo));

  foreach (var imethod in typeof(IFoo).GetMethods())
  {
    var valueString = ((DescriptionAttribute)imethod.GetCustomAttribute(typeof(DescriptionAttribute))).Description;

    var method = 
      tb.DefineMethod
      (
        "@@" + imethod.Name, 
        MethodAttributes.Private | MethodAttributes.Static, 
        imethod.ReturnType,
        new [] { tb }
      );

    // Needless to say, I'm making a lot of assumptions here :)
    var thisParameter = Expression.Parameter(typeof(IFoo), "this");

    var bodyExpression =
      Expression.Lambda
      (
        Expression.Constant
        (
          Convert.ChangeType(valueString, imethod.ReturnType)
        ),
        thisParameter
      );

    bodyExpression.CompileToMethod(method);

    var stub =
      tb.DefineMethod(imethod.Name, MethodAttributes.Public | MethodAttributes.Virtual, imethod.ReturnType, new Type[0]);

    var il = stub.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.EmitCall(OpCodes.Call, method, null);
    il.Emit(OpCodes.Ret);

    tb.DefineMethodOverride(stub, imethod);
  }

  var fooType = tb.CreateType();
  var ifoo = (IFoo)Activator.CreateInstance(fooType);

  Console.WriteLine(ifoo.Bar()); // 5
  Console.WriteLine(ifoo.Baz()); // True
}

public interface IFoo
{
    [Description("5")]
    int Bar();
    [Description("true")]
    bool Baz();
}

如果您曾经使用过 .NET emits,那么这应该非常简单。我们定义了一个动态程序集、模块、类型(理想情况下,您希望在一个动态程序集中一次定义所有类型)。棘手的部分是Lambda.CompileToMethod 只支持静态方法,所以我们需要作弊。首先,我们创建一个以this 作为参数的静态方法,并在那里编译 lamdba 表达式。然后,我们创建一个方法存根——一个简单的 IL 片段,确保我们的静态方法被正确调用。最后,我们将接口方法绑定到存根。

在我的示例中,我假设一个无参数方法,但只要确保LambdaExpression 使用与接口方法完全相同的类型,存根就像执行所有Ldargs 一样简单在一个序列中,单个Call 和单个Ret。如果你的真实代码(在静态方法中)足够短,它通常会被内联。并且由于this 和其他任何参数一样,如果您喜欢冒险,您可以将生成的方法的方法体直接放入虚拟方法中 - 请注意您需要这样做不过,两次通过。

【讨论】:

  • 出色的工作,谢谢。但是有可能完全删除这个 ldarg-red 代理吗?我正在努力,但我想知道你的意见。是因为CompileToMethod 仅用于静态?
  • @AlexZhukovskiy 你可以直接从生成的方法中取出IL代码,放到另一个方法中。但是,这意味着您需要先创建类型和静态方法才能到达 IL/方法体,然后再创建不同的类型(派生类型,如果您需要保留 this)将 IL 放入。而且我也不是 100% 肯定那会是多么安全。您也可以直接使用LambdaCompiler,尽管这可能是个坏主意。你为什么要避免它?您只需要一个辅助方法来创建您需要的任何类型的存根,并保持它有据可查(和测试覆盖)。
  • 你又是对的。我现在正在研究这种方法。完成后,我会发布一个指向要点或其他内容的链接。
  • Here 是我的实现,正如我所承诺的。我使用代码生成来为 WCF REST 服务生成客户端。在实现中,我们可以使用参数调用 HttpClient ,仅此而已。使用起来似乎很简单。这是一个概念,但很好。
  • @AlexZhukovskiy 好吧,IL 中操作码的选择有好有坏。想想你的指令是如何布局的——0x0E04 代表 ldarg_s 4 然后 0x00 三次代表三个 NOP。节省你的是 LSB 是第一个字节,其余的都是 NOP。所以你是对的,在你的情况下,它不会产生一个无效的程序——靠运气。但无论如何要解决这个问题,只是为了提醒自己,您为 Emit 选择的方法重载很重要 - 在您的情况下,您实际上是在不知情的情况下发出了四个操作码,而不是发出一个操作码。
【解决方案2】:

您可以使用 CodeDOM 和 Emit。但是,我认为这不值得。

对于类似的事情,我正在使用以下库: http://www.castleproject.org/projects/dynamicproxy/

即使目标类不可用,它也能创建代理类(你必须拦截所有方法)。

【讨论】:

    【解决方案3】:

    我实际上目前正在使用CodeGeneration.Roslyn 包,它允许您集成到 MSBuild 管道并构建东西。例如,查看我的REST swaggersolidity -> C# codegen

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-21
      • 2021-11-10
      • 2013-10-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多