.NET基礎拾遺(4)委托、事件、反射與特性


 Index :

 (1)類型語法、內存管理和垃圾回收基礎

 (2)面向對象的實現和異常的處理基礎

 (3)字符串、集合與流

 (4)委托、事件、反射與特性

 (5)多線程開發基礎

 (6)ADO.NET與數據庫開發基礎

 (7)WebService的開發與應用基礎

一、委托基礎

1.1 簡述委托的基本原理

  委托這個概念對C++程序員來說並不陌生,因為它和C++中的函數指針非常類似,很多碼農也喜歡稱委托為安全的函數指針。無論這一說法是否正確,委托的的確確實現了和函數指針類似的功能,那就是提供了程序回調指定方法的機制

  在委托內部,包含了一個指向某個方法的指針(這一點上委托實現機制和C++的函數指針一致),為何稱其為安全的呢?因此委托和其他.NET成員一樣是一種類型,任何委托對象都是繼承自System.Delegate的某個派生類的一個對象,下圖展示了在.NET中委托的類結構:

  從上圖也可以看出,任何自定義的委托都繼承自基類System.Delegate,在這個類中,定義了大部分委托的特性。那么,下面可以看看在.NET中如何使用委托:

    // 定義的一個委托
    public delegate void TestDelegate(int i);

    public class Program
    {
        public static void Main(string[] args)
        {
            // 定義委托實例
            TestDelegate td = new TestDelegate(PrintMessage);
            // 調用委托方法
            td(0);
            td.Invoke(1);

            Console.ReadKey();
        }

        public static void PrintMessage(int i)
        {
            Console.WriteLine("這是第{0}個方法!", i.ToString());
        }
    }
View Code

  運行結果如下圖所示:

  

  上述代碼中定義了一個名為TestDelegate的新類型,該類型直接繼承自System.MulticastDelegate,而且其中會包含一個名為Invoke、BeginInvoke和EndInvoke的方法,這些步驟都是由C#編譯器自動幫我們完成的,可以通過Reflector驗證一下如下圖所示:

  需要注意的是,委托既可以接受實例方法,也可以接受靜態方法(如上述代碼中接受的就是靜態方法),其區別我們在1.2中詳細道來。最后,委托被調用執行時,C#編譯器可以接收一種簡化程序員設計的語法,例如上述代碼中的:td(1)。但是,本質上,委托的調用其實就是執行了在定義委托時所生成的Invoke方法。

1.2 委托回調靜態方法和實例方法有何區別?

  首先,我們知道靜態方法可以通過類名來訪問而無需任何實例對象,當然在靜態方法中也就不能訪問類型中任何非靜態成員。相反,實例方法則需要通過具體的實例對象來調用,可以訪問實例對象中的任何成員。

  其次,當一個實例方法被調用時,需要通過實例對象來訪問,因此可以想象當綁定一個實例方法到委托時必須同時讓委托得到實例方法的代碼段和實例對象的信息,這樣在委托被回調的時候.NET才能成功地執行該實例方法。

  下圖展示了委托內部的主要結構:

  ① _target是一個指向目標實例的引用,當綁定一個實例方法給委托時,該參數會作為一個指針指向該方法所在類型的一個實例對象。相反,當綁定一個靜態方法時,該參數則被設置為null。

  ② _methodPtr則是一個指向綁定方法代碼段的指針,這一點和C++的函數指針幾乎一致。綁定靜態方法或實例方法在這個成員的設置上並沒有什么不同。

  System.MulticastDelegate在內部結構上相較System.Delegate增加了一個重要的成員變量:_prev,它用於指向委托鏈中的下一個委托,這也是實現多播委托的基石。

1.3 神馬是鏈式委托?

  鏈式委托也被稱為“多播委托”,其本質是一個由多個委托組成的鏈表。回顧上面1.2中的類結構,System.MulticastDelegate類便是為鏈式委托而設計的。當兩個及以上的委托被鏈接到一個委托鏈時,調用頭部的委托將導致該鏈上的所有委托方法都被執行

  下面看看在.NET中,如何申明一個鏈式委托:

    // 定義的一個委托
    public delegate void TestMulticastDelegate();

    public class Program
    {
        public static void Main(string[] args)
        {
            // 申明委托並綁定第一個方法
            TestMulticastDelegate tmd = new TestMulticastDelegate(PrintMessage1);
            // 綁定第二個方法
            tmd += new TestMulticastDelegate(PrintMessage2);
            // 綁定第三個方法
            tmd += new TestMulticastDelegate(PrintMessage3);
            // 調用委托
            tmd();

            Console.ReadKey();
        }

        public static void PrintMessage1()
        {
            Console.WriteLine("調用第1個PrintMessage方法");
        }

        public static void PrintMessage2()
        {
            Console.WriteLine("調用第2個PrintMessage方法");
        }

        public static void PrintMessage3()
        {
            Console.WriteLine("調用第3個PrintMessage方法");
        }
    }
View Code

  其運行結果如下圖所示:

  

  可以看到,調用頭部的委托導致了所有委托方法的執行。通過前面的分析我們也可以知道:為委托+=增加方法以及為委托-=移除方法讓我們看起來像是委托被修改了,其實它們並沒有被修改。事實上,委托是恆定的。在為委托增加和移除方法時實際發生的是創建了一個新的委托,其調用列表是增加和移除后的方法結果。

MulticastDelegate

  另一方面,+= 或-= 這是一種簡單明了的寫法,回想在WindowsForm或者ASP.NET WebForms開發時,當添加一個按鈕事件,VS便會自動為我們生成類似的代碼,這樣一想是不是又很熟悉了。

  現在,我們再用一種更簡單明了的方法來寫:

    TestMulticastDelegate tmd = PrintMessage1;
    tmd += PrintMessage2;
    tmd += PrintMessage3;
    tmd();
View Code

  其執行結果與上圖一致,只不過C#編譯器的智能化已經可以幫我們省略了很多代碼。

  最后,我們要用一種比較復雜的方法來寫,但是卻是鏈式委托的核心所在:

    TestMulticastDelegate tmd1 = new         TestMulticastDelegate(PrintMessage1);
    TestMulticastDelegate tmd2 = new  TestMulticastDelegate(PrintMessage2);
    TestMulticastDelegate tmd3 = new     TestMulticastDelegate(PrintMessage3);
    // 核心本質:將三個委托串聯起來
    TestMulticastDelegate tmd = tmd1 + tmd2 + tmd3;
    tmd.Invoke();
View Code

  我們在實際開發中經常使用第二種方法,但是卻不能不了解方法三,它是鏈式委托的本質所在。

