前言
來說一說委托(delegate)和事件(event),本篇采取的形式是翻譯微軟Delegate的docs中的重要部分(不要問我為什么微軟的docs有中文還要讀英文,因為讀中文感覺自己有閱讀障礙- -)+ 自己理解總結,適合不會或沒有使用過delegate的小白。
為什么要把委托和事件放在一起,因為委托Delegate是事件Event的基礎,並且他們容易被混淆。
原docs中對委托進行了一個定位:委托在.Net中提供后期綁定(Late Binding)機制。
System.Delegate和delegate關鍵字
定義委托類型
我們從delegate
關鍵字開始,因為它是你使用委托的主要方式。當你使用關鍵字delegate
時,編譯器生成的代碼將映射到一些方法,這些方法調用了Delegate
和MulticastDelegate
類的成員。
定義委托的語法跟定義方法簽名比較類似,你只需要在返回類型和訪問權限之間加上關鍵字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.MulticastDelegate
是System.Delegate
的單個直接子類。C#禁止從Delegate
和MulticastDelegate
。當使用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 條件運算符( ?. )會短路,這意味着不會嘗試調用委托方法。
小結
通過在設計中使用委托,不同的組件可以非常松散地耦合在一起。 這樣可提供多種優勢。 可輕松創建新的輸出機制並將它們附加到日志系統中。這些機制只需要一種方法:編寫日志消息的方法。這種設計在添加新功能時有非常強的彈性。任何編寫者只需要實現同一種參數和返回值的方法。該方法可以是靜態方法或實例方法。可以是公共的,私有的或其他任何合法的訪問權限。
下一篇我們講講事件