上次我們談到了.NET 4.0中爲什麼要引入Type Equivalency這樣一個新功能,這次我們來看一個比較簡單的一個程序:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Office.Interop.Excel;
namespace NOPIAExcelDemo
{
class Program
{
static void Main(string[] args)
{
Application excelApp = new Application();
excelApp.Workbooks.Add();
excelApp.Visible = true;
}
}
}
這個程序調用Excel,創建一個新的Workbook,並把Excel主程序設置爲可見。
首先,我們在VS 2010中添加一個新的C#控制檯項目,然後在Solution Explorer中選擇Add Reference,選擇Excel 12的Interop Assembly:
選擇點擊OK之後,在Reference下面會多出一項Microsoft.Office.Interop.Excel的引用,在其上右鍵點擊選擇Properties:
裏面有一項Embed Interop Types,修改爲True。
完成之後運行程序,沒發現區別是不是?呵呵,這就對了。我們回頭來看看生成的代碼是什麼樣子的。
用ILDASM打開生成的EXE,雙擊Manifest,結果如下:
// Metadata version: v4.0.11001
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z/V.4..
.ver 4:0:0:0
}
.assembly extern System.Core
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z/V.4..
.ver 4:0:0:0
}
.assembly NOPIAExcelDemo
{
// ...
}
上面的結果說明什麼呢?雖然我們之前通過Add Reference讓這個項目引用了Excel的PIA,但是,這個EXE並沒有對Excel的PIA的引用,也就是說,這個EXE可以獨立於PIA運行!這也正是這個Feature的最直接的作用:消除PIA,直接將Interop相關的託管類型直接嵌入(Embed)到EXE中。現在我們再看看這個EXE中又有那些類型:
非常清楚,除了NOPIAExcelDemo本身的類型之外,這個EXE把Excel的PIA中部分類型,如_Application, _Workbook, WorkBooks…等等,都包括進來了。注意這些類型都是剛纔的程序所引用到的,而沒有用到是不會出現在這個EXE中的。再進一步看看Workbooks這個接口。注意我們調用到了了Workbooks.Add方法,而在這個EXE中的Workbooks類型也只有Add方法!那麼其他方法都去了那裏呢?讓我們再回頭看看PIA中的Workbooks類型是什麼樣子的:
可以看到其中的方法要多上不少。而且EXE中的Workbooks類型還有兩個奇怪的_VtblGap1_3,_VtblGap2_15。這兩個函數的作用是什麼呢?CLR的Interop有一個不太爲人知的特性:因爲COM中的接口是基於虛函數表的,如果只調用虛函數表中的某個函數,而不用到其他函數,那麼其他函數的入口點是可以不需要的,只需要用到的那個函數在正確的虛表位置,指向正確的函數地址即可。因此,CLR提供了一個功能,凡是接口中以_VtblGap_<N>_<M>形式命名的函數,都視爲虛函數表中的M個空白項(注意N值忽略)。以Workbooks函數爲例,第一個_VtblGap1_3函數表明這裏有三個不用到的虛函數表項,對應着Workbooks裏面的get_Application, get_Creator, get_Parent函數。Get_Parent之後正好是Add函數。在Add函數之後,又有一個_VtblGap2_15,對應着從Close到OpenXML這15個函數。注意.NET屬性並非函數(而是由編譯器翻譯成對應的函數),因此不算在內。這種做法可以有效的節約空間佔用,減少用到的接口的複雜性。這個將Interop Assembly中的類型“拖入”到用戶的EXE中的操作,我們內部稱之爲Pull-in,是由C#編譯器實現的。
談了這麼多,總結一下:C#編譯器允許將Interop Assembly中的類型直接嵌入在最終生成的EXE中,從而斷絕和Interop Assembly(包括PIA)之間的引用關係。需要Interop Assembly從此只限於在編譯時,而非運行時。
不過,這個只解決了問題的一部分,那就是如何避免最終的EXE和Interop Assembly之間存在引用關係。然而,最重要的是,如果在另外的Assembly中,也引用到了另外版本的Interop Assembly中的某個類型,比如Office 11中的Workbooks,在這種情況下,如何解決託管類型衝突的問題呢?答案在TypeIdentifierAttribute中。如果我們查看EXE中的Workbooks的接口定義,我們會發現下面的內容:
.class interface public abstract auto ansi import Microsoft.Office.Interop.Excel.Workbooks
implements [mscorlib]System.Collections.IEnumerable
{
.custom instance void [mscorlib]System.Runtime.InteropServices.TypeIdentifierAttribute::.ctor(string,
string) = ( 01 00 00 00 00 00 )
.custom instance void [mscorlib]System.Runtime.InteropServices.GuidAttribute::.ctor(string) = ( 01 00 24 30 30 30 32 30 38 44 42 2D 30 30 30 30 // ..$000208DB-0000
2D 30 30 30 30 2D 43 30 30 30 2D 30 30 30 30 30 // -0000-C000-00000
30 30 30 30 30 34 36 00 00 ) // 0000046..
} // end of class Microsoft.Office.Interop.Excel.Workbooks
其中的TypeIdentifierAttribute是Type Equivalency,也就是NOPIA這個Feature的核心內容。在下一篇文章中,我將講解這個Attribute的作用,以及C#編譯器如何處理具有這個Attribute的類型之間的交互。
作者: 張羿
轉載請註明出處