1.4 鏈式委托的執行順序是怎么樣的?

  前面我們已經知道鏈式委托的基本特性就是一個以委托組成的鏈表,而當委托鏈上任何一個委托方法被調用時,其后面的所有委托方法都將會被依次地順序調用。那么問題來了,委托鏈上的順序是如何形成的?這里回顧一下上面1.3中的示例代碼,通過Reflector反編譯一下,一探究竟:

  從編譯后的結果可以看到,+=的本質又是調用了Delegate.Combine方法,該方法將兩個委托鏈接起來,並且把第一個委托放在第二個委托之前,因此可以將兩個委托的相加理解為Deletegate.Combine(Delegate a,Delegate b)的調用。我們可以再次回顧System.MulticastDelegate的類結構:

  其中_prev成員是一個指向下一個委托成員的指針,當某個委托被鏈接到當前委托的后面時,該成員會被設置為指向那個后續的委托實例。.NET也是依靠這一個引用來逐一找到當前委托的所有后續委托並以此執行方法。

  那么,問題又來了?程序員能夠有能力控制鏈式委托的執行順序呢?也許我們會說,只要在定義時按照需求希望的順序來依次添加就可以了。但是,如果要在定義完成之后突然希望改變執行順序呢?又或者,程序需要按照實際的運行情況再來決定鏈式委托的執行順序呢?

  接下來就是見證奇跡的時刻:

    // 申明委托並綁定第一個方法
    TestMulticastDelegate tmd = new TestMulticastDelegate(PrintMessage1);
    // 綁定第二個方法
    tmd += new TestMulticastDelegate(PrintMessage2);
    // 綁定第三個方法
    tmd += new TestMulticastDelegate(PrintMessage3);
    // 獲取所有委托方法
    Delegate[] dels = tmd.GetInvocationList();

  上述代碼調用了定義在System.MulticastDelegate中的GetInvocationList()方法,用以獲得整個鏈式委托中的所有委托。接下來,我們就可以按照我們所希望的順序去執行它們。

1.5 可否定義有返回值方法的委托鏈?

  委托的方法既可以是無返回值的,也可以是有返回值的,但如果多一個帶返回值的方法被添加到委托鏈中時,我們需要手動地調用委托鏈上的每個方法,否則只能得到委托鏈上最后被調用的方法的返回值

  為了驗證結論,我們可以通過如下代碼進行演示:

    // 定義一個委托
    public delegate string GetStringDelegate();

    class Program
    {
        static void Main(string[] args)
        {
            // GetSelfDefinedString方法被最后添加
            GetStringDelegate myDelegate1 = GetDateTimeString;
            myDelegate1 += GetTypeNameString;
            myDelegate1 += GetSelfDefinedString;
            Console.WriteLine(myDelegate1());
            Console.WriteLine();
            // GetDateTimeString方法被最后添加
            GetStringDelegate myDelegate2 = GetSelfDefinedString;
            myDelegate2 += GetTypeNameString;
            myDelegate2 += GetDateTimeString;
            Console.WriteLine(myDelegate2());
            Console.WriteLine();
            // GetTypeNameString方法被最后添加
            GetStringDelegate myDelegate3 = GetSelfDefinedString;
            myDelegate3 += GetDateTimeString;
            myDelegate3 += GetTypeNameString;
            Console.WriteLine(myDelegate3());
            
            Console.ReadKey();
        }

        static string GetDateTimeString()
        {
            return DateTime.Now.ToString();
        }

        static string GetTypeNameString()
        {
            return typeof(Program).ToString();
        }

        static string GetSelfDefinedString()
        {
            string result = "我是一個字符串!";
            return result;
        }
    }
View Code

  其運行結果如下圖所示:

  

  從上圖可以看到,雖然委托鏈中的所有方法都被正確執行,但是我們只得到了最后一個方法的返回值。在這種情況下,我們應該如何得到所有方法的返回值呢?回顧剛剛提到的GetInvocationList()方法,我們可以利用它來手動地執行委托鏈中的每個方法。

    GetStringDelegate myDelegate1 = GetDateTimeString;
    myDelegate1 += GetTypeNameString;
    myDelegate1 += GetSelfDefinedString;
    foreach (var del in myDelegate1.GetInvocationList())
    {
          Console.WriteLine(del.DynamicInvoke());
     }
View Code

  通過上述代碼,委托鏈中每個方法的返回值都不會丟失,下圖是執行結果:

  

1.6 簡述委托的應用場合

  委托的功能和其名字非常類似,在設計中其思想在於將工作委派給其他特定的類型、組件、方法或程序集。委托的使用者可以理解為工作的分派者,在通常情況下使用者清楚地知道哪些工作需要執行、執行的結果又是什么,但是他不會親自地去做這些工作,而是恰當地把這些工作分派出去。

  這里,我們假設要寫一個日志子系統,該子系統的需求是使用者希望的都是一個單一的方法傳入日志內容和日志類型,而日志子系統會根據具體情況來進行寫日志的動作。對於日志子系統的設計者來說,寫一條日志可能需要包含一系列的工作,而日志子系統決定把這些工作進行適當的分派,這時就需要使用一個委托成員。

  下面的代碼展示了該日志子系統的簡單實現方式:

  ① 定義枚舉:日志的類別

    public enum LogType
    {
        Debug,
        Trace,
        Info,
        Warn,
        Error
    }
