.Net 中的反射(序章) - Part.1

摘自:http://www.tracefact.net/CLR-and-Framework/Reflection-Part1.aspx 

引言

反射是.Net提供給我們的一件強力武器,儘管大多數情況下我們不常用到反射,儘管我們可能也不需要精通它,但對反射的使用作以初步瞭解在日後的開發中或許會有所幫助。

反射是一個龐大的話題,牽扯到的知識點也很多,包括程序集、自定義特性、泛型等,想要完全掌握它非常不易。本文僅僅對反射做一個概要介紹,關於它更精深的內容,需要在實踐中逐漸掌握。本文將分爲下面幾個部分介紹.Net中的反射:

  1. 序章,我將通過一個例子來引出反射,獲得對反射的第一印象。
  2. 反射初步、Type類、反射普通類型。(修改中,近期發佈...)
  3. 反射特性(Attribute)。
  4. xxxx (待定)
  5. ...

序章

如果你還沒有接觸過反射,而我現在就下一堆定義告訴你什麼是反射,相信你一定會有當頭一棒的感覺。我一直認爲那些公理式的定義和概念只有在你充分懂得的時候才能較好的發揮作用。所以,我們先來看一個開發中常遇到的問題,再看看如何利用反射來解決:

在進行數據庫設計的過程中,常常會建立一些基礎信息表,比如說:全國的城市,又或者訂單的狀態。假設我們將城市的表,起名爲City,它通常包含類似這樣的字段:

Id     Int Identity(1,1) 城市Id
Name   Varchar(50)           城市名稱
ZIP    Varchar(10)           城市郵編
... // 略

這個表將供許多其他表引用。假如我們在建立一個酒店預訂系統,那麼酒店信息表(Hotel)就會引用此表,用CityId字段來引用酒店所在城市。對於城市(City)表這種情況,表裏存放的記錄(城市信息)是不定的,意思就是說:我們可能隨時會向這張表裏添加新的城市(當某個城市的第一家酒店想要加入預訂系統時,就需要在City表裏新添這家酒店所在的城市)。此時,這樣的設計是合理的。

1.建表及其問題

我們再看看另外一種情況,我們需要標識酒店預訂的狀態:未提交、已提交、已取消、受理中、已退回、已訂妥、已過期。此時,很多開發人員會在數據庫中建立一張小表,叫做BookingStatus(預訂狀態),然後將如上狀態加入進去,就好像這樣:

如同城市(City)表一樣,在系統的其他表,比如說酒店訂單表(HotelOrder)中,通過字段StatusId引用這個表來獲取酒店預訂狀態。然而,幾個月以後,雖然看上去和城市表的用法一樣,結果卻發現這個表只在數據庫做聯合查詢或者 只在程序中調用,卻從來不做修改,因爲預訂流程確定下來後一般是不會變更的。在應用程序中,也不會給用戶提供對這個表記錄的增刪改操作界面。

而在程序中調用這個表時,經常是這種情況:我們需要根據預訂狀態對訂單列表進行篩選。此時通常的做法是使用一個下拉菜單(DropDownList),菜單的數據源(DataSource),我們可以很輕易地通過一個SqlDataReader獲得,我們將DropDownList的文本Text設爲Status字段,將值Value設爲Id字段。

此時,我們應該已經發現問題:

  1. 如果我們還有航班預訂、遊船預訂,或者其他一些狀態,我們需要在數據庫中創建很多類似的小表,造成數據庫表的數目過多。
  2. 我們使用DropDownList等控件獲取表內容時,需要連接到數據庫進行查詢,潛在地影響性能。

同時,我們也注意到三點:

  1. 此表一般會在數據庫聯合查詢中使用到。假設我們有代表酒店訂單的HotelOrder表,它包含代表狀態的StatusId字段,我們的查詢可能會像這樣:Select *, (Select Status From BookingStatus Where Id = HotelOrder.StatusId) as Status From HotelOrder。
  2. 在應用程序中,此表經常作爲DropDownList或者其他List控件的數據源。
  3. 這個表幾乎從不改動。

