天天看點

.Net中的反射(動态建立類型執行個體) - Part.4.Net 中的反射(動态建立類型執行個體) - Part.4

.Net 中的反射(動态建立類型執行個體) - Part.4

動态建立對象

在前面節中,我們先了解了反射,然後利用反射檢視了類型資訊,并學習了如何建立自定義特性,并利用反射來周遊它。可以說,前面三節,我們學習的都是

反射是什麼

,在接下來的章節中,我們将學習

反射可以做什麼

。在進行更有趣的話題之前,我們先看下如何動态地建立一個對象。

我們建立一個Console控制台項目,叫做Reflection4(因為本文是Part4,你也可以起别的名字)。然後,添加一個示範類,本文中将通過對這個示範類的操作來進行說明:

public class Calculator {

    private int x;

    private int y;

    public Calculator(){

       x = 0;

       y = 0;

    }

    public Calculator(int x, int y) {

       this.x = x;

       this.y = y;

}

1.使用無參數構造函數建立對象

上面這個類非常簡單,它包含兩個構造函數,一個是有參數的構造函數,一個是無參數的構造函數,我們先看看通過反射,使用無參數的構造函數建立對象。建立對象通常有兩種方式,一種是使用Assembly的CreateInstance方法:

Assembly asm = Assembly.GetExecutingAssembly();          

Object obj = asm.CreateInstance("Reflection4.Calculator", true);

// 輸出:Calculator() invoked

CreateInstance的第一個參數代表了要建立的類型執行個體的字元串名稱,第二個參數說明是不是大小寫無關(Ignore Case)。注意到CreateInstance傳回的是一個Object對象,意味着如果想使用這個對象,需要進行一次類型轉換。

建立對象的另一種方式是調用Activator類的靜态方法CreateInstance:

ObjectHandle handler = Activator.CreateInstance(null, "Reflection4.Calculator");

Object obj = handler.Unwrap();

其中CreateInstance的第一個參數說明是程式集的名稱,為null時表示目前程式集;第二個參數說明要建立的類型名稱。Activator.CreateInstance傳回的是一個ObjectHandle對象,必須進行一次Unwrap()才能傳回Object類型,進而可以強制轉換成我們需要的類型(本例中是Calculator)。ObjectHandle包含在System.Runtime.Remoting命名空間中,可見它是Remoting相關的,實際上ObjectHandle類隻是一個對原類型進行了一個包裝以便進行封送,更多内容可以參見

.Net Remoting(應用程式域)—Part.1

這篇文章。

2.使用有參數構造函數建立對象

如果我們想通過有參數的構造函數建立對象,我們可以使用Assembly的CreateInstanc()的重載方法:

// 有參數構造函數建立對象

Assembly asm = Assembly.GetExecutingAssembly();

Object[] parameters = new Object[2];    // 定義構造函數需要的參數

parameters[0] = 3;

parameters[1] = 5;

Object obj = asm.CreateInstance("Reflection4.Calculator", true, BindingFlags.Default, null, parameters, null, null);

// 輸出:Calculator(int x, int y) invoked

我們看一下CreateInstance需要提供的參數:

