代码生成器或T4模板,它们真的很邪恶吗?

时间:2009-02-07 18:24:23

标签: c# generics code-generation t4

我听说有人说不应该使用代码生成器和T4模板。这背后的逻辑是,如果您使用生成器生成代码,那么通过泛型和模板构建代码有一种更好的更有效的方法。

虽然我略微同意上述声明,但我还没有真正找到有效的方法来构建模板,例如可以说实例化它们自己。换句话说,我永远做不到:

return new T();

此外,如果我想根据数据库值生成代码,我发现将Microsoft.SqlServer.Management.SMO与T4模板结合使用可以很好地生成大量代码,而无需复制/粘贴或使用resharper。< / p>

我在Generics中发现的许多问题都是令我震惊的是,有很多开发人员不了解它们。当我检查泛型的解决方案时,有时它会变得复杂,因为C#声明你不能做一些看起来合乎逻辑的事情。

你有什么想法?你喜欢建造发电机,还是喜欢使用仿制药?此外,仿制药可以走多远?我对泛型有很多了解,但是我总是遇到陷阱和陷阱导致我使用T4模板。

处理需要大量灵活性的方案的更合适的方法是什么?哦,作为这个问题的一个奖励,C#和Generics有哪些好的资源?

15 个答案:

答案 0 :(得分:15)

你可以做新的T();如果你这样做

public class Meh<T>
  where T : new()
{
  public static T CreateOne()
  {
    return new T();
  }
}

代码生成器。我每天都使用一个没有任何问题。我现在正在使用一个: - )

泛型解决了一个问题,代码生成器解决了另一个问题。例如,使用UML编辑器创建业务模型,然后使用持久性代码生成类,因为我一直使用this tool无法使用泛型来实现,因为每个持久化类都完全不同。

关于仿制药的良好来源。最好的当然是Jon Skeet's book! : - )

答案 1 :(得分:13)

作为T4的创始人,我不得不多次捍卫这个问题: - )

我的信念是,最好的代码生成是使用可重用库生成等价值的一步。

正如许多其他人所说,维护DRY的关键概念永远不会手动更改生成的代码,而是保留在源元数据更改或您在代码生成器中发现错误时重新生成的能力。此时,生成的代码具有目标代码的许多特性,并且您不会遇到复制/粘贴类型问题。

一般来说,生成参数化代码生成器(特别是基于模板的系统)要比正确设计高质量的基础库(将使用成本降低到同一水平)要少得多,因此它很快从一致性中获取价值并消除重复错误的方法。

但是,我仍然相信完成的系统通常会通过减少总代码来改进。如果不出意外,其内存占用率几乎总是会小得多(尽管人们倾向于认为泛型在这方面是免费的,但他们肯定不会这样做。)

如果您已经使用代码生成器实现了某些价值,那么这通常会花费您一些时间或金钱或善意来投资从生成的代码库中获取库。然后,您可以逐步重新设计代码生成器以定位新库,并希望生成更少的代码。冲洗并重复。

我已经提出了一个有趣的对立点,就是这个主题,丰富,复杂的参数库在学习曲线方面并不是最简单的,特别是那些没有深入沉浸在平台中的人。坚持将代码生成到更简单的基本框架上可以产生冗长的代码,但它通常非常简单易读。

当然,如果您的生成器中存在大量差异和极其丰富的参数化,那么您可能只是为了模板的复杂性而牺牲产品的复杂性。这是一条很容易滑入的路径,并且可以让维护变得非常令人头疼 - 请注意这一点。

答案 2 :(得分:8)

生成代码不是邪恶的,它没有味道!关键是在合适的时间生成正确的代码。我认为T4很棒 - 我偶尔只使用它,但是当我这样做时非常有帮助。无条件地说,生成代码是坏的无条件疯狂!

答案 3 :(得分:6)

在我看来代码生成器很好,只要代码生成是正常构建过程的一部分,而不是你运行一次然后保持其输出。我添加了这个警告,因为如果只使用代码生成器一次并丢弃创建它的数据,那么您只是自动创建一个大规模的DRY违规和维护问题;而每次有效地生成代码意味着无论你用什么做生成都是真正的源代码,生成的文件只是你应该忽略的中间编译阶段。