2.數組及其問題

意識到這樣設計存在問題,我們現在就想辦法解決它。我們所想到的第一個辦法是可以在程序中創建一個數組來表示預訂狀態,這樣我們就可以刪掉BookingStatus狀態表(注意可以這樣做是因爲BookingStatus表的內容確定後幾乎從不改動)。

string[] BookingStatus = {  
   "NoUse", "未提交","已提交","已取消","受理中","已退回","已訂妥","已過期"
};     // 注意數組的0號元素僅僅是起一個佔位作用,以使程序簡潔。因爲StatusId從1開始。

我們先看它解決了什麼:上面提到的問題1、問題2都解決了,既不需要在數據庫中創建表,又無需連接到數據庫進行查詢。

我們再看看當我們想要用文本顯示酒店的預訂時,該怎麼做(假設有訂單類HotelOrder,其屬性StatusId代表訂單狀態,爲int類型 )。

// GetItem用於獲取一個酒店訂單對象, orderId爲int類型,代表訂單的Id
HotelOrder myOrder = GetItem(orderId);
lbStatus.Text = BookingStatus[myOrder.StatusId];  //lbStatus是一個Label控件

目前爲止看上去還不錯,現在我們需要進行一個操作,將訂單的狀態改爲“受理中”。

myOrder.StatusId = 4;

很不幸,我們發現了使用數組可能帶來的第一個問題:不方便使用,當我們需要更新訂單的狀態值時,我們需要去查看BookingStatus數組的定義(除非你記住所有狀態的數字值),然後根據狀態值在數組中的位置來給對象的屬性賦值。

我們再看另一個操作,如果某個訂單的狀態爲“已過期”,就要對它進行刪除:

if(BookingStatus[myOrder.StatusId]=="已過期"){
    DeleteItem(myOrder);     // 刪除訂單
}

此時的問題和上面的類似:我們需要手動輸入字符串“已過期”,此時Vs2005 的智能提示發揮不了任何作用,如果我們不幸將狀態值記錯,或者手誤打錯,就將導致程序錯誤,較爲穩妥的做法還是按下F12導向到BookingStatus數組的定義,然後將“已過期”複製過來。

現在,我們再看看如何來綁定到一個DropDownList下拉列表控件(Id爲ddlStatus)上。

ddlStatus.DataSource = BookingStatus;
ddlStatus.DataBind();

但是我們發現產生的HTML代碼是這樣:

<select name="ddlStatus" id="ddlStatus">
    <option value="未提交">未提交</option>
    <option value="已提交">已提交</option>
    <option value="已取消">已取消</option>
    <option value="受理中">受理中</option>
    <option value="已退回">已退回</option>
    <option value="已訂妥">已訂妥</option>
    <option value="已過期">已過期</option>
</select>

我們看到,列表項的value值與text值相同,這顯然不是我們想要的,怎麼辦呢?我們可以給下拉列表寫一個數據綁定的事件處理方法。

protected void Page_Load(object sender, EventArgs e) {   
    ddlStatus.DataSource = BookingStatus;
    ddlStatus.DataBound += new EventHandler(ddlStatus_DataBound);
    ddlStatus.DataBind();
}

void ddlStatus_DataBound(object sender, EventArgs e) {
    int i = 0;
    ListControl list = (ListControl)sender; //注意,將sender轉換成ListControl
    foreach (ListItem item in list.Items) {
       i++;
       item.Value = i.ToString();         
    }
}

這樣,我們使用數組完成了我們期望的效果,雖然這樣實現顯得有點麻煩,雖然還存在上面提到的不便於使用的問題,但這些問題我們耐心細心一點就能克服,而軟件開發幾乎從來就沒有100%完美的解決方案,那我們乾脆就這樣好了。

