使用类似于装饰器的方式在C#中生成代码的最佳方法是什么?

17

假设我有一个经常重复的模式。类似于:

static class C {
  [DllImport("mydll")]
  private static extern uint MyNativeCall1(Action a);
  public static uint MyWrapper1(Action a) {
    // Do something
    return MyNativeCall1(a);
  }

  [DllImport("mydll")]
  private static extern uint MyNativeCall2(Action a);
  public static uint MyWrapper2(Action a) {
    // Do something
    return MyNativeCall2(a);
  }

  //...

  [DllImport("mydll")]
  private static extern uint MyNativeCallN(Action a);
  public static uint MyWrapperN(Action a) {
    // Do something
    return MyNativeCallN(a);
  }
}
所有这些中唯一不同的是本机函数和包装器方法的名称。有没有办法通过类似于装饰器的东西来生成它们?起初我以为 C# 属性就像装饰器一样。也就是说,我可以通过类似于 [GenerateScaffolding("MyNativeCall1")] 的方式生成代码。但是它似乎更像注释,实例化一个保存某些元数据的类。C# 也没有宏。那么有没有办法做到这一点呢?
需要注意以下几点:
- 包装器方法具有额外的代码;它们不仅仅是调用本机函数。 - 生成的代码应该与类内部的其他现有代码交错,而不是生成类文件本身;类似于装饰器或 C/C++ 宏之类的东西。 - 方法不应依赖于任何特定的 IDE。特别地,我不使用 Visual Studio。

4
你可以使用 T4 模板 吗? - CodingIntrigue
为什么不使用 DllImportEntryPoint 属性来指定被调用方法的不同名称呢? - Shlomi Borovitz
[DllImport("mydll", EntryPoint = "MyNativeCall1")] public static extern uint MyWrapper1(Action a); - Shlomi Borovitz
@ShlomiBorovitz 因为我可能不仅需要在包装方法中调用本地函数,还有一些我忘记让它更明显的东西。 - SaldaVonSchwartz
如果您事先有方法列表,是否考虑过反射或自己发出IL代码?这是相当深奥和棘手的东西,但可能会很有效。 - rhughes
你可以考虑使用另一种语言(如Nemerle)编写这样的类,它包含模板。语法类似,并且几乎肯定会在您的平台上得到支持。 - Magus
5个回答

10

参考这篇MSDN文章中对T4模板的描述,您可以这样实现:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>
static class C {
<#  
    int N = 15;
    for(int i=0; i<N; i++)
    { #>
    [DllImport("mydll")]
    private static extern uint MyNativeCall<#= i #>(Action a);
    public static uint MyWrapper<%#= i #>(Action a) {
        return MyNativeCall<#= i #>(a);
    }
<# }  #>
}

如果没有更好的方法,我会采用这种方式。但是这些模板看起来有点丑陋。等一下我会读更多关于它们的内容,但你知道吗?A:它们可以与类中余下的代码交错使用吗?比如说,不仅要生成文件本身,而且要在我正在工作的文件中插入文本(更像C/C++宏) B:这只是VS的功能吗?因为我没有使用VS。我应该将这些细节添加到我的问题中。 - SaldaVonSchwartz
@SaldaVonSchwartz 你可以在T4中编写基本定义,然后使用include file语法来包含任何现有的代码。是的,这是Visual Studio的一个功能,在将代码发送到编译器之前生成代码-因此它不适用于您的情况。听起来你只想要一个预构建的shell/batch/其他脚本,但这不是C#的一部分,所以它始终会依赖于你正在使用的任何IDE。 - CodingIntrigue
我实际上想要的是,除了装饰器之外,至少有像C预处理器这样的东西,任何编译器都能识别 :/ - SaldaVonSchwartz
@SaldaVonSchwartz 抱歉,在 C# 语言规范中没有类似的内容。 - CodingIntrigue
C# 没有预处理器。你需要依赖实际编译器之外的工具。.tt 系统可以在 Visual Studio 之外使用,但通常需要一些手动工作。 - Lasse V. Karlsen

