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

 Index :html

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

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

 (3)字符串、集合與流數據庫

 (4)委託、事件、反射與特性編程

 (5)多線程開發基礎設計模式

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

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

1、委託基礎

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中:

  

2、事件基礎

  事件這一名稱對於咱們.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、反射基礎

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、特性基礎

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

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》

 

相關文章
相關標籤/搜索