NOTE:在ddlStatus_DataBound事件中,引發事件的對象sender顯然是DropDownList,但是這裏卻沒有將sender轉換成DropDownList,而是將它轉換成基類型ListControl。這樣做是爲了更好地進行代碼重用,ddlStatus_DataBound事件處理方法將不僅限於 DropDownList,對於繼承自ListControl的其他控件,比如RadioButtonList、ListBox也可以不加改動地使用ddlStatus_DataBound方法。

    如果你對事件綁定還不熟悉,請參考 C#中的委託和事件 一文。

    這裏也可以使用Dictionary<String, Int>來完成,但都存在類似的問題,就不再舉例了。

3.枚舉及其問題

然而不幸的事又發生了... 我們的預訂程序分爲兩部分:一部分爲B/S端,在B/S端可以進行酒店訂單的 創建(未提交)、提交(已提交)、取消提交(已取消),另外還可以看到是不是已訂妥;一部分爲C/S端,爲酒店的預訂中心,它可以進行其他狀態的操作。

此時,對於整個系統來說,應該有全部的7個狀態。但對於B/S端來說,它只有 未提交、已提交、已取消、已訂妥 四個狀態,對應的值分別爲 1、2、3、6。

我們回想一下上面是如何使用數組來解決的,它存在一個缺陷:我們默認地將訂單狀態值與數組的索引一一對應地聯繫了起來。

所以在綁定DropDownList時,我們採用自增的方式來設定列表項的Value值;或者在顯示狀態時,我們通過lbStatus.Text = BookingStatus[myOrder.StatusId]; 這樣的語句來完成。而當這種對應關係被打破時,使用數組的方法就失效了,因爲如果不利用數組索引,我們沒有額外的地方去存儲狀態的數字值。

此時,我們想到了使用枚舉:

public enum BookingStatus {
    未提交 = 1,
    已提交,
    已取消,
    已訂妥 = 6
}

我們想在頁面輸出一個訂單的狀態時,可以這樣:

HotelOrder myOrder = GetItem(orderId);         //獲取一個訂單對象
lbStatus.Text = ((BookingStatus)myOrder.StatusId).ToString(); // 輸出文本值

我們想更新訂單的狀態爲 “已提交”:

myOrder.StatusId = (int)BookingStatus.已提交;

當狀態爲“已取消”時我們想執行某個操作:

if(BookingStatus.已取消 == (BookingStatus)myOrder.StatusId){
    // Do some action
}

此時,VS 2005 的智能提示已經可以發揮完全作用,當我們在BookingStatus後按下“.”時,可以顯示出所有的狀態值。

NOTE:當我們使用枚舉存儲狀態時,myOrder對象的StatusId最好爲BookingStatus枚舉類型,而非int類型,這樣操作會更加便捷一些,但爲了和前面使用數組時的情況保持統一,這裏StatusId仍使用int類型。

以上三種情況使用枚舉都顯得非常的流暢,直到我們需要綁定枚舉到DropDownList下拉列表的時候:我們知道,可以綁定到下拉列表的有兩類對象,一類是實現了IEnumerable接口的可枚舉集合,比如ArrayList,String[],List<T>;一類是實現了IListSource的數據源,比如DataTable,DataSet。

NOTE:實際上IListSource接口的GetList()方法返回一個IList接口,IList接口又繼承了IEnumerable接口。由此看來,IEnumerable是實現可枚舉集合的基礎,在我翻譯的一篇文章 C#中的枚舉器 中,對這個主題做了詳細的討論。

可我們都知道:枚舉enum是一個基本類型,它不會實現任何的接口,那麼我們下來該如何做呢?

4.使用反射遍歷枚舉字段