5
你不需要IDE在运行时生成和处理模板,但是你必须创建自己的指令处理器和/或托管
Engine engine = new Engine();

//read the text template
string input = File.ReadAllText(templateFileName);

//transform the text template
string output = engine.ProcessTemplate(input, host);

在您的模板中,您可以将模板语言与C#代码混合使用(生成示例HTML):
<table>
  <# for (int i = 1; i <= 10; i++)
     { #>
        <tr><td>Test name <#= i #> </td>
          <td>Test value <#= i * i #> </td> 
        </tr>
  <# } #>
</table>

这是我如何使用T4从文本文件生成各种状态机的方式。
你甚至可以为C#类在运行时生成源代码,编译和加载并从程序中执行。
如果您将所有这些技术结合起来,甚至包括可组合部件,例如MEF,我相信您将能够实现所需的目标。 更新 没有MEF,但仍然需要IDE预处理模板。 由于我没有您的DLL,因此无法给出确切答案,但也许这会有所帮助。
给定此模板(ExtDll.tt):
<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="mscorlib" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<# 
            var extraCodeArray = new[]
                             { string.Empty,
                                 "var localVar = 1;",
                                 "var localVar = 2;",
                                 "var localVar = 3;",
                                 "var localVar = 4;",
                                 "var localVar = 5;",
                                 "var localVar = 6;",
                                 "var localVar = 7;",
                                 "var localVar = 8;",
                                 "var localVar = 9;",
                                 "var localVar = 10;",
                             };

#>
using System;
static class C{
<# for (int i = 1; i <= 10; i++)
       { #>
       public static double MyWrapper<#= i #>(Func<int,double> a) {
         <#= extraCodeArray[i] #>
       return a.Invoke(localVar);
       }
    <# } #>
}

和这个程序:

using System;
using System.Linq;

namespace ConsoleApplication4
{
    using System.CodeDom.Compiler;
    using System.Reflection;

    using Microsoft.CSharp;

    class Program
    {
        static void Main(string[] args)
        {
            ExtDll code = new ExtDll();
            string source = code.TransformText();
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters()
                                            {
                                                GenerateInMemory = true,
                                                GenerateExecutable = false
                                            };
            parameters.ReferencedAssemblies.AddRange(
                new[]
                {
                    "System.Core.dll",
                    "mscorlib.dll"
                });
            CompilerResults results = provider.CompileAssemblyFromSource(parameters, source);
            if (results.Errors.HasErrors)
            {
                var errorString = String.Join("\n", results.Errors.Cast<CompilerError>().Select(error => String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText)));

                throw new InvalidOperationException(errorString);
            }
            Assembly assembly = results.CompiledAssembly;
            Func<int,double> squareRoot = (i) => { return Math.Sqrt(i); };
            Type type = assembly.GetType("C");
            //object instance = Activator.CreateInstance(type);
            MethodInfo method = type.GetMethod("MyWrapper4");
            Console.WriteLine(method.Invoke(null, new object[]{squareRoot})); 
        }
    }

}

它会打印出2,因为它是4的平方根。

更新2

在略微修改第二个链接中的CustomCmdLineHost后:

    public IList<string> StandardAssemblyReferences
    {
        get
        {
            return new string[]
            {
                //If this host searches standard paths and the GAC,
                //we can specify the assembly name like this.
                //---------------------------------------------------------
                //"System"

                //Because this host only resolves assemblies from the 
                //fully qualified path and name of the assembly,
                //this is a quick way to get the code to give us the
                //fully qualified path and name of the System assembly.
                //---------------------------------------------------------
                typeof(System.Uri).Assembly.Location,
                typeof(System.Linq.Enumerable).Assembly.Location
            };
        }
    }

