代码生成器或 T4 模板,它们真的那么恶劣吗?

10

我听到有人说过,代码生成器和T4模板不应该被使用。背后的逻辑是,如果你正在使用生成器生成代码,那么通过泛型和模板化构建代码会更好、更有效率。

尽管我在某种程度上同意上述观点,但我并没有找到有效的方法来构建模板,例如能够自我实例化的模板。换句话说,我永远无法做到:

return new T();

此外,如果我想根据数据库值生成代码,我发现使用 Microsoft.SqlServer.Management.SMO 结合 T4 模板可以轻松生成大量代码,而无需复制/粘贴或使用 Resharper。
我发现许多泛型问题是因为惊讶地有很多开发人员不理解它们。当我检查泛型解决方案时,有时会变得很复杂,因为 C# 声明您不能做在我的脑海中似乎很合理的事情。
你的想法是什么?你更喜欢构建生成器还是使用泛型?另外,泛型能达到多远?我对泛型有相当多的了解,但总是遇到陷阱和问题,这使我不得不使用 T4 模板。
在需要大量灵活性的情况下,处理方式应该是什么?哦,作为这个问题的奖励,C# 和泛型的好资源是什么?

评论我的帖子,Activator.CreateInstance<T>(); 解决了这个问题(有点儿)... - jwendl
2
泛型和T4模板有不同的用途... - Andersson Melo
唯一正确的生成代码的方法是构建所需语言的AST,并创建一个算法将AST转储为实际代码,这样您甚至可以实现格式化,也可以很容易地将AST转换为其他语言的AST,因为大多数语言非常相似。T4模板是对那些编写/使用过解析器的人的一种侮辱。 - oren revenge
15个回答

16

如果您这样做,可以使用 new T();

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

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

泛型解决了一个问题,而代码生成器解决了另一个问题。例如,使用UML编辑器创建业务模型,然后生成带有持久化代码的类,就像我一直使用这个工具做的那样,泛型无法实现,因为每个持久化类都是完全不同的。

至于泛型的良好来源。当然是 Jon Skeet 的书! :-)


1
那我得去看看Jon Skeet的书了。是啊,持久化的对象不能泛化,往往是一大块乏味的代码。 - jwendl

15

作为T4的创始人,我不得不像你们想象的那样多次为这个问题辩护 :-)

我的信仰是,在其最佳状态下,代码生成是生产可重用库等价价值的一步。

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

通常情况下,制作参数化代码生成器(尤其是基于模板的系统)所需的工作量要比正确地设计一个高质量的基础库把使用成本降到同样水平所需的工作量要少得多,因此这是从一致性中获得价值并消除重复错误的快速方法。

然而,我仍然相信,通过减少总代码量可以最大程度地改善成品系统。如果没有其他的,它的内存占用几乎总是明显较小的(虽然人们倾向于认为泛型在这方面是免费的,但它们肯定不是)。

如果您已经意识到使用代码生成器会带来一些价值,那么这通常会为您节省一些时间或金钱或好意,以便从生成的代码库中收集一个库。然后,您可以逐步重新设计代码生成器以针对新库,并希望生成更少的代码。反复洗涤。

在我与其他人的讨论中,有一个有趣的反面观点,即丰富、复杂、参数化的库在学习曲线方面并不是最容易的东西,尤其是对于那些没有深度沉浸在平台中的人。坚持将代码生成到更简单的基本框架上可能会产生冗长的代码,但它通常很简单且易于阅读。

当然,如果您的生成器具有大量变异和极其丰富的参数化,您可能只是交换了您的产品的复杂性和模板的复杂性。这是一个容易滑入的路径,可能会使维护同样令人头痛-小心。


8

生成代码并不是邪恶的,也没有什么异味!关键在于在正确的时间生成正确的代码。我认为T4很棒——虽然我只偶尔使用它,但当我使用时它非常有帮助。无条件地说生成代码是不好的是绝对疯狂的!


6