最笨也是最簡單的辦法,我們可以先創建一個GetDataTable方法,此方法依據枚舉的字段值和數字值構建一個DataTable,最後返回這個構建好的DataTable:

  private static DataTable GetDataTable() {
     DataTable table = new DataTable();
     table.Columns.Add("Name", Type.GetType("System.String"));       //創建列
     table.Columns.Add("Value", Type.GetType("System.Int32"));       //創建列

     DataRow row = table.NewRow();
     row[0] = BookingStatus.未提交.ToString();
     row[1] = 1;
     table.Rows.Add(row);

     row = table.NewRow();
     row[0] = BookingStatus.已提交.ToString();
     row[1] = 2;
     table.Rows.Add(row);

     row = table.NewRow();
     row[0] = BookingStatus.已取消.ToString();
     row[1] = 3;
     table.Rows.Add(row);

     row = table.NewRow();
     row[0] = BookingStatus.已訂妥.ToString();
     row[1] = 6;
     table.Rows.Add(row);

     return table;
 }

接下來,爲了方便使用,我們再創建一個專門採用這個DataTable來設置列表控件的方法SetListCountrol():

// 設置列表
 public static void SetListControl(ListControl list) {
     list.DataSource = GetDataTable();      // 獲取DataTable
     list.DataTextField = "Name";
     list.DataValueField = "Value";
     list.DataBind();
 }

現在,我們就可以在頁面中這樣去將枚舉綁定到列表控件:

protected void Page_Load(object sender, EventArgs e)
{
    SetListControl(ddlStatus);   // 假設頁面中已有ID爲ddlStatus 的DropDownList
}

如果所有的枚舉都要通過這樣去綁定到列表,我覺得還不如在數據庫中直接建表,這樣實在是太麻煩了,而且我們是根據枚舉的文本和值去HardCoding出一個DataTable的:

DataRow row = table.NewRow();
row[0] = BookingStatus.未提交.ToString();
row[1] = 1;
table.Rows.Add(row);

row = table.NewRow();
row[0] = BookingStatus.已提交.ToString();
row[1] = 2;
table.Rows.Add(row);

row = table.NewRow();
row[0] = BookingStatus.已取消.ToString();
row[1] = 3;
table.Rows.Add(row);

row = table.NewRow();
row[0] = BookingStatus.已訂妥.ToString();
row[1] = 6;
table.Rows.Add(row);

這個時候,我們想有沒有辦法通過遍歷來實現這裏?如果想要遍歷這裏,首先,我們需要一個包含枚舉的每個字段信息的對象,這個對象至少包含兩條信息,一個是字段的文本(比如“未提交”),一個是字段的數字型值(比如1),我們暫且管這個對象叫做field。其次,應該存在一個可遍歷的、包含了字段信息的對象(也就是filed) 的集合,我們暫且管這個集合叫做enumFields。

那麼,上面就可以這樣去實現:

foreach (xxxx field in enumFields)
{
    DataRow row = table.NewRow();
    row[0] = field.Name;         // 杜撰的屬性,代表 文本值(比如“未提交”)
    row[1] = filed.intValue;     // 杜撰的屬性,代表 數字值(比如1)

    table.Rows.Add(row);
}

這段代碼很不完整,我們注意到 xxxx,它應該是封裝了字段信息(或者叫元數據metadata)的對象的類型。而對於enumFields,它的類型應該是xxxx這個類型的集合。這段代碼是我們按照思路假想和推導出來的。實際上,.Net 中提供了 Type類 和 System.Reflection命名空間來幫助解決我們現在的問題。

我在後面將較詳細地介紹 Type類,現在只希望你能對反射有個第一印象,所以只簡略地作以說明:Type抽象類提供了訪問類型元數據的能力,當你實例化了一個Type對象後,你可以通過它的屬性和方法,獲取類型的元數據信息,或者進一步獲得該類型的成員的元數據。注意到這裏,因爲Type對象總是基於某一類型的,並且它是一個抽象類,所以我們在創建Type類型時,必須要提供 類型,或者類型的實例,或者類型的字符串值(Part.2會說明)。

創建Type對象有很多種方法,本例中,我們使用typeof操作符來進行,並傳遞BookingStatus枚舉:

Type enumType = typeof(BookingStatus);

然後,我們應該想辦法獲取 封裝了字段信息的對象 的集合。Type類提供 GetFields()方法來實現這一過程,它返回一個 FieldInfo[] 數組。實際上,也就是上面我們enumFields集合的類型。

FieldInfo[] enumFields = enumType.GetFields();

現在,我們就可以遍歷這一集合:

foreach (FieldInfo field in enumFields)
{
    if (!field.IsSpecialName)
    {
       DataRow row = table.NewRow();
       row[0] = field.Name;     // 獲取字段文本值
       row[1] = Convert.ToInt32(myField.GetRawConstantValue()); // 獲取int數值
       table.Rows.Add(row);
    }
}

這裏field的Name屬性獲取了枚舉的文本,GetRawConstantValue()方法獲取了它的int類型的值。

我們看一看完整的代碼:

private static DataTable GetDataTable() {

     Type enumType = typeof(BookingStatus);    // 創建類型
     FieldInfo[] enumFields = enumType.GetFields();    //獲取字段信息對象集合

     DataTable table = new DataTable();
     table.Columns.Add("Name", Type.GetType("System.String"));
     table.Columns.Add("Value", Type.GetType("System.Int32"));
    // 遍歷集合
     foreach (FieldInfo field in enumFields) {
        if (!field.IsSpecialName) {
            DataRow row = table.NewRow();
            row[0] = field.Name;
            row[1] = Convert.ToInt32(field.GetRawConstantValue());
            //row[1] = (int)Enum.Parse(enumType, field.Name); //也可以這樣

            table.Rows.Add(row);
        }
     }

     return table;
 }

注意,SetListControl()方法依然存在並有效,只是爲了節省篇幅,我沒有複製過來,它的使用和之前是一樣的,我們只是修改了GetDataTable()方法。

5.使用泛型來達到代碼重用

觀察上面的代碼,如果我們現在有另一個枚舉,叫做TicketStatus,那麼我們要將它綁定到列表,我們唯一需要改動的就是這裏:

Type enumType = typeof(BookingStatus); //將BookingStatus改作TicketStatus

既然這樣,我們何不定義一個泛型類來進行代碼重用呢?我們管這個泛型類叫做EnumManager<TEnum>。

public static class EnumManager<TEnum>
{
    private static DataTable GetDataTable()
    {
       Type enumType = typeof(TEnum);  // 獲取類型對象
       FieldInfo[] enumFields = enumType.GetFields();

       DataTable table = new DataTable();
       table.Columns.Add("Name", Type.GetType("System.String"));
       table.Columns.Add("Value", Type.GetType("System.Int32"));
       //遍歷集合
       foreach (FieldInfo field in enumFields)
       {
           if (!field.IsSpecialName)
           {
               DataRow row = table.NewRow();
              row[0] = field.Name;
              row[1] = Convert.ToInt32(field.GetRawConstantValue());
              //row[1] = (int)Enum.Parse(enumType, field.Name); 也可以這樣

              table.Rows.Add(row);
           }
       }
       return table;
    }

    public static void SetListControl(ListControl list)
    {
       list.DataSource = GetDataTable();
       list.DataTextField = "Name";
       list.DataValueField = "Value";
       list.DataBind();
    }
}

OK,現在一切都變得簡便的多,以後,我們再需要將枚舉綁定到列表,只要這樣就行了(ddl開頭的是DropDownList,rbl開頭的是RadioButtonList):

EnumManager<BookingStauts>.SetListControl(ddlBookingStatus);
EnumManager<TicketStatus>.SetListControl(rblTicketStatus);

NOTE:如果你對泛型不熟悉,請參閱 C# 中的泛型 一文。上面的實現並沒有考慮到性能的問題,僅僅爲了引出反射使用的一個實例。

6 .Net 中反射的一個範例。

