孜孜不倦的程序员

多模式 .NET,第 5 部分:自动元编程

Ted Neward


上个月的文章中,我们深入研究了对象,特别论述了继承为我们提供的通用性/可变性分析的“基准线”。尽管继承不是现代面向对象 (OO) 语言(例如 C# 或 Visual Basic)中唯一的通用性/可变性形式,但它无疑是 OO 模式的核心。另外我们也说到,它并不总是能为所有问题提供最好的解决方案。

总的来说,到目前为止,我们发现 C# 和 Visual Basic 既是过程式语言,也是 OO 语言。不止如此,这两种语言还都是元编程 语言,Microsoft .NET Framework 开发人员可以利用它们以各种不同的方式从程序构建程序:自动、反射和生成。

自动元编程

元编程的核心原理很简单:传统的过程式或 OO 编程构造不能解决所有软件设计问题,至少不能以令人满意的方式解决。以下例子可以证明这种基本缺陷:开发人员经常发现需要一个数据结构,用于维护某种类型的排序列表,这样才能在列表的特定位置插入项,并按照指定的顺序查看项。出于性能考虑,有时这个列表需要采用链接的节点列表形式。话句话说,我们想要得到一个排序的链接列表,但对其中存储的类型进行强类型化处理。

从 C++ 领域转入 .NET Framework 的开发人员知道此问题的一种解决方案,即参数化类型,也就是通常所称的泛型。但是,从早期 Java 领域转入 .NET 的开发人员知道,早在模板出现之前就出现了另一种解决方案(该解决方案最终融入了 Java 平台)。该解决方案就是在必要时直接编写每个所需列表的实现,如图 1 所示。

图 1 必要时编写列表实现的示例

Class ListOfInt32

  Class Node
    Public Sub New(ByVal dt As Int32)
      data = dt
    End Sub
    Public data As Int32
    Public nextNode As Node = Nothing
  End Class

  Private head As Node = Nothing

  Public Sub Insert(ByVal newParam As Int32)
    If IsNothing(head) Then
      head = New Node(newParam)
    Else
      Dim current As Node = head
      While (Not IsNothing(current.
nextNode))
        current = current.
nextNode
      End While
        current.
nextNode = New Node(newParam)
    End If
  End Sub

  Public Function Retrieve(ByVal index As Int32)
    Dim current As Node = head
    Dim counter = 0
    While (Not IsNothing(current.
nextNode) And counter < index)
      current = current.
nextNode
      counter = counter + 1
    End While

    If (IsNothing(current)) Then
      Throw New Exception("Bad index")
    Else
      Retrieve = current.data
    End If
  End Function
End Class

现在,很明显,这无疑违反了“切勿重复”(DRY) 原则:每次当设计调用这种新列表时,都需要“手动”进行编写。随着时间的推移,这逐渐会成为一个问题。 尽管列表实现并不复杂,但逐个编写列表实现还是相当费力、耗时的,特别是在需要更多功能时。

当然,没人认为开发人员必须亲自来编写这些代码。 我们应该求助于代码生成解决方案,有时也称为自动元编程。 另一个程序可以轻松完成这些工作,例如旨在剔除针对每个所需类型自定义的类的程序,如图 2 所示。

图 2 自动元编程示例

Sub Main(ByVal args As String())
  Dim CRLF As String = Chr(13).ToString + Chr(10).ToString()
  Dim template As String =
   "Class ListOf{0}" + CRLF +
   "  Class Node" + CRLF +
   "    Public Sub New(ByVal dt As {0})" + CRLF +
   "      data = dt" + CRLF +
   "    End Sub" + CRLF +
   "    Public data As {0}" + CRLF +
   "    Public nextNode As Node = Nothing" + CRLF +
   "  End Class" + CRLF +
   "  Private head As Node = Nothing" + CRLF +
   "  Public Sub Insert(ByVal newParam As {0})" + CRLF +
   "    If IsNothing(head) Then" + CRLF +
   "      head = New Node(newParam)" + CRLF +
   "    Else" + CRLF +
   "      Dim current As Node = head" + CRLF +
   "      While (Not IsNothing(current.
nextNode))" + CRLF +
   "        current = current.
nextNode" + CRLF +
   "      End While" + CRLF +
   "      current.
nextNode = New Node(newParam)" + CRLF +
   "    End If" + CRLF +
   "  End Sub" + CRLF +
   "  Public Function Retrieve(ByVal index As Int32)" + CRLF +
   "    Dim current As Node = head" + CRLF +
   "    Dim counter = 0" + CRLF +
   "    While (Not IsNothing(current.
nextNode) And counter < index)"+ CRLF +
   "      current = current.