在我的看法中,只要代码生成是您正常构建过程的一部分,而不是运行一次然后保留其输出,那么代码生成器就很好用。我之所以这样说是因为如果只使用代码生成器一次并丢弃创建它的数据,您只是自动创建了一个大规模的DRY违规和维护难题;而每次生成代码则意味着您使用来生成的工具是真正的源代码,生成的文件仅是您应该忽略的中间编译阶段。

Lex和yacc是典型的工具示例,它们允许您以高效的方式指定功能并从中生成高效的代码。手工完成它们的工作将延长开发时间,并可能产生效率更低且难以阅读的代码。虽然您肯定可以直接将类似于Lex和Yacc的工具纳入代码中,并在运行时而不是编译时执行它们的工作,但这肯定会增加代码的复杂性并减慢它的速度。如果您确实需要在运行时更改规格,那么这可能值得一试,但在大多数正常情况下,在编译时使用lex/yacc为您生成代码是一个巨大的胜利。


好的回答和观点,但从哲学上讲,如果您可以生成代码并强制其始终保持最新状态,那么是否会有所不同。 - jwendl
我给了你一票,但可能没有你对yacc的喜爱那么高。我正在开发一个C++代码生成器 - user250176

6

如果没有代码生成,Visual Studio 2010 中的大部分内容都不可能存在。Entity Framework 不可能存在。拖放控件到窗体上这样简单的操作也不可能存在,Linq 也一样。说不应该使用代码生成是奇怪的,因为很多人甚至没有考虑过它。


4
也许有点苛刻,但对我来说,代码生成有些不妥。使用代码生成意味着存在许多基本原则可以用“不要重复自己”的方式表达出来。这可能需要更长的时间,但当你最终得到仅包含真正改变部分的类时,会感到满意,这是基于包含机制的基础设施。
至于泛型...不,我对它没有太多问题。目前唯一不起作用的是说:
List<Animal> a = new List<Animal>();
List<object> o = a;

但是在C#的下一个版本中,这将成为可能。

DRY很重要,我认为这就是为什么这个问题在我的脑海中。虽然如果生成器正在重复,你真的在重复自己吗? - jwendl
2
不,这在 C# 的下一个版本中将不可能。类型参数的协变性和逆变性仅适用于接口和委托。 - Rafa Castaneda
1
代码生成有时可能会出现问题,但手动从wsdl编写soap类或手动从xsd创建类都是浪费时间的不必要操作,尤其是如果生成的类(特别是从xsd生成的类)是那些需要频繁更改的。 - Schalk Versteeg

2
我曾经使用过T4进行代码生成,也使用过Generics。两者都很好,各有优缺点,适用于不同的场景。
在我的情况下,我使用T4基于数据库模式生成实体、数据访问层(DAL)和业务逻辑层(BLL)。然而,DAL和BLL引用了我构建的迷你ORM,该ORM基于Generics和反射。因此,我认为你可以将它们并排使用,只要你保持控制,使其小而简单即可。
T4生成静态代码,而Generics是动态的。如果您使用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();

1
一旦代码被JIT编译,您将获得与通常相同的性能。这里没有反射。最多可以将泛型称为“懒惰”,因为它们将等待编译代码,直到对于某种类型真正需要它。 - Barsonax

2

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

所以,我虽然哭泣,但我必须使用它们。


1

引用: 我并没有找到有效的方法来构建能够例如自我实例化的模板。换句话说,我永远无法做到:

return new 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();
    }
} 

1

更多的代码意味着更多的复杂性。更多的复杂性意味着更多的漏洞藏身之处,这意味着修复周期更长,进而意味着整个项目成本更高。

在可能的情况下,我更喜欢最小化代码量以提供等效功能;理想情况下使用动态(编程)方法而不是代码生成。反射、属性、方面和泛型为DRY策略提供了很多选择,将生成作为最后的手段。


4
取决于您的工作/思考抽象级别。您的编译器也是一个代码生成器,但我怀疑您不会夜里担心生成了多少IL以及如何保持最小化的IL数量。高级别的代码生成是静态语言的未来...现在微软推出的每个产品都在使用它。 - Jack Ukleja

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