示例程序不再需要集成开发环境:

        var host = new CustomCmdLineHost();
        host.TemplateFileValue = "ExtDll.tt";
        Engine engine = new Engine();
        string input = File.ReadAllText("ExtDll.tt");
        string source = engine.ProcessTemplate(input, host);
        if (host.Errors.HasErrors)
        {
            var errorString = String.Join("\n", host.Errors.Cast<CompilerError>().Select(error => String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText)));

            throw new InvalidOperationException(errorString);            
        }

        CSharpCodeProvider provider = new CSharpCodeProvider();
... rest of the code as before

我希望这能满足您的需求。 更新3 如果您进一步修改示例主机如下:
internal string TemplateFileValue = Path.Combine(
    AppDomain.CurrentDomain.BaseDirectory,"CustomCmdLineHost.tt");

然后您可以避免指定模板文件名,直接使用内存处理:
var host = new CustomCmdLineHost();
Engine engine = new Engine();
string input = File.ReadAllText("ExtDll.tt");
string source = engine.ProcessTemplate(input, host);

享受并友好地标记您喜欢的答案。


4

Visual Studio中的代码片段只用于此目的。

请查看这篇MSDN文章,教你如何创建自己的自定义代码片段。 http://msdn.microsoft.com/en-us/library/ms165394.aspx

编辑:

好的..我看到你编辑了你的问题,并且添加了你不在寻找一个IDE特定的功能。所以我的回复现在变得无关紧要了。尽管如此,对于那些正在寻找内置Visual Studio功能的人来说,它可能是有用的。


1
我想我会用悲伤的语气回答:不行。在C#中没有办法做到这一点。也就是说,语言本身和框架中都没有相关的内容。
然而,如果在Visual Studio上,可以使用模板,就像RGraham和Pradeep指出的那样;其他IDE可能也有不同的工具/功能来完成这个任务。但是,C#本身没有预处理器或装饰器之类的东西。

2
不仅仅是Visual Studio,任何支持预/后构建命令(因此能够执行bash/shell)的IDE都可以。任何值得一试的IDE都应该支持这些功能。 - CodingIntrigue

0

这里还有另一种选择,加入了一点PostSharp的AOP魔法:

using System;
using System.Reflection;

using PostSharp.Aspects;

internal class Program
{
    #region Methods

    private static void Main(string[] args)
    {
        Action action = () => { Console.WriteLine("Action called."); };
        Console.WriteLine(C.MyWrapper1(action));
    }

    #endregion
}

[Scaffolding(AttributeTargetMembers = "MyWrapper*")]
internal static class C
{
    #region Public Methods and Operators

    public static uint MyWrapper1(Action a)
    {
        DoSomething1();
        return Stub(a);
    }

    #endregion

    #region Methods

    private static void DoSomething1() { Console.WriteLine("DoSomething1"); }

    private static uint Stub(Action a) { return 0; }

    #endregion
}

internal static class ExternalStubClass
{
    #region Public Methods and Operators

    public static uint Stub(Action a)
    {
        a.Invoke();
        return 5;
    }

    #endregion
}

[Serializable]
public class ScaffoldingAttribute : OnMethodBoundaryAspect
{
    #region Fields

    private MethodInfo doSomethingInfo;

    #endregion

    #region Public Methods and Operators

    public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
    {
        Type type = typeof(C);
        this.doSomethingInfo = type.GetMethod(method.Name.Replace("MyWrapper", "DoSomething"), BindingFlags.NonPublic | BindingFlags.Static);
    }

    public override void OnEntry(MethodExecutionArgs args)
    {
        this.doSomethingInfo.Invoke(null, null);
        args.ReturnValue = ExternalStubClass.Stub(args.Arguments[0] as Action);
        args.FlowBehavior = FlowBehavior.Return;
    }

    #endregion
}

这个示例基本上展示了如何通过另一个具有相同名称的类中的方法动态地覆盖一个类中的方法。原始的C存根永远不会被调用。您必须遵守一些命名约定。
如果您将其与动态绑定本机DLL方法 结合使用, 您将实现所需的脚手架,我认为。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接