nextNode" + CRLF +
   "      counter = counter + 1" + CRLF +
   "    End While" + CRLF +
   "    If (IsNothing(current)) Then" + CRLF +
   "      Throw New Exception()" + CRLF +
   "    Else" + CRLF +
   "      Retrieve = current.data" + CRLF +
   "    End If" + CRLF +
   "  End Sub" + CRLF +
   "End Class"

    If args.Length = 0 Then
      Console.WriteLine("Usage: VBAuto <listType>")
      Console.WriteLine("   where <listType> is a fully-qualified CLR typename")
    Else
      Console.WriteLine("Producing ListOf" + args(0))

      Dim outType As System.Type =
        System.Reflection.Assembly.Load("mscorlib").GetType(args(0))
      Using out As New StreamWriter(New FileStream("ListOf" + outType.Name + ".vb",
                                              FileMode.Create))
        out.WriteLine(template, outType.Name)
      End Using
    End If

然后,当所需的类创建之后,只需进行编译即可:可以将其添加到项目中,也可以编译到其自己的程序集中以作为二进制文件重复使用。

当然,生成的语言不必是编写代码生成器时所用的语言。事实上,两种语言不一样会很有用处,因为在调试期间,这有助于使开发人员清醒地认识到两者之间的区别。

通用性、可变性及其优缺点

在通用性/可变性分析中,自动元编程处在一个有趣的位置。 在图 2 的示例中,它使结构和行为(上文中的类的概要)具有通用性,并使数据/类型行(也就是存储在所生成的类中的类型)具有可变性。 很明显,我们可以将所需的任意类型替换到 ListOf 类型中。

但如果需要,自动元编程也可以实现相反的替换方式。 利用丰富的模板语言,例如 Visual Studio 随附的文本模板转换工具包 (T4),代码生成模板可以在生成源代码时做出决策,使模板在数据/结构行方面实现通用性,而结构和行为行是可变的。 事实上,如果代码模板足够复杂(这并不值得提倡),甚至有可能消除所有通用性,而所有内容(数据、结构、行为等等)都是可变的。 但是这么做通常很快就会导致无法管理,因此一般来说应该避免。 这个问题揭示了有关自动元编程的一个重要事实:因为缺乏任何类型的继承结构限制,所以需要明确选择通用性和可变性,避免源代码模板由于过于追求灵活而失控。 例如,以图 2 中的 ListOf 为例,通用性体现在结构和行为方面,可变性体现在存储的数据类型方面,任何要在结构或行为中引入可变性的企图都应视为危险信号,并且可能导致混乱。

显然,代码生成本身带有一些重大风险,尤其是在维护方面:一旦发现错误(例如图 2 的 ListOf 示例中的并发错误),修复起来就不是简单的事。 模板显然可以修复,但对于已经生成的代码无补于事,每个源代码作品都需要重新生成,而这又是很难自动跟踪和确保的。 而且,对已经生成的文件进行的任何手动更改都必然会丢失,除非模板生成的代码允许进行自定义。 通过使用部分类可以缓解这种覆盖的风险,让开发人员可以填充(或不填充)所生成的类的“另一半”。而且通过扩展方法,开发人员有机会向现有的类型系列中“添加”方法而无需编辑类型。 但是部分类必须从一开始就存在于模板中,扩展方法又有一些限制,使其无法替换现有的行为,这再次使得这两种方法都不是实现负可变性的好机制。

代码生成

多年以来,从 C 预处理器宏到 C# T4 引擎,代码生成(或自动元编程)技术一直是编程的一部分;而且由于其理念的概念简单性,它还将继续发展。 但是,它的主要缺陷是在扩展过程中缺乏编译器结构和检查(当然,除非检查是由代码生成器自己进行的,但这项任务的难度超乎想象),而且无法以有效的方式实现负可变性。 .NET Framework 提供了一些机制,可以让代码生成变得更容易。在很多情况下,引入这些机制的目的是减轻其他 Microsoft 开发人员的痛苦,但这些机制并不能消除代码生成中的所有隐患,绝对不能。

但是自动元编程仍是使用极其广泛的元编程形式之一。 C# 有宏预处理器,C++ 和 C 也一样。 (使用宏来创建“小模板”,这在 C++ 提供模板功能之前是一种普遍的方式。)除此之外,在更大的框架或库中使用元编程也是很常见的,尤其是对于进程间的通信方案(例如由 Windows Communication Foundation 生成的客户端和服务器存根)。 其他工具包使用自动元编程来提供“框架”,以便使应用程序的早期工作更轻松(例如我们在 ASP.NET MVC 中所看到的)。 事实上,每个 Visual Studio 项目都从自动元编程开始,只不过其表现形式是“项目模板”和“项模板”,我们大多数人都利用它们来创建新项目或向项目中添加文件。 其他在此就不一一列举了。 与计算机科学领域的许多其他课题一样,尽管自动元编程有明显的缺陷和隐患,它仍是设计人员工具箱中一款方便、有用的工具。 幸运的是,它并不是程序员唯一能够使用的元工具。

Ted Neward 是 Neward & Associates 的负责人,这是一家专门研究 .NET Framework 企业系统和 Java 平台系统的独立公司。他曾写过 100 多篇文章,是 C# 领域最优秀的专家之一;是 INETA 发言人;并且著作或合著过十几本书,包括《Professional F# 2.0》(Wrox,2010 年)。此外,他还定期提供咨询和指导。您可通过 ted@tedneward.com 与他联系,也可通过 blogs.tedneward.com 访问其博客。