C#中的委托和事件(0) delegate


前言

來說一說委托(delegate)和事件(event),本篇采取的形式是翻譯微軟Delegate的docs中的重要部分(不要問我為什么微軟的docs有中文還要讀英文,因為讀中文感覺自己有閱讀障礙- -)+ 自己理解總結,適合不會或沒有使用過delegate的小白。

為什么要把委托和事件放在一起,因為委托Delegate是事件Event的基礎,並且他們容易被混淆。

原docs中對委托進行了一個定位:委托在.Net中提供后期綁定(Late Binding)機制。

System.Delegate和delegate關鍵字

定義委托類型

我們從delegate關鍵字開始,因為它是你使用委托的主要方式。當你使用關鍵字delegate時,編譯器生成的代碼將映射到一些方法,這些方法調用了DelegateMulticastDelegate類的成員。

定義委托的語法跟定義方法簽名比較類似,你只需要在返回類型和訪問權限之間加上關鍵字delegate

繼續使用List.Sort()方法(docs前面一直使用的例子)作為我們的例子,第一步是為Comparison委托創建一個類型:

public delegate int Comparison<in T>(T left, T right);

通過上述語句,編譯器生成了一個Comparison類,該類派生自System.Delegate。該類包含一個方法,該方法返回1個int,有2個參數(即和簽名相同)。

你可以在類內部、命名空間內、全局命名空間中定義委托。(當然,不建議在全局命名空間中定義委托)

編譯器同時會為該類生成添加、刪除程序,該類的使用者可以從1個實例的調用列表中添加、刪除方法。編譯器強制添加、刪除的方法的簽名與聲明該方法時使用的簽名匹配。

聲明委托的實例

定義委托類型之后,你就可以創建委托的實例了。實例的創建和其他變量的創建沒有區別。

public Comparison<T> comparator;

變量comparator的類型是我們之前定義的委托類型Comparison<T>。跟變量一樣,我們可以聲明局部委托變量,把委托變量當做方法參數等。

分配、添加和移除方法

每個委托實例包含1個調用列表,調用列表包含所有分配給委托實例的方法。

想要將方法分配給委托實例,首先需要定義簽名與委托類型定義匹配的方法。可以看到下面這個CompareLength方法的簽名與委托類型的定義相同,而其內部是個string 類的方法。

//這是一種用lambda表達式定義的方法
private static int CompareLength(string left, string right) =>
left.Length.CompareTo(right.Length);	

通過將該方法傳遞給 List.Sort() 方法來創建該關系:

//使用上述定義的方法名。
phrases.Sort(CompareLength);
//這里不用糾結為什么是這樣傳入,它只是docs的一個例子,其內部肯定有
//comparator = CompareLength;
//這樣的形式

這里 將方法名用作參數會告知編譯器將方法引用 轉換為可以用作委托調用目標的引用,並將該方法作為調用目標進行附加。其核心如下

//左邊是委托變量,右邊是方法名稱
comparator = CompareLength;

聲明Comparison類型的變量並進行分配的操作就是下面這樣:

public Comparison<string> comparer = CompareLength;
private static int CompareLength(string left, string right) =>
    left.Length.CompareTo(right.Length);

當然如果委托目標的方法是很短的方法 ,你也可以使用lambda

public Comparison<string> comparer = (left, right) =>
    left.Length.CompareTo(right.Length);

這里看到的都是單個目標方法添加到委托變量,但委托支持將多個方法添加到委托變量的調用列表

調用委托

通過下面這種委托變量名+參數的形式,我們調用了附加到委托的方法列表中的方法。

int result = comparator(left, right);

如果並沒有任何附加到comparator變量的方法,上面代碼將應發NullReferenceException

MulticastDelegate

System.MulticastDelegateSystem.Delegate的單個直接子類。C#禁止從DelegateMulticastDelegate。當使用delegate關鍵字定義、聲明委托類型時,C#編譯器會創建從MulticastDelegate派生的實例。為了類型安全的考慮,編譯器創建了具體的委托類。

與委托實例一起使用的最多的方法時Invoke()BeginInvoke()/EndInvoke()Invoke()調用已附加到特定委托實例上的所有方法。

強類型委托

上一節中我們看到可以用delegate關鍵字創建特定的委托類型。

當你需要不同的方法簽名時,你將創建新的委托類型。一段時間后這項工作可能會變得乏味,因為每個新功能都需要新的委托類型。

幸運的是,.NET Core框架包含幾種類型,你可以在需要委托類型時重用它們。

這些類型中第一個是Action:

public delegate void Action();
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);

Action委托有多種變體,最多包含16個參數。Action沒有返回值。

第二個常用的是Func:

public delegate TResult Func<out TResult>();
public delegate TResult Func<in T1, out TResult>(T1 arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

Func委托最多包含16個輸入參數,結果類型始終是最后一個類型參數。Func有返回值。

還有一種是Predicate<T>

public delegate bool Predicate<in T>(T obj);

那么你可以注意到,對於任何Predicate委托類型都有一個相等的Func委托類型:

Func<string, bool> TestForString;
Predicate<string> AnotherTestForString

現在你不需要為任何新功能定義新的委托類型,關於這些特殊的委托類型的用法我將在另外一篇博客中羅列,但現在我們可以想象到,Action的用法應該如下:

Action showMethod = SomeMethod();
showMethod();

委托的常用模式

委托提供了一種機制,它使軟件設計涉及的組件之間的耦合最小。

LINQ是這種設計的一個很好的例子。LINQ查詢表達式模式的所有功能都依賴於委托。考慮下面這樣一個簡單的例子:

var smallNumbers = numbers.Where(n => n < 10);
//括號中是Func的lambda寫法,Action和Func的用法我將在另一篇博客中介紹,這里你只需要知道括號中傳入的是一個已經賦值的委托實例。

上述例子將序列過濾為僅小於10的數字。Where方法使用委托來確定序列中哪些元素被過濾出來。

Where方法的原型是:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> souce, Func<TSource, bool> predicate);