Lex和yacc是允许您以有效方式指定功能并从中生成有效代码的工具的经典示例。尝试手工完成工作将延长您的开发时间,并可能产生效率较低且可读性较低的代码。虽然你当然可以将lex和yacc之类的东西直接合并到代码中并在运行时而不是在编译时完成它们的工作,但这肯定会增加代码的复杂性并减慢它的速度。如果你真的需要在运行时更改你的规范,那么它可能是值得的,但是在大多数正常情况下使用lex / yacc在编译时为你生成代码是一个很大的胜利。

答案 4 :(得分:6)

如果没有代码生成,Visual Studio 2010中很大一部分内容将无法实现。实体框架是不可能的。将控件拖放到表单上的简单操作是不可能的,Linq也不可能。要说不应该使用代码生成是很奇怪的,因为许多人在不考虑它的情况下使用它。

答案 5 :(得分:4)

也许它有点苛刻,但对我来说代码生成气味。

使用代码生成意味着有许多潜在的共同原则可以用“不要重复自己”的方式表达。它可能需要更长的时间,但是当你最终只有包含真正改变的位的类时,基于包含机制的基础结构,它会令人满意。

关于泛型......不,我没有太多问题。目前唯一不起作用的是说

List<Animal> a = new List<Animal>();
List<object> o = a;

但即便如此,在下一版C#中也是如此。

答案 6 :(得分:2)

更多代码意味着更复杂。更复杂意味着更多的地方可以隐藏错误,这意味着更长的修复周期,这反过来意味着整个项目的成本更高。

只要有可能,我更愿意尽量减少提供等效功能的代码量;理想情况下使用动态(编程)方法而不是代码生成。反思,属性,方面和泛型为干战策略提供了许多选择,将生成作为最后的手段。

答案 7 :(得分:2)

