【问题标题】:Code Generators or T4 Templates, are they really evil?代码生成器或 T4 模板,它们真的很邪恶吗?
【发布时间】:2010-10-06 04:11:52
【问题描述】:

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

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

return new T();

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

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

你的想法是什么?您更喜欢构建生成器,还是更喜欢使用泛型?另外,泛型能走多远?我对泛型有相当多的了解,但是我总是遇到一些陷阱和陷阱,这导致我求助于 T4 模板。

在需要大量灵活性的情况下,有什么更合适的方法来处理?哦,作为对这个问题的补充,关于 C# 和泛型的好资源是什么?

【问题讨论】:

  • 评论我自己的帖子,Activator.CreateInstance();解决了这个问题(有点)...
  • 泛型和 T4 模板有不同的用途...

标签: c# generics code-generation t4


【解决方案1】:

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

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

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

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

关于泛型的一个很好的来源。最好的当然是Jon Skeet's book! :-)

【讨论】:

  • 那我得去看看 Jon Skeet 的书了。是的,持久化的对象无法泛化,并且往往是一大块乏味的代码。
【解决方案2】:

作为 T4 的发起者,我不得不为这个问题辩护好几次,你可以想象:-)

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

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

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

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

如果您已经意识到使用代码生成器的一些价值,那么这通常会为您赢得一些时间、金钱或善意,以便投资于从生成的代码库中获取库。然后,您可以逐步重新设计代码生成器以针对新库并希望生成更少的代码。冲洗并重复。

在这个线程中对我提出的一个有趣的反驳是,就学习曲线而言,丰富、复杂的参数库并不是最简单的事情,尤其是对于那些没有深入沉浸在平台中的人来说。坚持在更简单的基本框架上生成代码会产生冗长的代码,但它通常非常简单且易于阅读。

当然,如果您的生成器中存在大量差异和极其丰富的参数化,您可能只是在牺牲产品的复杂性来换取模板的复杂性。这是一个很容易滑入的路径,并且可能使维护同样令人头疼 - 请注意这一点。

【讨论】:

    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

      • 好答案和好观点,但从哲学上讲,如果您可以生成代码并强制它始终是最新的,那么它会有所作为。
      • 我给了你一票,但我对 yacc 的评价可能没有你那么高。我正在处理C++ code generator
      【解决方案5】:

      如果没有代码生成,Visual Studio 2010 中的大部分功能将无法实现。实体框架是不可能的。将控件拖放到窗体上的简单行为是不可能的,Linq 也不可能。说不应该使用代码生成是很奇怪的,因为很多人甚至没有考虑就使用它。

      【讨论】:

        【解决方案6】:

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

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

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

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

        但即使在下一版本的 C# 中也能做到这一点。

        【讨论】:

        • DRY 很重要,我想这就是我想到这个问题的原因。虽然如果生成器正在重复,你真的在​​重复自己吗?
        • 不,这在 C# 的下一版本中是不可能的。类型参数变化仅适用于接口和委托。
        • 代码生成有异味,但是从 wsdl 手动编写肥皂类或从 xsd 手动创建类是不必要的浪费时间,特别是如果生成的类,特别是从 xsd 生成的类是少数几个位做改变。
        【解决方案7】:

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

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

        【讨论】:

          【解决方案8】:

          我使用 T4 进行代码生成,也使用泛型。两者都很好,各有利弊,并且适用于不同的目的。

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

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

          关于“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();
          

          【讨论】:

          • 一旦代码被 jitted,您将获得与通常相同的性能。那里没有反射。最多你可以称泛型为惰性,因为它们会等待 jitting 代码直到它真正需要某种类型。
          【解决方案9】:

          更多的代码意味着更多的复杂性。更高的复杂性意味着更多隐藏错误的地方,这意味着更长的修复周期,进而意味着整个项目的成本更高。

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

          【讨论】:

          • 取决于您正在工作/思考的抽象级别。你的编译器也是一个代码生成器,但我怀疑你没有熬夜担心正在生成多少 IL 以及如何保持 IL 的数量最小化。高级代码生成是静态语言的未来。MS 现在推出的每一个产品都在使用它
          【解决方案10】:

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

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

          【讨论】:

            【解决方案11】:

            代码生成器可能被视为一种代码异味,表明目标语言存在缺陷或功能缺失。

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

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

            我想说的是,代码生成器表明所选语言存在缺陷,通过为手头的特定问题提供更方便的专业语法来解决。

            【讨论】:

              【解决方案12】:

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

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

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

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

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

              【讨论】:

                【解决方案13】:

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

                返回新的 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();
                    }
                } 
                

                【讨论】:

                  【解决方案14】:

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

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

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

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

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

                  我已经很久没有编写(甚至查看)任何包含连接对象的东西了,以至于我很难临时记住语法。

                  【讨论】:

                  • 也许您只见过真正愚蠢的代码生成器?我无法想象有人将 lex 和 yacc 的功能描述为能够非常快速地复制/粘贴......
                  • 回答你的第一个问题:因为生成代码中的错误可以在一个地方修复,所以重新生成完成。使用 c&p 时,它会被冷落,因为必须跟踪它被粘贴、修改和错误的所有地方。
                  • @Sol:很公平——我过于笼统了。我主要指的是由 ORM 生成的那种代码
                  • @David:当然,我明白这一点。我仍然更喜欢在一个地方修复一个错误,并且不必在很多地方复制它,即使生成器是快速和自动的。
                  【解决方案15】:

                  代码生成,如泛型、模板和其他此类快捷方式,是一个强大的工具。与大多数强大的工具一样,它增强了用户善恶的能力——它们是不可分割的。

                  因此,如果您彻底了解您的代码生成器,预测它将产生的所有内容以及原因,并且出于正当理由打算这样做,那么就去做吧。但不要使用它(或任何其他技术)让您经过一个您不确定要去哪里或如何到达那里的地方。

                  有些人认为,如果您解决了当前的问题并实施了一些行为,那么您就是金子。对于下一个开发者(可能是你自己),你在你的足迹中留下了多少不透明和不透明的东西并不总是很明显。

                  【讨论】:

                    猜你喜欢
                    • 2012-01-23
                    • 2013-10-03
                    • 2010-09-22
                    • 1970-01-01
                    • 2011-01-04
                    • 1970-01-01
                    • 2017-06-06
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多