  1. 前兩個在前一小節已經說明過了;
  2. BindingFlags在前面我們也用到過,它用于限定對類型成員的搜尋。在這裡指定Default,意思是不使用BingdingFlags的政策(你可以把它了解成null,但是BindingFlags是值類型,是以不可能為null,必須有一個預設值,而這個Default就是它的預設值);
  3. 接下來的參數是Binder,它封裝了CreateInstance綁定對象(Calculator)的規則,我們幾乎永遠都會傳遞null進去,實際上使用的是預定義的DefaultBinder;
  4. 接下來是一個Object[]數組類型,它包含我們傳遞進去的參數,有參數的構造函數将會使用這些參數;
  5. 接下來的參數是一個CultureInfo類型,它包含了關于語言和文化的資訊(簡單點了解就是什麼時候ToString("c")應該顯示“¥”,什麼時候應該顯示“$”)。

動态調用方法

接下來我們看一下如何動态地調用方法。注意,本文讨論的調用不是将上面動态建立好的對象由Object類型轉換成Calculator類型再進行方法調用,這和“正常調用”就沒有差別了,讓我們以.Net Reflection 的方式來進行方法的調用。繼續進行之前,我們為Calculator添加兩個方法,一個執行個體方法,一個靜态方法:

public int Add(){

    int total= 0;

    total = x + y;

    Console.WriteLine("Invoke Instance Method: ");

    Console.WriteLine(String.Format("[Add]: {0} plus {1} equals to {2}", x, y, total));

    return total;

public static void Add(int x, int y){

    int total = x + y;

    Console.WriteLine("Invoke Static Method: ");

調用方法的方式一般有兩種:

  1. 在類型的Type對象上調用InvokeMember()方法,傳遞想要在其上調用方法的對象(也就是剛才動态建立的Calculator類型執行個體),并指定BindingFlags為InvokeMethod。根據方法簽名,可能還需要傳遞參數。
  2. 先通過Type對象的GetMethond()方法,擷取想要調用的方法對象,也就是MethodInfo對象,然後在該對象上調用Invoke方法。根據方法簽名,可能還需要傳遞參數。

需要說明的是,使用InvokeMember不限于調用對象的方法,也可以用于擷取對象的字段、屬性,方式都是類似的,本文隻說明最常見的調用方法。

1.使用InvokeMember調用方法

我們先看第一種方法,代碼很簡單,隻需要兩行(注意obj在上節已經建立,是Calculator類型的執行個體):

Type t = typeof(Calculator);

int result = (int)t.InvokeMember("Add", BindingFlags.InvokeMethod, null, obj, null);

Console.WriteLine(String.Format("The result is {0}", result));

輸出:

Invoke Instance Method:

[Add]: 3 plus 5 equals to 8

The result is 8

在InvokeMember方法中,第一個參數說明了想要調用的方法名稱;第二個參數說明是調用方法(因為InvokeMember的功能非常強大,不光是可以調用方法,還可以擷取/設定 屬性、字段等。此枚舉的詳情可參看Part.2或者MSDN);第三個參數是Binder,null說明使用預設的Binder;第四個參數說明是在這個對象上(obj是Calculator類型的執行個體)進行調用;最後一個參數是數組類型,表示的是方法所接受的參數。

我們在看一下對于靜态方法應該如何調用:

Object[] parameters2 = new Object[2];

parameters2[0] = 6;

parameters2[1] = 9;

t.InvokeMember("Add", BindingFlags.InvokeMethod, null, typeof(Calculator), parameters2);

Invoke Static Method:

[Add]: 6 plus 9 equals to 15

我們和上面對比一下:首先,第四個參數傳遞的是 typeof(Calculator),不再是一個Calculator執行個體類型,這很容易了解,因為我們調用的是一個靜态方法,它不是基于某個具體的類型執行個體的,而是基于類型本身;其次,因為我們的靜态方法需要提供兩個參數,是以我們以數組的形式将這兩個參數進行了傳遞。

2.使用MethodInfo.Invoke調用方法

我們再看下第二種方式,先獲得一個MethodInfo執行個體,然後調用這個執行個體的Invoke方法,我們看下具體如何做:

MethodInfo mi = t.GetMethod("Add", BindingFlags.Instance | BindingFlags.Public);

mi.Invoke(obj, null);

請按任意鍵繼續. . .

在代碼的第二行,我們先使用GetMethod方法擷取了一個方法對象MethodInfo,指定BindingFlags為Instance和Public,因為有兩個方法都命名為“Add”,是以在這裡指定搜尋條件是必須的。接着我們使用Invoke()調用了Add方法,第一個參數obj是前面建立的Calculator類型執行個體,表明在該執行個體上建立方法;第二個參數為null,說明方法不需要提供參數。

我們再看下如何使用這種方式調用靜态方法:

MethodInfo mi = t.GetMethod("Add", BindingFlags.Static | BindingFlags.Public);

mi.Invoke(null, parameters2);

// mi.Invoke(t, parameters2);   也可以這樣

可以看到與上面的大同小異,在GetMethod()方法中,我們指定為搜尋BindingFlags.Static,而不是BindingFlags.Public,因為我們要調用的是靜态的Add方法。在Invoke()方法中,需要注意的是第一個參數,不能在傳遞Calculator類型執行個體,而應該傳遞Calculator的Type類型或者直接傳遞null。因為靜态方法不是屬于某個執行個體的。

NOTE

:通過上面的例子可以看出:使用反射可以達到最大程度上的多态,舉個例子,你可以在頁面上放置一個DropDownList控件,然後指定它的Items的value為你某個類的方法的名稱,然後在SelectedIndexChanged事件中,利用value的值來調用類的方法。而在以前,你隻能寫一些if else 語句,先判斷DropDownList傳回的值,根據值再決定調用哪個方法。使用這種方式,編譯器在代碼運作之前(或者說使用者選擇了某個選項之前)完全不知道哪個方法将被調用,這也就是常說的

遲綁定(Late Binding)

Coding4Fun:周遊System.Drawing.Color結構

我們已經講述了太多的基本方法和理論,現在讓我們來做一點有趣的事情:大家知道在Asp.Net中控件的顔色設定,比如說ForeColor, BackColor等,都是一個System.Draw.Color結構類型。在某些情況下我們需要使用自定義的顔色,那麼我們會使用類似這樣的方式Color.FromRgb(125,25,13)建立一個顔色值。但有時候我們會覺得比較麻煩,因為這個數字太不直覺了,我們甚至需要把這個值貼到PhotoShop中看看是什麼樣的。

這時候,我們可能會想要使用Color結構提供的預設顔色,也就是它的141個靜态屬性,但是這些值依然是以名稱,比如DarkGreen的形式給出的,還是不夠直覺,如果能把它們以色塊的形式輸出到頁面就好了,這樣我們檢視起來會友善的多,以後使用也會比較便利。我已經實作了它,可以點選下面的連結檢視:

效果預覽:

http://www.tracefact.net/demo/reflection/color.aspx

基本實作

現在我們來看一下實作過程:

先建立頁面Color.aspx(或其他名字),然後在Head裡添加些樣式控制頁面顯示,再拖放一個Panel控件進去。樣式表需要注意的是#pnColors div部分,它定義了頁面上将顯示的色塊的樣式;Id為pnHolder的Panel控件用于裝載我們動态生成的div。

<head>

<style type="text/css">

body{font-size:14px;}

h1{font-size:26px;}

#pnColors div{

    float:left;width:140px;

    padding:7px 0;

    text-align:center;

    margin:3px;

    border:1px solid #aaa;

    font-size:11px;

    font-family:verdana, arial

</style>

</head>

<body>

    <h1>Coding4Fun:使用反射周遊System.Drawing.Color結構</h1>

    <form id="form1" runat="server">

       <asp:Panel ID="pnColors" runat="server"></asp:Panel>

    </form>

</body>

:如果将頁面命名為了 Color.aspx,那麼需要在代碼後置檔案中修改類名,比如改成:Reflection_Color,同時頁面頂部也需要修改成Inherits="Reflection_Color",不然會出現命名沖突的問題。

下一步的思路是這樣的:我們在phColors中添加一系列的div,這些div也就是頁面上我們将要顯示的色塊。我們設定div的文本為 顔色的名稱 和 RGB數值,它的背景色我們設為相應的顔色(色塊的其他樣式,比如寬、邊框、寬度已經在head中定義)。我們知道在Asp.Net中,并沒有一個Div控件,隻有HtmlGenericControl,此時,我們最好定義一個Div讓它繼承自HtmlGenericControl。

public class Div:HtmlGenericControl

{

    private string name;

    public Div(Color c)

       : base("div")     // 調用基類構造函數,建立一個 Div

    {

       this.name = c.Name;      // 顔色名稱

       // 設定文本

       this.InnerHtml = String.Format("{0}<br />RGB({1},{2},{3})", name, c.R, c.G, c.B);

       int total = c.R + c.G + c.B;

       if (total <= 255)     // 如果底色太暗,前景色改為明色調

           this.Style.Add("color", "#eee");

       // 設定背景顔色

       this.Style.Add("background", String.Format("rgb({0},{1},{2})", c.R, c.G, c.B));

如同我們前面所描述的,這個Div接受一個Color類型作為構造函數的參數,然後在構造函數中,先設定了它的文本為 顔色名稱 和 顔色的各個數值(通過Color結構的R, G, B屬性獲得)。然後設定了div的背景色為相應的RGB顔色。

:在上面 if(total<=255)那裡,可能有的顔色本身就很暗,如果這種情況再使用黑色的前景色那麼文字會看不清楚,是以我添加了判斷,如果背景太暗,就将前景色調的明亮一點。

OK,現在我們到後置代碼中隻要做一點點的工作就可以了:

protected void Page_Load(object sender, EventArgs e)

    List<Div> list = new List<Div>();

    Type t = typeof(Color);      // 頁首已經包含了 using System.Drawing;

    // 擷取屬性

    PropertyInfo[] properties = t.GetProperties(BindingFlags.Static | BindingFlags.Public);

    Div div;

    // 周遊屬性

    foreach (PropertyInfo p in properties)

       // 動态獲得屬性

       Color c;         

       c = (Color)t.InvokeMember(p.Name, BindingFlags.GetProperty, null, typeof(Color), null);       

       div = new Div(c);

       list.Add(div);

    foreach (Div item in list)   {

       pnColors.Controls.Add(item);

上面的代碼是很直白的:先建立一個Div清單,用于儲存即将建立的色塊。然後擷取Color類型的Type執行個體。接着我們使用GetProperties()方法,并指定BindingFlags擷取所有的靜态公共屬性。然後周遊屬性,并使用InvokeMember()方法擷取了屬性值,因為傳回的是一個Object類型,是以我們需要把它強制轉換成一個Color類型。注意在這裡InvokeMember的BindingFlags指定為GetProperty,意為擷取屬性值。第四個參數為typeof(Color),因為顔色屬性(比如DarkGreen)是靜态的,不是針對于某個執行個體的,如果是執行個體,則需要傳遞調用此屬性的類型執行個體。最後,我們根據顔色建立div,并将它加入清單,周遊清單并逐一加入到Id為pnColors的Panal控件中。

現在已經OK了,如果打開頁面,應該可以看到類似這樣的效果:

.Net中的反射(動态建立類型執行個體) - Part.4.Net 中的反射(動态建立類型執行個體) - Part.4

為清單排序

上面的頁面看上去會比較亂,因為清單大緻是按顔色名稱排序的(Transparnet例外),我們最好可以讓清單基于顔色進行排序。關于清單排序,我在

基于業務對象的排序

一文中已經非常詳細地進行了讨論,是以這裡我僅給出實作過程,而不再進行講述。這一小節與反射無關,如果你對排序已經非常熟悉,可以跳過。

在頁面上添加一個RadioButtonList控件,将AutoPostBack設為true,我們要求可以按名稱和顔色值兩種方式進行排序:

排序:

<asp:RadioButtonList ID="rblSort" runat="server" AutoPostBack="true" RepeatDirection="Horizontal" RepeatLayout="Flow">

    <asp:ListItem Selected="True">Name</asp:ListItem>

     <asp:ListItem>Color</asp:ListItem>       

</asp:RadioButtonList>

在後置代碼中,添加一個枚舉作為排序的依據:

public enum SortBy{

    Name,         // 按名稱排序

    Color         // 暗顔色值排序

修改Div類,添加 ColorValue字段,這個字段代表顔色的值,并建立嵌套類型ColorComparer,以及方法GetComparer:

    private int colorValue;

       this.colorValue =        // 顔色的色彩值

           c.R * 256 * 256 + c.G * 256 + c.B;

    // 傳回一個Comparer()用于排序

    public static ColorComparer GetComparer(SortBy sort) {

       return new ColorComparer(sort);

    // 預設以名稱排序

    public static ColorComparer GetComparer() {

       return GetComparer(SortBy.Name);

    // 嵌套類型,用于排序

    public class ColorComparer : IComparer<Div>

       private SortBy sort;

       public ColorComparer(SortBy sort) {

           this.sort = sort;

       }

       // 實作IComparer<T>接口,根據sort判斷以何為依據一進排序

       public int Compare(Div x, Div y)

       {

           if (sort == SortBy.Name)

              return String.Compare(x.name, y.name);

           else

              return x.colorValue.CompareTo(y.colorValue);

在Page_Load事件上面,我們添加語句,擷取目前的排序依據(枚舉):

SortBy sort;

if (!IsPostBack) {

    sort = SortBy.Name;

} else {

    sort = (SortBy)Enum.Parse(typeof(SortBy), rblSort.SelectedValue);

在将清單輸出到頁面之前,我們調用清單的Sort方法:

list.Sort(Div.GetComparer(sort));   // 對清單進行排序

foreach (Div item in list)   {

    pnColors.Controls.Add(item);

好了,所有工作都完成了,再次打開頁面,可以看到類似如下畫面,我們可以按照名稱或者顔色值來對清單進行排序顯示:

.Net中的反射(動态建立類型執行個體) - Part.4.Net 中的反射(動态建立類型執行個體) - Part.4

總結

本文分三個部分講述了.Net中反射的一個應用:動态建立對象和調用對象方法(屬性、字段)。我們先學習最常見的動态建立對象的兩種方式,随後分别讨論了使用Type.InvokeMember()和MethodInfo.Invoke()方法來調用類型的執行個體方法和靜态方法。最後,我們使用反射周遊了System.Drawing.Color結構,并輸出了顔色值。

感謝閱讀,希望這篇文章能給你帶來幫助!

繼續閱讀