不管是VS2005的智能提示,還是修改變量名時的重構功能,都使用了反射功能。在.Net FCL中,也經常能看到反射的影子,這裏就向大家演示一個最常見的例子。大家知道,在CLR中一共有兩種類型,一種是值類型,一種是引用類型。聲明一個引用類型的變量並對類型實例化,會在應用程序堆(Application Heap)上分配內存,創建對象實例,然後將對象實例的內存地址返回給變量,變量保存的是內存地址,實際相當於一個指針;聲明一個值類型的實例變量,則會將它分配在線程堆棧(Thread Stack)上,變量本身包含了值類型的所有字段。

現在假設我們需要比較兩個對象是否相等。當我們比較兩個引用類型的變量是否相等時,我們比較的是這兩個變量所指向的是不是堆上的同一個實例(內存地址是否相同)。而當我們比較兩個值類型變量是否相等時,怎麼做呢?因爲變量本身就包含了值類型所有的字段(數據),所以在比較時,就需要對兩個變量的字段進行逐個的一對一的比較,看看每個字段的值是否都相等,如果任何一個字段的值不等,就返回false。

實際上,執行這樣的一個比較並不需要我們自己編寫代碼,Microsoft已經爲我們提供了實現的方法:所有的值類型繼承自 System.ValueType, ValueType和所有的類型都繼承自System.Object ,Object提供了一個Equals()方法,用來判斷兩個對象是否相等。但是ValueType覆蓋了Object的Equals()方法。當我們比較兩個值類型變量是否相等時,可以調用繼承自ValueType類型的Equals()方法。

public struct ValPoint {
    public int x;
    public int y;
}
static void Main(string[] args) {
    bool result;

    ValPoint A1;
    A1.x = A1.y = 3;

    ValPoint B1 = A1;            // 複製A的值給B
    result = A1.Equals(B1);
    Console.WriteLine(result);      // 輸出 True;
}

你有沒有想到當調用Equals()方法時會發生什麼事呢?前面我們已經提到如果是值類型,會對兩個變量的字段進行逐個的比較,看看每個字段的值是否都相等,但是如何獲取變量的所有字段,遍歷字段,並逐一比較呢?此時,你應該意識到又到了用到反射的時候了,讓我們使用reflector來查看ValueType類的Equals()方法,看看微軟是如何做的吧:

public override bool Equals(object obj) {
    if (obj == null) {
       return false;
    }
    RuntimeType type = (RuntimeType)base.GetType();
    RuntimeType type2 = (RuntimeType)obj.GetType();
    if (type2 != type) {
       return false;
    }
    object a = this;
    if (CanCompareBits(this)) {
       return FastEqualsCheck(a, obj);
    }
    // 獲取所有實體字段
    FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    // 遍歷字段,判斷字段值是否相等
    for (int i = 0; i < fields.Length; i++) {
       object obj3 = ((RtFieldInfo)fields[i]).InternalGetValue(a, false);
       object obj4 = ((RtFieldInfo)fields[i]).InternalGetValue(obj, false);
       if (obj3 == null) {
           if (obj4 != null) {
              return false;
           }
       } else if (!obj3.Equals(obj4)) {
           return false;
       }
    }
    return true;
}

注意到上面加註釋的那兩段代碼,可以看到當對值變量進行比較時,是會使用反射來實現。反射存在着性能不佳的問題(不僅如此,還存在着很多的裝箱操作),由此可見,在值類型上調用Equals()方法開銷是會很大的。但是這個例子僅僅爲了說明反射的用途,我想已經達到了目的。上面的代碼不能完全理解也不要緊,後面會再提到。

7.小結

看到這裏,你應該對反射有了一個初步的概念(或者叫反射的一個用途):反射是一種寬泛的叫法,它通過 System.Reflection 命名空間 並 配合 System.Type 類,提供了在運行時(Runtime)對於 類型和對象(及其成員)的基本信息 以及 元數據(metadata)的訪問能力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章