View Code

  ② 定義委托,由日志使用者直接執行來完成寫日志的工作

  public delegate void Log(string content, LogType type);

  ③ 定義日志管理類,在構造方法中為記錄日志委托定義了默認的邏輯(這里采用了部分類的書寫,將各部分的委托方法分隔開,便於理解)

    public sealed partial class LogManager:IDisposable
    {
        private Type _componentType;
        private String _logfile;
        private FileStream _fs;
        public Log WriteLog;         //用來寫日志的委托
        //
        private static object mutext = new object();
        //嚴格控制無參的構造方法
        private LogManager()
        {
            WriteLog = new Log(PrepareLogFile);
            WriteLog += OpenStream; //打開流
            WriteLog += AppendLocalTime;    //添加本地時間
            WriteLog += AppendSeperator;    //添加分隔符
            WriteLog += AppendComponentType;//添加模塊類別
            WriteLog += AppendSeperator;    //添加分隔符
            WriteLog += AppendType;         //添加日志類別
            WriteLog += AppendSeperator;    //添加分隔符
            WriteLog += AppendContent;      //添加內容
            WriteLog += AppendNewLine;      //添加回車
            WriteLog += CloseStream;        //關閉流
        }
        /// <summary>
        /// 構造方法
        /// </summary>
        /// <param name="type">使用該日志的類型</param>
        /// <param name="file">日志文件全路徑</param>
        public LogManager(Type type, String file):this()
        {
            _logfile = file;
            _componentType = type;
            
        }
        /// <summary>
        /// 釋放FileStream對象
        /// </summary>
        public void Dispose()
        {
            if (_fs != null)
                _fs.Dispose();
            GC.SuppressFinalize(this);
        }
        ~LogManager()
        {
            if (_fs != null)
                _fs.Dispose();
        }

    }

    /// <summary>
    /// 委托鏈上的方法(和日志文件有關的操作)
    /// </summary>
    public sealed partial class LogManager:IDisposable
    {
        /// <summary>
        /// 如果日志文件不存在,則新建日志文件
        /// </summary>
        private void PrepareLogFile(String content, LogType type)
        {
            //只允許單線程創建日志文件
            lock(mutext)
            {
                if (!File.Exists(_logfile))
                    using (FileStream fs = File.Create(_logfile))
                    { }
            }
        }
        /// <summary>
        /// 打開文件流
        /// </summary>
        private void OpenStream(String content, LogType type)
        {
            _fs = File.Open(_logfile, FileMode.Append);
        }
        /// <summary>
        /// 關閉文件流
        /// </summary>
        private void CloseStream(String content, LogType type)
        {
            _fs.Close();
            _fs.Dispose();
        }
    }

    /// <summary>
    /// 委托鏈上的方法(和日志時間有關的操作)
    /// </summary>
    public sealed partial class LogManager : IDisposable
    {
        /// <summary>
        /// 為日志添加當前UTC時間
        /// </summary>
        private void AppendUTCTime(String content, LogType type)
        {
            String time=DateTime.Now.ToUniversalTime().ToString();
            Byte[] con = Encoding.Default.GetBytes(time);
            _fs.Write(con, 0, con.Length);
        }
        /// <summary>
        /// 為日志添加本地時間
        /// </summary>
        private void AppendLocalTime(String content, LogType type)
        {
            String time = DateTime.Now.ToLocalTime().ToString();
            Byte[] con = Encoding.Default.GetBytes(time);
            _fs.Write(con, 0, con.Length);
        }
    }

    /// <summary>
    /// 委托鏈上的方法(和日志內容有關的操作)
    /// </summary>
    public sealed partial class LogManager : IDisposable
    {
        /// <summary>
        /// 添加日志內容
        /// </summary>
        private void AppendContent(String content, LogType type)
        {
            Byte[] con = Encoding.Default.GetBytes(content);
            _fs.Write(con, 0, con.Length);
        }
        /// <summary>
        /// 為日志添加組件類型
        /// </summary>
        private void AppendComponentType(String content, LogType type)
        {
            Byte[] con = Encoding.Default.GetBytes(_componentType.ToString());
            _fs.Write(con, 0, con.Length);
        }
        /// <summary>
        /// 添加日志類型
        /// </summary>
        private void AppendType(String content, LogType type)
        {
            String typestring = String.Empty;
            switch (type)
            {
                case LogType.Debug:
                    typestring = "Debug";
                    break;
                case LogType.Error:
                    typestring = "Error";
                    break;
                case LogType.Info:
                    typestring = "Info";
                    break;
                case LogType.Trace:
                    typestring = "Trace";
                    break;
                case LogType.Warn:
                    typestring = "Warn";
                    break;
                default:
                    typestring = "";
                    break;
            }
            Byte[] con = Encoding.Default.GetBytes(typestring);
            _fs.Write(con, 0, con.Length);
        }
    }

    /// <summary>
    /// 委托鏈上的方法(和日志的格式控制有關的操作)
    /// </summary>
    public sealed partial class LogManager : IDisposable
    {
        
        /// <summary>
        /// 添加分隔符
        /// </summary>
        private void AppendSeperator(String content, LogType type)
        {
            Byte[] con = Encoding.Default.GetBytes(" | ");
            _fs.Write(con, 0, con.Length);
        }
        /// <summary>
        /// 添加換行符
        /// </summary>
        private void AppendNewLine(String content, LogType type)
        {
            Byte[] con = Encoding.Default.GetBytes("\r\n");
            _fs.Write(con, 0, con.Length);
        }
    }

    /// <summary>
    /// 修改所使用的時間類型
    /// </summary>
    public sealed partial class LogManager : IDisposable
    {
        /// <summary>
        /// 設置使用UTC時間
        /// </summary>
        public void UseUTCTime()
        {
            WriteLog = new Log(PrepareLogFile);
            WriteLog += OpenStream;
            WriteLog += AppendUTCTime;
            WriteLog += AppendSeperator;
            WriteLog += AppendComponentType;
            WriteLog += AppendSeperator;
            WriteLog += AppendType;
            WriteLog += AppendSeperator;
            WriteLog += AppendContent;
            WriteLog += AppendNewLine;
            WriteLog += CloseStream;
        }
        /// <summary>
        /// 設置使用本地時間
        /// </summary>
        public void UseLocalTime()
        {
            WriteLog = new Log(PrepareLogFile);
            WriteLog += OpenStream;
            WriteLog += AppendLocalTime;
            WriteLog += AppendSeperator;
            WriteLog += AppendComponentType;
            WriteLog += AppendSeperator;
            WriteLog += AppendType;
            WriteLog += AppendSeperator;
            WriteLog += AppendContent;
            WriteLog += AppendNewLine;
            WriteLog += CloseStream;
        }
    }
View Code

  日志管理類定義了一些列符合Log委托的方法,這些方法可以被添加到記錄日志的委托對象之中,以構成整個日志記錄的動作。在日后的擴展中,主要的工作也集中在添加新的符合Log委托定義的方法,並且將其添加到委托鏈上。

  ④ 在Main方法中調用LogManager的Log委托實例來寫日志,LogManager只需要管理這個委托,負責分派任務即可。

    class Program
    {
        static void Main(string[] args)
        {
            //使用日志
            using (LogManager logmanager =
                new LogManager(Type.GetType("LogSystem.Program"), "C:\\TestLog.txt"))
            {
                logmanager.WriteLog("新建了日志", LogType.Debug);
                logmanager.WriteLog("寫數據", LogType.Debug);
                logmanager.UseUTCTime();
                logmanager.WriteLog("現在是UTC時間", LogType.Debug);
                logmanager.UseLocalTime();
                logmanager.WriteLog("回到本地時間", LogType.Debug);
                logmanager.WriteLog("發生錯誤", LogType.Error);
                logmanager.WriteLog("准備退出", LogType.Info);
            }

            Console.ReadKey();
        }
    }
View Code

  代碼中初始化委托成員的過程既是任務分派的過程,可以注意到LogManager的UseUTCTime和UseLocalTime方法都是被委托成員進行了重新的分配,也可以理解為任務的再分配。

  下圖是上述代碼的執行結果,將日志信息寫入了C:\TestLog.txt中:

  

二、事件基礎

  事件這一名稱對於我們.NET碼農來說肯定不會陌生,各種技術框架例如WindowsForm、ASP.NET WebForm都會有事件這一名詞,並且所有的定義都基本相同。在.NET中,事件和委托在本質上並沒有太多的差異,實際環境下事件的運用卻比委托更加廣泛。

2.1 簡述事件的基本使用方法

  在Microsoft的產品文檔上這樣來定義的事件:事件是一種使對象或類能夠提供通知的成員。客戶端可以通過提供事件處理程序為相應的事件添加可執行代碼。設計和使用事件的全過程大概包括以下幾個步驟:

  下面我們來按照規范的步驟來展示一個通過控制台輸出事件的使用示例:

  ① 定義一個控制台事件ConsoleEvent的參數類型ConsoleEventArgs

    /// <summary>
    /// 自定義一個事件參數類型
    /// </summary>
    public class ConsoleEventArgs : EventArgs
    {
        // 控制台輸出的消息
        private string message;

        public string Message
        {
            get
            {
                return message;
            }
        }

        public ConsoleEventArgs()
            : base()
        {
            this.message = string.Empty;
        }

        public ConsoleEventArgs(string message)
            : base()
        {
            this.message = message;
        }
    }
View Code

  ② 定義一個控制台事件的管理者,在其中定義了事件類型的私有成員ConsoleEvent,並定義了事件的發送方法SendConsoleEvent

    /// <summary>
    /// 管理控制台,在輸出前發送輸出事件
    /// </summary>
    public class ConsoleManager
    {
        // 定義控制台事件成員對象
        public event EventHandler<ConsoleEventArgs> ConsoleEvent;

        /// <summary>
        /// 控制台輸出
        /// </summary>
        public void ConsoleOutput(string message)
        {
            // 發送事件
            ConsoleEventArgs args = new ConsoleEventArgs(message);
            SendConsoleEvent(args);
            // 輸出消息
            Console.WriteLine(message);
        }

        /// <summary>
        /// 負責發送事件
        /// </summary>
        /// <param name="args">事件的參數</param>
        protected virtual void SendConsoleEvent(ConsoleEventArgs args)
        {
            // 定義一個臨時的引用變量,確保多線程訪問時不會發生問題
            EventHandler<ConsoleEventArgs> temp = ConsoleEvent;
            if (temp != null)
            {
                temp(this, args);
            }
        }
    }