代码生成对我来说是语言,框架等中发现的许多问题的解决方法。它们本身并不邪恶,我会说发布语言(C#)和框架是非常非常糟糕的(即邪恶的)强迫你复制和粘贴(交换属性,触发事件,缺少宏)或使用魔法数字(wpf绑定)。

所以,我哭了,但我用它们,因为我必须这样做。

答案 8 :(得分:2)

我已经将T4用于代码生成以及泛型。两者都很好,有利有弊,适合不同的目的。

就我而言,我使用T4根据数据库模式生成实体,DAL和BLL。但是,DAL和BLL引用了我构建的迷你ORM,基于泛型和反射。所以我认为你可以并排使用它们,只要你保持控制并保持小而简单。

T4生成静态代码,而Generics是动态的。如果你使用泛型,你使用反射,据说它比“硬编码”解决方案性能更差。当然,您可以缓存反射结果。

关于“return new T();”,我使用这样的动态方法:

public class ObjectCreateMethod
    {
    delegate object MethodInvoker();
    MethodInvoker methodHandler = null;

    public ObjectCreateMethod(Type type)
    {
        CreateMethod(type.GetConstructor(Type.EmptyTypes));
    }

    public ObjectCreateMethod(ConstructorInfo target)
    {
        CreateMethod(target);
    }

    void CreateMethod(ConstructorInfo target)
    {
        DynamicMethod dynamic = new DynamicMethod(string.Empty,
                    typeof(object),
                    new Type[0],
                    target.DeclaringType);
        ILGenerator il = dynamic.GetILGenerator();
        il.DeclareLocal(target.DeclaringType);
        il.Emit(OpCodes.Newobj, target);
        il.Emit(OpCodes.Stloc_0);
        il.Emit(OpCodes.Ldloc_0);
        il.Emit(OpCodes.Ret);

        methodHandler = (MethodInvoker)dynamic.CreateDelegate(typeof(MethodInvoker));
    }

    public object CreateInstance()
    {
        return methodHandler();
    }
}

然后,我称之为:

ObjectCreateMethod _MetodoDinamico = new ObjectCreateMethod(info.PropertyType);
object _nuevaEntidad = _MetodoDinamico.CreateInstance();

答案 9 :(得分:1)

泛型和代码生成是两回事。在某些情况下,您可以使用泛型而不是代码生成,对于那些我认为应该使用的代码。对于其他情况,代码生成是一个强大的工具。

对于您只需要根据某些数据输入生成代码的所有情况,代码生成是可行的方法。最明显的,但绝不是唯一的例子是Visual Studio中的表单编辑器。这里输入是设计器数据,输出是代码。在这种情况下,泛型实际上根本没有任何帮助,但是VS很简单地生成基于GUI布局的代码。

答案 10 :(得分:1)

代码生成器可被视为代码气味,表明目标语言中存在缺陷或缺乏功能。

例如,虽然这里曾经说过“持久存在的对象无法推广”,但最好将其视为“C#中的对象自动持久化其数据不能在C#中推广”,因为我肯定可以通过各种方法在Python中使用。

但是,Python方法可以通过使用operator [](method_name as string)在静态语言中进行模拟,根据需要,它可以返回一个函子或一个字符串。不幸的是,该解决方案并不总是适用,并且返回仿函数可能会很不方便。

我要说的是代码生成器通过为手头的特定问题提供更方便的专用语法来指示所选语言中的缺陷。

答案 11 :(得分:1)

生成代码的复制/粘贴类型(如ORMs make)也非常有用......

您可以创建数据库,然后让ORM生成以您喜欢的语言表达的数据库定义的副本。

当您更改原始定义(数据库),按下编译并且ORM(如果您有一个好的)可以重新生成定义的副本时,优势就出现了。现在,编译器类型检查器可以检查对数据库的所有引用,并且当您使用不再存在的表或列时,代码将无法编译。

想一想:如果我在我的代码中多次调用一个方法,我不是指我最初给这个方法的名字吗?我一遍又一遍地重复这个名字......语言设计师认识到了这个问题,并提出了“类型安全”作为解决方案。不删除副本(如DRY建议我们应该这样做),而是检查它们的正确性。

ORM生成的代码在引用表名和列名时带来了相同的解决方案。不删除副本/引用,而是将数据库定义引入您的(类型安全)语言,您可以在其中引用类和属性。与编译器类型检查一起,这以类似的方式解决了类似的问题:当您引用过时或拼写错误的表(类)或列(属性)时,保证编译时错误而不是运行时错误。

答案 12 :(得分:1)

报价: 我还没有真正找到有效的方法来构建模板,例如可以说实例化自己。换句话说,我永远做不到:

返回新的T();

public abstract class MehBase<TSelf, TParam1, TParam2>
    where TSelf : MehBase<TSelf, TParam1, TParam2>, new()
{
    public static TSelf CreateOne()
    {
        return new TSelf();
    }
}

public class Meh<TParam1, TParam2> : MehBase<Meh<TParam1, TParam2>, TParam1, TParam2>
{
    public void Proof()
    {
        Meh<TParam1, TParam2> instanceOfSelf1 = Meh<TParam1, TParam2>.CreateOne();
        Meh<int, string> instanceOfSelf2 = Meh<int, string>.CreateOne();
    }
} 

答案 13 :(得分:0)

代码生成,如泛型,模板和其他此类快捷方式,是一个强大的工具。和大多数强大的工具一样,它扩大了用户的善意和恶意 - 他们无法分开。

因此,如果你彻底了解你的代码生成器,预测它将产生的一切,为什么,并打算出于正当理由这样做,然后就可以了。但是不要使用它(或任何其他技术)让你经过一个你不确定你要去哪里或者如何到达那里的地方。

有些人认为,如果你解决了当前的问题并实施了一些行为,那你就是金色的。对于下一位开发人员(可能是您自己)而言,您留下多少瑕疵和不透明度并不总是很明显。

答案 14 :(得分:-1)

为什么能够真正快速地复制/粘贴,使其更容易被接受?

这是我能看到的代码生成的唯一理由。

即使生成器提供了您所需的所有灵活性,您仍然必须学习如何使用这种灵活性 - 这是另一层学习和测试所需。

即使它在零时间内运行,它仍会使代码膨胀。

我推出了自己的数据访问类。它知道关于连接,事务,存储过程参数等的一切,我只需要编写一次所有ADO.NET的东西。

现在已经有很长时间了,因为我不得不用连接对象写任何内容(或者甚至看看),我很难记住语法。