本文章是由機器翻譯。

孜孜不倦的程式師

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