View Code

  ③ 定義了事件的訂閱者Log,在其中通過控制台時間的管理類公開的事件成員訂閱其輸出事件ConsoleEvent

    /// <summary>
    /// 日志類型,負責訂閱控制台輸出事件
    /// </summary>
    public class Log
    {
        // 日志文件
        private const string logFile = @"C:\TestLog.txt";

        public Log(ConsoleManager cm)
        {
            // 訂閱控制台輸出事件
            cm.ConsoleEvent += this.WriteLog;
        }

        /// <summary>
        /// 事件處理方法,注意參數固定模式
        /// </summary>
        /// <param name="sender">事件的發送者</param>
        /// <param name="args">事件的參數</param>
        private void WriteLog(object sender, EventArgs args)
        {
            // 文件不存在的話則創建新文件
            if (!File.Exists(logFile))
            {
                using (FileStream fs = File.Create(logFile)) { }
            }

            FileInfo fi = new FileInfo(logFile);

            using (StreamWriter sw = fi.AppendText())
            {
                ConsoleEventArgs cea = args as ConsoleEventArgs;
                sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "|" + sender.ToString() + "|" + cea.Message);
            }
        }
    }
View Code

  ④ 在Main方法中進行測試:

    class Program
    {
        static void Main(string[] args)
        {
            // 控制台事件管理者
            ConsoleManager cm = new ConsoleManager();
            // 控制台事件訂閱者
            Log log = new Log(cm);

            cm.ConsoleOutput("測試控制台輸出事件");
            cm.ConsoleOutput("測試控制台輸出事件");
            cm.ConsoleOutput("測試控制台輸出事件");

            Console.ReadKey();
        }
    }
View Code

  

  當該程序執行時,ConsoleManager負責在控制台輸出測試的字符串消息,與此同時,訂閱了控制台輸出事件的Log類對象會在指定的日志文件中寫入這些字符串消息。可以看出,這是一個典型的觀察者模式的應用,也可以說事件為觀察者模式提供了便利的實現基礎。

2.2 事件和委托有神馬聯系?

  事件的定義和使用方式與委托極其類似,那么二者又是何關系呢?經常聽人說,委托本質是一個類型,而事件本質是一個特殊的委托類型的實例。關於這個解釋,最好的辦法莫過於通過查看原代碼和編譯后的IL代碼進行分析。

  ① 回顧剛剛的代碼,在ConsoleManager類中定義了一個事件成員

