多模式 .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 访问其博客。