這一章所有的東東都是關於發現Type的信息的,在編譯時完全不知道該Type任何信息的情況下,創建一個Type的實例,訪問Type的成員等。本章的這些信息典型的應用就是創建一個可以動態擴展的應用程序。可擴展的應用程序就是一個公司寫一個主應用,由其他公司來寫“插件”從而來擴展這個主應用。主應用不能針對這個“插件”(add-in)來創建或測試,因爲這些“插件”是不同的公司寫的或“插件”是在主應用賣出去以後才寫的。這就是爲什麼主應用需要在運行時發現“插件”中的信息。
一個動態擴展的應用程序會利用21章所介紹的“公共語言運行時(CLR)宿主和應用程序域(AppDomain)”。宿主會在一個有自己的安全和配置的 AppDomain中運行“插件”。宿主也可以通過卸載這個AppDomain來卸載“插件”(add-in)。本章結束時,我會講一些關於如何把所有剛纔說的統統放到一起:CLR宿主、AppDomain、程序集加載、發現Type、構建Type實例和反射,目的是爲了創建一個健壯、安全、可動態擴展的應用程序。
程序集加載
如你所知,當JIT編譯器將一個方法編譯成中間語言(IL)時,它會看看IL代碼中引用了哪些Type。然後在運行時,JIT編譯器利用程序集的 “TypeRef”和“AssemblyRef”元數據表來決定程序集引用了哪些Type。“AssemblyRef”元數據表項包含了程序集的強名稱(strong name)的所有組成部分。JIT編譯器會將所有的這些部分鏈接起來--名稱(沒有擴展名和路徑)、版本、文化和公匙--形成一個串,然後試圖加載與標識信息相匹配的程序集到AppDomain中(如果該程序集還沒有被加載的話)。如果被加載的程序集是“弱名稱”(weakly named),那麼這個標識信息中僅僅有程序集的名字(沒有版本、文化或公匙信息)。
在內部,CLR試圖用System.Reflection.Assembly類的靜態方法Load來加載程序集。這個方法是公開地文檔化了,所以你可以調用它,來顯式地加載一個程序集到你的AppDomain中。這個CLR中的靜態方法相當於Win32中的LoadLibrary函數。實際上 Assembly的Load方法有幾個版本的重載。這有幾個很常用的重載函數的原型:
public class Assembly {
public static Assembly Load(AssemblyName assemblyRef);
public static Assembly Load(String assemblyString);
// Less commonly used overloads of Load are not shown
}
在內部,Load會促使CLR對程序集應用一個“版本-綁定重定向”策略,並且會在下面幾個地方查找程序集:GAC中,應用程序的基礎目錄,私目錄的子目錄和基本代碼處。如果你以弱名稱程序集調用Load方法,Load方法不會對程序集應用一個“版本-綁定重定向”策略,並且CLR不會在GCA中查找該程序集。如果Load方法找到指定的程序集,它會返回一個Assembley對象的引用,該對象代表已加載的程序集。如果Load方法沒有找到指定的程序集,它會拋出一個System.IO.FileNotFoundException。
注意:在一些極少的情況下,你可能會想加載一個爲一個特定版本的Windows創建的程序集。在這種情況下,當指定一個程序集身份信息時,你可以包括一個 “處理器架構”的部分。例如:如果我的全局程序集緩存(GAC)中有一個程序集的一個IL中立版本和一個特定的X86版本,CLR會偏愛與特定CPU有關的那個版本的程序集。然而,我可以通過給Assembly的Load方法傳遞下面的字符串來強制CLR加載IL中立版本的程序集:
"SomeAssembly, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=01234567890abcde, ProcessorArchitecture=MSIL"
今天,CLR爲處理器架構這個問題,支持四個可能的值:MSIL,X86,IA64,AMD64。
- Assembly類的靜態方法Load是加載一個程序集到一個AppDomain中的首先方法。
- AppDomain 的Load方法是一個非靜態方法,它允許加載一個程序集到一個指定的AppDomain中。但這個方法設計上是被非託管代碼調用的。這個方法允許宿主程序把一個程序集註入到一個指定的AppDomain中。託管代碼的開發人員一般情況下不應該調用這個方法。因爲會有一些與加載程序集的路徑問題。
- 如果想調用一個指定路徑下的程序集,可以調用Assembly的LoadFrom方法。Assembly.LoadFrom內部會調用 Assembly.Load方法。如果Assembly.Load通過“版本-綁定重定向”策略和在其他有關的目錄下沒有找到指定的程序集文件,它纔會用 LoadFrom參數中提供的路徑信息。
- Assembly.LoadFrom方法可以這樣調用:Assembly a = Assembly.LoadFrom(@"http://Wintellect.com/SomeAssembly.dll");當你提供了一個 Internet地址,CLR會下載這個文件到“用戶下載緩存”中,然後再從那裏加載該程序集。這時如果你沒有聯到互聯網的話,那麼會拋出一個異常。然而,如果這個文件以前已經下載過,並且IE已經設置成“離線”工作,那麼CLR就加載以前下載的那個文件,而不拋出異常。
- 如果你想加載指定位置上的程序集,而不要CLR應用任何的查找策略,可以調用Assembly類的LoadFile方法。
- 如果你只是想通過反射來分析一個程序集中元數據,而不會執行程序集中的代碼,那麼Assembly的靜態方法ReflectionOnlyLoadFrom和ReflectionOnlyLoad比較適合。
- CLR不支持卸載單獨的程序集的功能。
- 如果你想卸載一個程序集,你必須卸載包括這個程序集的整個AppDomain。
運用反射來建立可動態擴展的應用程序
應該知道一些反射類型或一些類型的某些方法是爲那些寫CLR編譯器的開發人員而專門設計的。應用程序開發人員典型情況下用不到這些類型和方法。
反射的性能
反射也被用在一些情況中,例如當一個應用程序需要在運行時加載特別的程序集中的特別的類型來完成一些工作。一個應用程序可能要求用戶提供程序集和類型的名字,然後這個應用程序可能顯式地加載這個程序集,構造指定的類型的實例,並調用此類型定義的方法。
反射是一個極其強大的機制,因爲它允許你在運行時發現並使用在編譯時不知道的類型和成員。這種強大也帶來了兩方面主要的缺點:
1、反射阻止編譯時的類型安全機制。由於反射嚴重依賴於字符串,於是在編譯時,你就失去了類型安全。例如Type.GetType("Jef");如果這個"Jef"拼寫錯誤,你只能得到一個運行時錯誤。
2、反射是慢的。當使用反射機制時,類型的名字和成員在編譯時都是未知的。你只能在運行時通過字符串形式的名字去唯一地識別類型和類型定義的方法。這就意味着反射要經常地執行字符串搜索,在System.Reflection名字空間掃描整個的程序集的元數據以獲得類型。一般情況下,這種字符串搜索是大小寫敏感的比較,這進一步減慢了速度。
使用反射調用一個成員,也會傷害到性能。當使用反射來調用一個方法時,你必須先將參數打包到一個數組中。在內部,反射必須在線程棧上解包這個數組。因此,CLR必須在執行一個方法之前檢查這些參數有正確的數據類型。最後,CLR要確定調用者有適當的安全許可來訪問這個被調用的成員。
因爲所有的這些原因,最好避免使用反射來訪問一個成員。如果你正在寫一個動態發現並動態創建類型實例的應用程序,你應該採用以下的方法:
- 以一個編譯時已知的類型作爲基類來派生需要動態創建的類型。在運行時,創建一個子類型的實例,然後以基類的變量來引用此實例(通過強制類型轉換),最後調用基類類型定義的虛方法。
- 讓需要動態創建的類型去實現一個編譯時類型可知的接口(interface)。在運行時,創建一個子類型的實例,然後以接口類型的變量來引用到這個實例(通過強制類型轉換),調用接口中的定義的方法。(這種方法能好一些,因爲第一種通過基類的方法,在一些特殊的情況下,不允許開發人員選擇最適合的基類。)
當你採用以上的兩種方法時,強列建議那個接口和基類要定義在在自己的程序集中。這會減少版本問題。
發現一個程序集中定義的Type
Assembly的GetExportedTypes方法可以得到一個程序集的所有公共類型,這些公共類型在程序集外可見。
代碼:
String dataAssembly = "System.Data, version=2.0.0.0, " +
"culture=neutral, PublicKeyToken=b77a5c561934e089";
Assembly a = Assembly.Load(dataAssembly );
foreach (Type t in a.GetExportedTypes()) {
Console.WriteLine(t.FullName);
}
一個Type對象倒底是什麼東東?
System.Type 是從System.Reflection.MemberInfo繼承的抽象類型。FCL中的 System.RuntimeType,System.ReflectionOnlyType,System.Reflection.TypeDelegator 類都是從System.Type中繼承而來。System.Reflection.Emit名字空間中的EnumBuilder, GenericTypeParameterBuilder 和TypeBuilder也是從System.Type中繼承而來。
- TypeDelegator類通過封裝Type可以代碼動態地子類化一個Type,允許你覆蓋一些方法,但用原來的Type處理大多數的工作。這一強大的機制允許你覆蓋反射工作的方式。
- System.RuntimeType 是Framework Class Library(FCL)內部的類型,在外部是看不到的,即此類型沒有文檔描述。當調用System.Object的GetType方法時,此方法就返回 RuntimeType的引用。在一個AppDomain中一個Type只有一個RuntimeType對象。可以如下驗證:
object o1 = new ArrayList();
object o2 = new ArrayList();
if(o1.GetType() == o2.GetType())
{
.......
} - 運算符typeof在編譯時就可以確定對象的類型(早綁定),而GetType方法在運行時得到對象的類型(晚綁定):
private static void SomeMethod(Object o) {
// GetType returns the type of the object at run time (late-bound)
// typeof returns the type of the specified class (early-bound)
if (o.GetType() == typeof(FileInfo)) { ... } //----(1)
if (o.GetType() == typeof(DirectoryInfo)) { ... }
}
注意:行(1)的代碼檢查變量o是不是引用到一個FileInfo類型上,它不檢查o所引用的對象是不是引用到FileInfo的子類型上。即它檢查的是精確匹配的類型,而不是兼容的類型。兼容類型的檢查是is和as運算符所做的。
建立類的異常繼承類型層次
public static class ProgramTest
{
public static void Main()
{
// Explicitly load the assemblies that we want to reflect over
LoadAssemblies();
// Initialize our counters and our exception type list
Int32 totalPublicTypes = 0, totalExceptionTypes = 0;
List<String> exceptionTree = new List<String>();
// Iterate through all assemblies loaded in this AppDomain
foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
{
// Iterate through all types defined in this assembly
foreach (Type t in a.GetExportedTypes())
{
totalPublicTypes++;
// Ignore type if not a public class
if (!t.IsClass || !t.IsPublic) continue;
// Build a string of the type's derivation hierarchy
StringBuilder typeHierarchy = new StringBuilder(t.FullName, 5000);
// Assume that the type is not an Exception-derived type
Boolean derivedFromException = false;
// See if System.Exception is a base type of this type
Type baseType = t.BaseType;
while ((baseType != null) && !derivedFromException)
{
// Append the base type to the end of the string
typeHierarchy.Append("-" + baseType);
derivedFromException = (baseType == typeof(System.Exception));
baseType = baseType.BaseType;
}
// No more bases and not Exception-derived, try next type
if (!derivedFromException) continue;
// We found an Exception-derived type
totalExceptionTypes++;
// For this Exception-derived type,
// reverse the order of the types in the hierarchy
String[] h = typeHierarchy.ToString().Split('-');
Array.Reverse(h);
// Build a new string with the hierarchy in order
// from Exception -> Exception-derived type
// Add the string to the list of Exception types
exceptionTree.Add(String.Join("-", h, 1, h.Length - 1));
}
}
// Sort the Exception types together in order of their hierarchy
exceptionTree.Sort();
// Display the Exception tree
foreach (String s in exceptionTree)
{
// For this Exception type, split its base types apart
string[] x = s.Split('-');
// Indent based on the number of base types
// and then show the most-derived type
Console.WriteLine(new String(' ', 3 * x.Length) + x[x.Length - 1]);
}
// Show final status of the types considered
Console.WriteLine("/n---> of {0} types, {1} are " +
"derived from System.Exception.",
totalPublicTypes, totalExceptionTypes);
}
private static void LoadAssemblies()
{
String[] assemblies = {
"System, PublicKeyToken={0}",
"System.Data, PublicKeyToken={0}",
"System.Design, PublicKeyToken={1}",
"System.DirectoryServices, PublicKeyToken={1}",
"System.Drawing, PublicKeyToken={1}",
"System.Drawing.Design, PublicKeyToken={1}",
"System.Management, PublicKeyToken={1}",
"System.Messaging, PublicKeyToken={1}",
"System.Runtime.Remoting, PublicKeyToken={0}",
"System.Security, PublicKeyToken={1}",
"System.ServiceProcess, PublicKeyToken={1}",
"System.Web, PublicKeyToken={1}",
"System.Web.RegularExpressions, PublicKeyToken={1}",
"System.Web.Services, PublicKeyToken={1}",
"System.Windows.Forms, PublicKeyToken={0}",
"System.Xml, PublicKeyToken={0}",
};
String EcmaPublicKeyToken = "b77a5c561934e089";
String MSPublicKeyToken = "b03f5f7f11d50a3a";
// Get the version of the assembly containing System.Object
// We'll assume the same version for all the other assemblies
Version version =
typeof(System.Object).Assembly.GetName().Version;
// Explicitly load the assemblies that we want to reflect over
foreach (String a in assemblies)
{
String Assemblyldentity = String.Format(a, EcmaPublicKeyToken, MSPublicKeyToken) +
", Culture=neutral, Version=" + version;
Assembly.Load(Assemblyldentity);
}
}
}
構建一個Type的實例
一旦你有一個Type對象的引用,你可能想構建這個Type的一個實例。FCL提供了幾種機制來做這件事兒:
- System.Activator的CreateInstance靜態方法,Activator類提供了幾個CreateInstance方法的重載方法。當調用這個方法時,你或者傳遞一個Type對象的引用或者傳遞一個Type對象身份信息的字符串作爲參數。
ObjectHandle是一個允許在一個AppDomain創建對象然後再將該對象傳遞給另一個AppDomain中,而不需要強制具體化該對象的類型。當你想具體化這個對象,你調用ObjectHandle的Unwrap方法即可。(具體化就是指返回被包裝的對象)。 - System.Activator的CreateInstanceFrom方法。這個方法與上面的方法行爲相似,除了它只接受字符串形式的參數。程序集被加載到調用該函數的AppDomain中。
- System.AppDomain 的方法。本類提供了4個方法來創建一個類型的實例:CreateInstance, CreateInstanceAndUnwrap, CreateInstanceFrom, CreateInstanceFromAndUnwrap這些方法與Activator相應方法的行爲相同。
- System.Type的InvokeMember實例方法。在一個Type對象的實例上,你可以調用InvokeMember方法。這個方法會找到與你傳遞的參數匹配的構造函數並構造出一個對象。這個對象總是創建在調用AppDomain中,並返回創建的對象的引用。
- System.Reflection名字空間的ConstructorInfo類的Invoke方法。
注意:CLR不需要值類型定義任何構造函數。所以上面所說的實例化對象的方法中凡與調用構造函數有關的方法在遇到值類型時,會出現問題。然而,Activator類的CreateInstance方法可以創建一個值類型的實例。
(補充,C#中的值類型和引用類型的區別:
結構與類共享大多數相同的語法,但結構比類受到的限制更多:
-
在結構聲明中,除非字段被聲明爲 const 或 static,否則無法初始化。
-
結構不能聲明默認構造函數(沒有參數的構造函數)或析構函數。
-
結構不能從類或其他結構繼承。
-
結構在賦值時進行復制。將結構賦值給新變量時,將複製所有數據,並且對新副本所做的任何修改不會更改原始副本的數據。
-
結構是值類型,而類是引用類型。
-
與類不同,結構的實例化可以不使用 new 運算符。
-
結構可以聲明帶參數的構造函數。
-
一個結構不能從另一個結構或類繼承,而且不能作爲一個類的基。所有結構都直接繼承自 System.ValueType,後者繼承自 System.Object。
-
結構可以實現接口。
-
結構可用作可爲 null 的類型,因而可向其賦 null 值。
) --end