這個示例說明了委托是如何減少組件之間的耦合的,你可以無需創建派生自特定積累的類,你也不需要實現特定接口。你唯一要做的是提供實現手頭任務的方法

使用代理創建你自己的組件

(這里開始docs舉了一個例子來說明如何在實際中使用委托)

讓我們來定義一個可用於大型系統中的日志消息組件,該組件中有很多常用功能,它接收來自系統中任何地方的消息。這些消息將具有不同的優先級。

首次實現

原始的實現是這樣的:我們接收一個message,然后使用委托將消息寫到控制台。

public static class Logger
{
    //Action委托實例
    public static Action<string> WriteMessage;
    //對外接口
    public static void LogMessage(string msg)
    {
        //調用委托上的方法
    	WriteMessage(msg);
    }

}

public static class LoggingMethods{
    //將信息打印到控制台的方法
    public static void LogToConsole(string message)
    {
    	Console.Error.WriteLine(message);
    }   
}


//委托實例賦值,這句話一般發生在LoggingMethods的構造器中
Logger.WriteMessage += LoggingMethods.LogToConsole;

附加到委托實例上的方法,可以是實例方法,也可以具有任何訪問權限。

格式化輸出

LogMessage方法中添加一些參數,以便日志類創建更多結構化消息。

public enum Severity{
	Verbose,
    Trace,
    Information,
    Warning,
    Error,
    Critical
}

利用Severity過濾打印的消息。

public static class Logger
{
    public static Action<string> WriteMessage;
    public static Severity LogLevel {get;set;} = Severity.Warning;
    public static void LogMessage(Severity s, string component, string
    msg)
    {
        //繼續增加篩選功能
        if (s < LogLevel)
        	return;
        var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
        WriteMessage(outputMsg);
    }
}

這里我們可以看到,Logger與任何輸出類的耦合非常松散,當我們改變Logger的打印條件時,具體的委托實現完全不需要改動。在實際中,日志輸出類可能位於不同的程序集中,利用委托進行耦合,它們完全不需要被重建。

第二個輸出引擎

讓我們在添加一個將消息記錄到文件的輸出引擎。這稍微有點復雜,這是一個封裝文件操作的類,並要確保每次寫入后始終關閉文件(這樣可以確保在生成每條消息后將所有數據刷新到磁盤)。

public class FileLogger
{
    private readonly string logPath;
    public FileLogger(string path)
    {
        logPath = path;
        Logger.WriteMessage += LogMessage;
    }
    public void DetachLog() => Logger.WriteMessage -= LogMessage;
    // make sure this can't throw.
    private void LogMessage(string msg)
    {
        try
        {
            using (var log = File.AppendText(logPath))
            {
                log.WriteLine(msg);
                log.Flush();
            }
        }
        catch (Exception)
        {
            // Hmm. We caught an exception while
            // logging. We can't really log the
            // problem (since it's the log that's failing).
            // So, while normally, catching an exception
            // and doing nothing isn't wise, it's really the
            // only reasonable option here.
        }
    }
}

創建此類后,可將它進行實例化,然后它會將其LogMessage 方法附加到Logger中:

var file = new FileLogger("log.txt");

也就是說你可以同時附加這兩種輸出日志的方法(向控制台和文件輸出)。

var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LogToConsole;

以后,即使在同一個應用程序中,也可刪除其中一個方法,而不會對系統造成任何其他問題:

Logger.WriteMessage -= LogToConsole;

再次提醒一下,你無需構建任何其他基礎結構即可支持多種輸出方法,這些被添加到委托實例的方法只是調用列表上的一種方法而已。

請注意,一定要確保委托方法不會引發任何異常,如果委托實例的調用列表中的任何一個方法拋出異常,則調用列表上其他方法都不會被調用。

Null 委托

WriteMessage未附加方法時,調用其將引發NullReferenceException

最后,讓我們更新LogMessage方法,以確保它在沒有任何委托方法的時候具有魯棒性。

public static void LogMessage(string msg)
{
	WriteMessage?.Invoke(msg);
}

當左操作數(本例中為 WriteMessage )為 null 時,null 條件運算符( ?. )會短路,這意味着不會嘗試調用委托方法。

小結

通過在設計中使用委托,不同的組件可以非常松散地耦合在一起。 這樣可提供多種優勢。 可輕松創建新的輸出機制並將它們附加到日志系統中。這些機制只需要一種方法:編寫日志消息的方法。這種設計在添加新功能時有非常強的彈性。任何編寫者只需要實現同一種參數和返回值的方法。該方法可以是靜態方法或實例方法。可以是公共的,私有的或其他任何合法的訪問權限。

下一篇我們講講事件


免責聲明!

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



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