public event EventHandler<ConsoleEventArgs> ConsoleEvent;

  EventHandler是.NET框架中提供的一種標准的事件模式,它是一個特殊的泛型委托類型,通過查看元數據可以驗證這一點:

    [Serializable]
    public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

  正如上面代碼所示,我們定義一個事件時,實際上是定義了一個特定的委托成員實例。該委托沒有返回值,並且有兩個參數:一個事件源和一個事件參數。而當事件的使用者訂閱該事件時,其本質就是將事件的處理方法加入到委托鏈之中

  ② 下面通過Reflector來查看一下事件ConsoleEvent的IL代碼(中間代碼),可以更方便地看到這一點:

  首先,查看EventHandler的IL代碼,可以看到在C#編譯器編譯delegate代碼時,編譯后是成為了一個class。

  其次,當C#編譯器編譯event代碼時,會首先為類型添加一個EventHandler<T>的委托實例對象,然后為其增加一對add/remove方法用來實現從委托鏈中添加和移除方法的功能。

  通過查看add_ConsoleEvent的IL代碼,可以清楚地看到訂閱事件的本質是調用Delegate的Combine方法將事件處理方法綁定到委托鏈中

    L_0000: ldarg.0 
    L_0001: ldfld class [mscorlib]System.EventHandler`1<class ConsoleEventDemo.ConsoleEventArgs> ConsoleEventDemo.ConsoleManager::ConsoleEvent
    L_0006: stloc.0 
    L_0007: ldloc.0 
    L_0008: stloc.1 
    L_0009: ldloc.1 
    L_000a: ldarg.1 
    L_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
    L_0010: castclass [mscorlib]System.EventHandler`1<class ConsoleEventDemo.ConsoleEventArgs>
    L_0015: stloc.2 
    L_0016: ldarg.0 
    L_0017: ldflda class [mscorlib]System.EventHandler`1<class ConsoleEventDemo.ConsoleEventArgs> ConsoleEventDemo.ConsoleManager::ConsoleEvent

Summary:事件是一個特殊的委托實例,提供了兩個供訂閱事件和取消訂閱的方法:add_event和remove_event,其本質都是基於委托鏈來實現。

2.3 如何設計一個帶有很多事件的類型?

  多事件的類型在實際應用中並不少見,尤其是在一些用戶界面的類型中(例如在WindowsForm中的各種控件)。這些類型動輒將包含數十個事件,如果為每一個事件都添加一個事件成員,將導致無論使用者是否用到所有事件,每個類型對象都將占有很大的內存,那么對於系統的性能影響將不言而喻。事實上,.NET的開發小組運用了一種比較巧妙的方式來避免這一困境。

Solution:當某個類型具有相對較多的事件時,我們可以考慮顯示地設計訂閱、取消訂閱事件的方法,並且把所有的委托鏈表存儲在一個集合之中。這樣做就能避免在類型中定義大量的委托成員而導致類型過大。

  下面通過一個具體的實例來說明這一設計:

  ① 定義包含大量事件的類型之一:使用EventHandlerList成員來存儲所有事件

    public partial class MultiEventClass
    {
        // EventHandlerList包含了一個委托鏈表的容器,實現了多事件存放在一個容器之中的包裝,它使用的是鏈表數據結構
        private EventHandlerList events;

        public MultiEventClass()
        {
            // 初始化EventHandlerList
            events = new EventHandlerList();
        }

        // 釋放EventHandlerList
        public void Dispose()
        {
            events.Dispose();
        }
    }
View Code

  ② 定義包含大量事件的類型之二:申明多個具體的事件

    public partial class MultiEventClass
    {
        #region event1
        // 事件1的委托原型
        public delegate void Event1Handler(object sender, EventArgs e);
        // 事件1的靜態Key
        protected static readonly object Event1Key = new object();
        // 訂閱事件和取消訂閱
        // 注意:EventHandlerList並不提供線程同步,所以加上線程同步屬性
        public event Event1Handler Event1
        {
            [MethodImpl(MethodImplOptions.Synchronized)]
            add
            {
                events.AddHandler(Event1Key, value);
            }
            [MethodImpl(MethodImplOptions.Synchronized)]
            remove
            {
                events.RemoveHandler(Event1Key, value);
            }
        }
        // 觸發事件1
        protected virtual void OnEvent1(EventArgs e)
        {
            events[Event1Key].DynamicInvoke(this, e);
        }
        // 簡單地觸發事件1,以便於測試
        public void RiseEvent1()
        {
            OnEvent1(EventArgs.Empty);
        }
        #endregion

        #region event2
        // 事件2的委托原型
        public delegate void Event2Handler(object sender, EventArgs e);
        // 事件2的靜態Key
        protected static readonly object Event2Key = new object();
        // 訂閱事件和取消訂閱
        // 注意:EventHandlerList並不提供線程同步,所以加上線程同步屬性
        public event Event2Handler Event2
        {
            [MethodImpl(MethodImplOptions.Synchronized)]
            add
            {
                events.AddHandler(Event2Key, value);
            }
            [MethodImpl(MethodImplOptions.Synchronized)]
            remove
            {
                events.RemoveHandler(Event2Key, value);
            }
        }
        // 觸發事件2
        protected virtual void OnEvent2(EventArgs e)
        {
            events[Event2Key].DynamicInvoke(this, e);
        }
        // 簡單地觸發事件2,以便於測試
        public void RiseEvent2()
        {
            OnEvent2(EventArgs.Empty);
        }
        #endregion
    }
View Code

  ③ 定義事件的訂閱者(它對多事件類型內部的構造一無所知)

    public class Customer
    {
        public Customer(MultiEventClass events)
        {
            // 訂閱事件1
            events.Event1 += Event1Handler;
            // 訂閱事件2
            events.Event2 += Event2Handler;
        }

        // 事件1的回調方法
        private void Event1Handler(object sender, EventArgs e)
        {
            Console.WriteLine("事件1被觸發");
        }

        // 事件2的回調方法
        private void Event2Handler(object sender, EventArgs e)
        {
            Console.WriteLine("事件2被觸發");
        }
    }
View Code

  ④ 編寫入口方法來測試多事件的觸發

    class Program
    {
        static void Main(string[] args)
        {
            using(MultiEventClass mec = new MultiEventClass())
            {
                Customer customer = new Customer(mec);
                mec.RiseEvent1();
                mec.RiseEvent2();
            }

            Console.ReadKey();
        }
    }
View Code

  最終運行結果如下圖所示:

  

  總結EventHandlerList的用法,在多事件類型中為每一個事件都定義了一套成員,包括事件的委托原型、事件的訂閱和取消訂閱方法,在實際應用中,可能需要定義事件專用的參數類型。這樣的設計主旨在於改動包含多事件的類型,而訂閱事件的客戶並不會察覺這樣的改動。設計本身不在於減少代碼量,而在於有效減少多事件類型對象的大小。

2.4 如何使用事件模擬場景:貓叫->老鼠逃跑 & 主人驚醒

  這是一個典型的觀察者模式的應用場景,事件的發源在於貓叫這個動作,在貓叫之后,老鼠開始逃跑,而主人則會從睡夢中驚醒。可以發現,主人和老鼠這兩個類型的動作相互之間沒有聯系,但都是由貓叫這一事件觸發的。

  設計的大致思路在於,貓類包含並維護一個貓叫的動作,主人和老鼠的對象實例需要訂閱貓叫這一事件,保證貓叫這一事件發生時主人和老鼠可以執行相應的動作。

  (1)設計貓類,為其定義一個貓叫的事件CatCryEvent:

    public class Cat
    {
        private string name;
        // 貓叫的事件
        public event EventHandler<CatCryEventArgs> CatCryEvent;

        public Cat(string name)
        {
            this.name = name;
        }

        // 觸發貓叫事件
        public void CatCry()
        {
            // 初始化事件參數
            CatCryEventArgs args = new CatCryEventArgs(name);
            Console.WriteLine(args);
            // 開始觸發事件
            CatCryEvent(this, args);
        }
    }

    public class CatCryEventArgs : EventArgs
    {
        private string catName;

        public CatCryEventArgs(string catName)
            : base()
        {
            this.catName = catName;
        }

        public override string ToString()
        {
            string message = string.Format("{0}叫了", catName);
            return message;
        }
    }
View Code

  (2)設計老鼠類,在其構造方法中訂閱貓叫事件,並提供對應的處理方法

    public class Mouse
    {
        private string name;
        // 在構造方法中訂閱事件
        public Mouse(string name, Cat cat)
        {
            this.name = name;
            cat.CatCryEvent += CatCryEventHandler;
        }

        // 貓叫的處理方法
        private void CatCryEventHandler(object sender, CatCryEventArgs e)
        {
            Run();
        }

        // 逃跑方法
        private void Run()
        {
            Console.WriteLine("{0}逃走了:我勒個去,趕緊跑啊!", name);
        }
    }
View Code

  (3)設計主人類,在其構造犯法中訂閱貓叫事件,並提供對應的處理方法

    public class Master
    {
        private string name;

        // 在構造方法中訂閱事件
        public Master(string name, Cat cat)
        {
            this.name = name;
            cat.CatCryEvent += CatCryEventHandler;
        }

        // 針對貓叫的處理方法
        private void CatCryEventHandler(object sender, CatCryEventArgs e)
        {
            WakeUp();
        }

        // 具體的處理方法——驚醒
        private void WakeUp()
        {
            Console.WriteLine("{0}醒了:我勒個去,叫個錘子!", name);
        }
    }
View Code

  (4)最后在Main方法中進行場景的模擬:

    class Program
    {
        static void Main(string[] args)
        {
            Cat cat = new Cat("假老練");
            Mouse mouse1 = new Mouse("風車車", cat);
            Mouse mouse2 = new Mouse("米奇妙", cat);
            Master master = new Master("李扯火", cat);
            // 毛開始叫了,老鼠和主人有不同的反應
            cat.CatCry();

            Console.ReadKey();
        }
    }
View Code

  這里定義了一只貓,兩只老鼠與一個主人,當貓的CatCry方法被執行到時,會觸發貓叫事件CatCryEvent,此時就會通知所有這一事件的訂閱者。本場景的關鍵之處就在於主人和老鼠的動作應該完全由貓叫來觸發。下面是場景模擬代碼的運行結果:

  

三、反射基礎

3.1 反射的基本原理是什么?其實現的基石又是什么?

  反射是一種動態分析程序集、模塊、類型及字段等目標對象的機制,它的實現依托於元數據。元數據,就是描述數據的數據。在CLR中,元數據就是對一個模塊定義或引用的所有東西的描述系統。

3.2 .NET中提供了哪些類型實現反射?

  在.NET中,為我們提供了豐富的可以用來實現反射的類型,這些類型大多數都定義在System.Reflection命名空間之下,例如Assembly、Module等。利用這些類型,我們就可以方便地動態加載程序集、模塊、類型、方法和字段等元素。

  下面我們來看一個使用示例,首先是創建一個程序集SimpleAssembly,其中有一個類為SimpleClass:

    [Serializable]
    class SimpleClass
    {
        private String _MyString;
        public SimpleClass(String mystring)
        {
            _MyString = mystring;
        }

        public override string ToString()
        {
            return _MyString;
        }

        static void Main(string[] args)
        {
            Console.WriteLine("簡單程序集");
            Console.Read();
        }
    }
View Code

  其次是對程序集中的模塊進行分析,分別利用反射對程序集、模塊和類進行分析:

    public class AnalyseHelper
    {
        /// <summary>
        /// 分析程序集
        /// </summary>
        public static void AnalyzeAssembly(Assembly assembly)
        {
            Console.WriteLine("程序集名字:" + assembly.FullName);
            Console.WriteLine("程序集位置:" + assembly.Location);
            Console.WriteLine("程序集是否在GAC中:" +
                        assembly.GlobalAssemblyCache.ToString());
            Console.WriteLine("包含程序集的模塊名" +
                assembly.ManifestModule.Name);
            Console.WriteLine("運行程序集需要的CLR版本:" +
                assembly.ImageRuntimeVersion);
            Console.WriteLine("現在開始分析程序集中的模塊");
            Module[] modules = assembly.GetModules();
            foreach (Module module in modules)
            {
                AnalyzeModule(module);
            }
        }

        /// <summary>
        /// 分析模塊
        /// </summary>
        public static void AnalyzeModule(Module module)
        {
            Console.WriteLine("模塊名:" + module.Name);
            Console.WriteLine("模塊的UUID:" + module.ModuleVersionId);
            Console.WriteLine("開始分析模塊下的類型");
            Type[] types = module.GetTypes();
            foreach (Type type in types)
            {
                AnalyzeType(type);
            }
        }

        /// <summary>
        /// 分析類型
        /// </summary>
        public static void AnalyzeType(Type type)
        {
            Console.WriteLine("類型名字:" + type.Name);
            Console.WriteLine("類型的類別是:" + type.Attributes);
            if (type.BaseType != null)
                Console.WriteLine("類型的基類是:" + type.BaseType.Name);
            Console.WriteLine("類型的GUID是:" + type.GUID);
            //設置感興趣的類型成員
            BindingFlags flags = (BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
            //分析成員
            FieldInfo[] fields = type.GetFields(flags);
            if (fields.Length > 0)
            {
                //Console.WriteLine("開始分析類型的成員");
                foreach (FieldInfo field in fields)
                {
                    // 分析成員
                }
            }
            //分析包含的方法
            MethodInfo[] methods = type.GetMethods(flags);
            if (methods.Length > 0)
            {
                //Console.WriteLine("開始分析類型的方法");
                foreach (MethodInfo method in methods)
                {
                    // 分析方法
                }
            }
            //分析屬性
            PropertyInfo[] properties = type.GetProperties(flags);
            if (properties.Length > 0)
            {
                //Console.WriteLine("開始分析類型的屬性");
                foreach (PropertyInfo property in properties)
                {
                    // 分析屬性
                }
            }
        }
    }
View Code

  最后編寫入口方法來嘗試分析一個具體的程序集:

    [PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")]
    class Program
    {
        static void Main(string[] args)
        {
            Assembly assembly = Assembly.LoadFrom(@"..\..\..\SimpleAssembly\bin\Debug\SimpleAssembly.exe");
            AnalyseHelper.AnalyzeAssembly(assembly);

            // 創建一個程序集中的類型的對象
            Console.WriteLine("利用反射創建對象");
            string[] paras = { "測試一下反射效果" };
            object obj = assembly.CreateInstance(assembly.GetModules()[0].GetTypes()[0].ToString(), true, BindingFlags.CreateInstance, null, paras, null, null);
            Console.WriteLine(obj);

            Console.ReadKey();
        }
    }
View Code

  上面的代碼按照 程序集->模塊->類型 三個層次的順序來動態分析一個程序集,當然還可以繼續遞歸類型內部的成員,最后通過CreateInstance方法來動態創建了一個類型,這些都是反射經常被用來完成的功能,執行結果如下圖所示:

3.3 如何使用反射實現工廠模式?

  工廠模式是一種比較常用的設計模式,其基本思想在於使用不同的工廠類型來打造不同產品的部件。例如,我們在打造一間屋子時,可能需要窗戶、屋頂、門、房梁、柱子等零部件。有的屋子需要很多根柱子,而有的屋子又不需要窗戶。在這樣的需求下,就可以使用工廠模式。

  (1)工廠模式的傳統實現和其弊端

  下圖展示了針對屋子設計的傳統工廠模式架構圖:

  上圖的設計思路是:

  ①使用者告訴工廠管理者需要哪個產品部件;

  ②工廠管理者分析使用者傳入的信息,生成合適的實現工廠接口的類型對象;

  ③通過工廠生產出相應的產品,返回給使用者一個實現了該產品接口的類型對象;

  通過上述思路,實現代碼如下:

  ①首先是定義工廠接口,產品接口與產品類型的枚舉

    /// <summary>
    /// 屋子產品的零件
    /// </summary>
    public enum RoomParts
    {
        Roof,
        Window,
        Pillar
    }

    /// <summary>
    /// 工廠接口
    /// </summary>
    public interface IFactory
    {
        IProduct Produce();
    }

    /// <summary>
    /// 產品接口
    /// </summary>
    public interface IProduct
    {
        string GetName();
    }
View Code

  ②其次是具體實現產品接口的產品類:窗戶、屋頂和柱子

    /// <summary>
    /// 屋頂
    /// </summary>
    public class Roof : IProduct
    {
        // 實現接口,返回產品名字
        public string GetName()
        {
            return "屋頂";
        }
    }

    /// <summary>
    /// 窗戶
    /// </summary>
    public class Window : IProduct
    {
        // 實現接口,返回產品名字
        public string GetName()
        {
            return "窗戶";
        }
    }

    /// <summary>
    /// 柱子
    /// </summary>
    public class Pillar : IProduct
    {
        // 實現接口,返回產品名字
        public string GetName()
        {
            return "柱子";
        }
    }
View Code

  ③然后是具體實現工廠接口的工廠類:實現接口返回一個具體的產品對象

    /// <summary>
    /// 屋頂工廠
    /// </summary>
    public class RoofFactory : IFactory
    {
        // 實現接口,返回一個產品對象
        public IProduct Produce()
        {
            return new Roof();
        }
    }

    /// <summary>
    /// 窗戶工廠
    /// </summary>
    public class WindowFactory : IFactory
    {
        // 實現接口,返回一個產品對象
        public IProduct Produce()
        {
            return new Window();
        }
    }

    /// <summary>
    /// 柱子工廠
    /// </summary>
    public class PillarFactory : IFactory
    {
        // 實現接口,返回一個產品對象
        public IProduct Produce()
        {
            return new Pillar();
        }
    }
View Code  

  ④最后是工廠管理類:組織起眾多的產品與工廠

    /// <summary>
    /// 工廠管理者
    /// </summary>
    public class FactoryManager
    {
        public static IProduct GetProduct(RoomParts part)
        {
            IFactory factory = null;
            // 傳統工廠模式的弊端:工廠管理類和工廠類族的緊耦合
            switch (part)
            {
                case RoomParts.Roof:
                    factory = new RoofFactory();
                    break;
                case RoomParts.Window:
                    factory = new WindowFactory();
                    break;
                case RoomParts.Pillar:
                    factory = new PillarFactory();
                    break;
                default:
                    return null;
            }

            // 利用工廠生產產品
            IProduct product = factory.Produce();
            Console.WriteLine("生產了一個產品:{0}", product.GetName());

            return product;
        }
    }
View Code

  按照國際慣例,我們實現一個入口方法來測試一下:

    class Customer
    {
        static void Main(string[] args)
        {
            // 根據需要獲得不同的產品零件
            IProduct window = FactoryManager.GetProduct(RoomParts.Window);
            Console.WriteLine("我獲取到了{0}",window.GetName());
            IProduct roof = FactoryManager.GetProduct(RoomParts.Roof);
            Console.WriteLine("我獲取到了{0}", roof.GetName());
            IProduct pillar = FactoryManager.GetProduct(RoomParts.Pillar);
            Console.WriteLine("我獲取到了{0}", pillar.GetName());

            Console.ReadKey();
        }
    }
View Code

  在Customer類中,我們通過工廠管理類根據需要的不同零件類型獲取到了不同的產品零件,其運行結果如下圖所示:

  當一個新的產品—地板需要被添加時,我們需要改的地方是:添加零件枚舉記錄、添加針對地板的工廠類、添加新地板產品類,修改工廠管理類(在switch中添加一條case語句),這樣設計的優點在於無論添加何種零件,產品使用者都不需要關心內部的變動,可以一如既往地使用工廠管理類來得到希望的零件,而缺點也有以下幾點:

  ①工廠管理類和工廠類族耦合;

  ②每次添加新的零件都需要添加一對工廠類和產品類,類型會越來越多;

  (2)基於反射的工廠模式的實現

  利用反射機制可以實現更加靈活的工廠模式,這一點體現在利用反射可以動態地獲知一個產品由哪些零部件組成,而不再需要用一個switch語句來逐一地尋找合適的工廠。

  ①產品、枚舉和以上一致,這里的改變主要在於添加了兩個自定義的特性,這兩個特性會被分別附加在產品類型和產品接口上:

    /// <summary>
    /// 該特性用於附加在產品類型之上
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    public class ProductAttribute : Attribute
    {
        // 標注零件的成員
        private RoomParts myRoomPart;

        public ProductAttribute(RoomParts part)
        {
            myRoomPart = part;
        }

        public RoomParts RoomPart
        {
            get
            {
                return myRoomPart;
            }
        }
    }

    /// <summary>
    /// 該特性用於附加在產品接口類型之上
    /// </summary>
    [AttributeUsage(AttributeTargets.Interface)]
    public class ProductListAttribute : Attribute
    {
        // 產品類型集合
        private Type[] myList;

        public ProductListAttribute(Type[] products)
        {
            myList = products;
        }

        public Type[] ProductList
        {
            get
            {
                return myList;
            }
        }
    }
View Code

  ②下面是產品接口和產品類族的定義,其中產品接口使用了ProductListAttribute特性,而每個產品都使用了ProductAttribute特性:

    /// <summary>
    /// 產品接口
    /// </summary>
    [ProductList(new Type[] { typeof(Roof), typeof(Window), typeof(Pillar) })]
    public interface IProduct
    {
        string GetName();
    }

    /// <summary>
    /// 屋頂
    /// </summary>
    [Product(RoomParts.Roof)]
    public class Roof : IProduct
    {
        // 實現接口,返回產品名字
        public string GetName()
        {
            return "小天鵝屋頂";
        }
    }

    /// <summary>
    /// 窗戶
    /// </summary>
    [Product(RoomParts.Window)]
    public class Window : IProduct
    {
        // 實現接口,返回產品名字
        public string GetName()
        {
            return "雙匯窗戶";
        }
    }

    /// <summary>
    /// 柱子
    /// </summary>
    [Product(RoomParts.Pillar)]
    public class Pillar : IProduct
    {
        // 實現接口,返回產品名字
        public string GetName()
        {
            return "小米柱子";
        }
    }
View Code

  ③下面是修改后的工廠類,由於使用了反射特性,這里一個工廠類型就可以生產所有的產品:

    /// <summary>
    /// 工廠類
    /// </summary>
    public class Factory
    {
        public IProduct Produce(RoomParts part)
        {
            // 通過反射從IProduct接口中獲得屬性從而獲得所有產品列表
            ProductListAttribute attr = (ProductListAttribute)Attribute.GetCustomAttribute(typeof(IProduct), typeof(ProductListAttribute));
            // 遍歷所有的實現產品零件類型
            foreach (var type in attr.ProductList)
            {
                // 利用反射查找其屬性
                ProductAttribute pa = (ProductAttribute)Attribute.GetCustomAttribute(type, typeof(ProductAttribute));
                // 確定是否是需要到的零件
                if(pa.RoomPart == part)
                {
                    // 利用反射動態創建產品零件類型實例
                    object product = Assembly.GetExecutingAssembly().CreateInstance(type.FullName);
                    return product as IProduct;
                }
            }

            return null;
        }
    }
View Code

  ④最后時修改后的工廠管理類,核心只有三行代碼:

    /// <summary>
    /// 工廠管理者
    /// </summary>
    public class FactoryManager
    {
        public static IProduct GetProduct(RoomParts part)
        {
            // 一共只有一個工廠
            Factory factory = new Factory();
            IProduct product = factory.Produce(part);
            Console.WriteLine("生產了一個產品:{0}", product.GetName());
            return product;
        }
    }
View Code

  上述代碼中最主要的變化在於兩點:其一是工廠管理類不再需要根據不同的零件尋找不同的工廠,因為只有一個工廠負責處理所有的產品零件;其二是產品類型和產品接口應用了兩個自定義特性,來方便工廠進行反射。ProductAttribute附加在產品類上,標注了當前類型代表了哪個產品零件。而ProductListAttribute則附加在產品接口之上,方便反射得知一共有多少產品零件。

  這時需要添加一個新的地板產品零件類型時,我們需要做的是:添加零件枚舉記錄,添加代表地板的類型,修改添加在IProduct上的屬性初始化參數(增加地板類型),可以看到這時調用者、工廠管理類和工廠都不再需要對新添加的零件進行改動,程序只需要添加必要的類型和枚舉記錄即可。當然,這樣的設計也存在一定缺陷:反射的運行效率相對較低,在產品零件相對較多時,每生產一個產品就需要反射遍歷這是一件相當耗時的工作。

四、特性基礎

  特性機制可以幫助程序員以申明的方式進行編程,而不再需要考慮實現的細節。

4.1 神馬是特性?如何自定義一個特性?

  (1)特性是什么?

  特性是一種有別於普通命令式編程的編程方式,通常被稱為申明式編程方式。所謂申明式編程方式就是指程序員只需要申明某個模塊會有怎樣的特性,而無需關心如何去實現。下面的代碼就是特性在ASP.NET MVC中的基本使用方式:

        [HttpPost]
        public ActionResult Add(UserInfo userInfo)
        {
            if (ModelState.IsValid)
            {
                 // To do fun
            }

            return RedirectToAction("Index");
        }

  當一個特性被添加到某個元素上時,該元素就被認為具有了這個特性所代表的功能或性質,例如上述代碼中Add方法在添加了HttpPost特性之后,就被認為只有遇到以POST的方式請求該方法時才會被執行。

Note:特性在被編譯器編譯時,和傳統的命令式代碼不同,它會被以二進制數據的方式寫入模塊文件的元數據之中,而在運行時再被解讀使用。特性也是經常被反射機制應用的元素,因為它本身是以元數據的形式存放的。

  (2)如何自定義特性

  除了直接使用.NET中內建的所有特性之外,我們也可以建立自己的特性來實現業務邏輯。在上面反射工廠的實現中就使用到了自定義特性。具體來說,定義一個特性的本質就是定義一個繼承自System.Attribute類的類型,這樣的類型就被編譯器認為是一個特性類型。

  下面我們看看如何自頂一個特性並使用該特性:

  ①定義一個繼承自System.Attribute的類型MyCustomAttribute

    /// <summary>
    /// 一個自定義特性MyCustomAttribute
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    public class MyCustomAttribute : Attribute
    {
        private string className;

        public MyCustomAttribute(string className)
        {
            this.className = className;
        }

        // 一個只讀屬性ClassName
        public string ClassName
        {
            get
            {
                return className;
            }
        }
    }
View Code

  一個繼承自System.Attribute的類型,就是一個自定義特性,並且可以將其添加到適合的元素之上。特性將會被寫入到元數據之中,所以特性的使用基本都是基於反射機制。

  ②在入口方法中使用MyCustomAttribute

    [MyCustom("UseMyCustomAttribute")]
    class UseMyCustomAttribute
    {
        static void Main(string[] args)
        {
            Type t = typeof(UseMyCustomAttribute);
            // 通過GetCustomAttributes方法得到自定義特性
            object[] attrs = t.GetCustomAttributes(false);
            MyCustomAttribute att = attrs[0] as MyCustomAttribute;

            Console.WriteLine(att.ClassName);
            Console.ReadKey();
        }
    }
View Code

  為入口方法所在的類型UseMyCustomAttribute類添加了一個自定義特性,就可以在該類的方法中通過調用該類型的GetCustomAttributes方法獲取所有添加到該類型的自定義特性數組,也就可以方便使用該自定義特性所具備的性質和能力(例如代碼中的屬性成員可以方便獲取)。

關於自定義特性,有幾點需要注意:

  • 雖然沒有強制規定,但按照約定最好特性類型的名字都以Attribute結尾;
  • 在C#中為了方便起見,使用特性時都可以省略特性名字后的Attribute,例如上述代碼中的[MyCustom("UseMyCustomAttribute")]代替了[MyCustomAttribute("UseMyCustomAttribute")];
  • 特性類型自身也可以添加其他的特性;

4.2 .NET中特性可以在哪些元素上使用?

  特性可以被用來使用到某個元素之上,這個元素可以是字段,也可以是類型。對於類、結構等元素,特性的使用可以添加在其定義的上方,而對於程序集、模塊等元素的特性來說,則需要顯式地告訴編譯器這些特性的作用目標。例如,在C#中,通過目標關鍵字加冒號來告訴編譯器的使用目標:

// 應用在程序集
[assembly:MyCustomAttribute]
// 應用在模塊
[module: MyCustomAttribute]
// 應用在類型
[type: MyCustomAttribute]

  我們在設計自定義特性時,往往都具有明確的針對性,例如該特性只針對類型、接口或者程序集,限制特性的使用目標可以有效地傳遞設計者的意圖,並且可以避免不必要的錯誤使用特性而導致的元數據膨脹。AttributeUsage特性就是用來限制特性使用目標元素的,它接受一個AttributeTargets的枚舉對象作為輸入來告訴AttributeUsage西望望對特性做何種限定。例如上面展示的一個自定義特性,使用了限制范圍:

    [AttributeUsage(AttributeTargets.Class)]
    public class MyCustomAttribute : Attribute
    {
        .....
    }

Note:一般情況下,自定義特性都會被限制適用范圍,我們也應該養成這樣的習慣,為自己設計的特性加上AttributeUsage特性,很少會出現使用在所有元素上的特性。即便是可以使用在所有元素上,也應該顯式地申明[AttributeUsage(AttributesTargets.All)]來提高代碼的可讀性。

4.3 如何獲知一個元素是否申明了某個特性?

  在.NET中提供了很多的方法來查詢一個元素是否申明了某個特性,每個方法都有不同的使用場合,但是萬變不離其宗,都是基於反射機制來實現的。

  首先,還是以上面的MyCustomAttribute特性為例,新建一個入口方法類Program:

    /// <summary>
    /// 一個自定義特性MyCustomAttribute
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    public class MyCustomAttribute : Attribute
    {
        private string className;

        public MyCustomAttribute(string className)
        {
            this.className = className;
        }

        // 一個只讀屬性ClassName
        public string ClassName
        {
            get
            {
                return className;
            }
        }
    }

    [MyCustom("Program")]
    class Program
    {
        static void Main(string[] args)
        {
            Type attributeType = typeof(MyCustomAttribute);
            Type thisClass = typeof(Program);
        
        }
    }
View Code

  (1)System.Attribute.IsDefined方法

    // 使用IsDefined方法
    bool isDefined = Attribute.IsDefined(thisClass, attributeType);
    Console.WriteLine("Program類是否申明了MyCustomAttribute特性:{0}", isDefined);

  (2)System.Attribute.GetCustomerAttribute方法

    // 使用Attribute.GetCustomAttribute方法
    Attribute att = Attribute.GetCustomAttribute(thisClass, attributeType);
    if (att != null)
    {
       Console.WriteLine("Program類申明了MyCustomAttribute特性,特性的成員為:{0}", (att as MyCustomAttribute).ClassName);
    }

  (3)System.Attribute.GetCustomerAttributes方法

    // 使用Attribute.GetCustomAttributes方法
    Attribute[] atts = Attribute.GetCustomAttributes(thisClass, attributeType);
    if (atts.Length > 0)
    {
         Console.WriteLine("Program類申明了MyCustomAttribute特性,特性名稱為:{0}", ((MyCustomAttribute)atts[0]).ClassName);
    }

  (4)System.Reflection.CustomAttributeData類型

    // 使用CustomAttributeData.GetCustomAttributes方法
    IList<CustomAttributeData> attList = CustomAttributeData.GetCustomAttributes(thisClass);
    if (attList.Count > 0)
    {
        Console.WriteLine("Program類申明了MyCustomAttribute特性");
        // 注意:這里可以對特性進行分析,但無法得到其實例
        CustomAttributeData attData = attList[0];
        Console.WriteLine("該特性的名字是:{0}", attData.Constructor.DeclaringType.Name);
        Console.WriteLine("該特性的構造方法有{0}個參數", attData.ConstructorArguments.Count);
    }

  下圖是四種方式的執行結果:

  這四種方法各有其特點,但都可以實現查詢某個元素是否申明了某個特性的這一功能。其中,可以看到第(4)種方式,可以對特性進行分析,但無法得到其實例。另外,自定義特性被申明為sealed表示不可繼承,這是因為在特性被檢查時,無法分別制定特性和其派生特性,這一點需要我們注意

4.4 一個元素是否可以重復申明同一個特性?

  對於有些業務邏輯來說,一個特性反復地申明在同一個元素上市沒有必要的,但同時對於另一些邏輯來說,又非常有必要對同一元素多次申明同一特性。很幸運,.NET的特性機制完美支持了這一類業務需求。

  當一個特性申明了AttributeUsage特性並且顯式地將AllowMultiple屬性設置為true時,該特性就可以在同一元素上多次申明,否則的話編譯器將報錯。

  例如下面一段代碼,類型Program多次申明了MyCustomAttribute特性:

    [MyCustom("Class1")]
    [MyCustom("Class2")]
    [MyCustom("Class3")]
    public class Program
    {
        public static void Main(string[] args)
        {
        }
    }

    /// <summary>
    /// 一個自定義特性MyCustomAttribute
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    public class MyCustomAttribute : Attribute
    {
        private string className;

        public MyCustomAttribute(string className)
        {
            this.className = className;
        }

        // 一個只讀屬性ClassName
        public string ClassName
        {
            get
            {
                return className;
            }
        }
    }

  通常情況下,重復申明同一特性往往會傳入不同的參數。此外,如果不顯式地設置AllowMultiple屬性時多次申明同一特性會如何呢?在這種情況下,編譯器將會認為自定義特性不能多次申明在同一元素上,會出現以下的編譯錯誤:

參考資料

(1)朱毅,《進入IT企業必讀的200個.NET面試題》

(2)張子陽,《.NET之美:.NET關鍵技術深入解析》

(3)王濤,《你必須知道的.NET》

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM