1. 程式人生 > >16.2 【C# 5】呼叫者資訊特性

16.2 【C# 5】呼叫者資訊特性

16.2.1 基本行為

  .NET 4.5引入了三個新特性(attribute),即 CallerFilePathAttribute 、 CallerLineNumber- Attribute 和 CallerMemberNameAttribute 。 三 者 均 位 於 System.Runtime.Compiler- Services 名稱空間下。和其他特性一樣,在應用時可以省略 Attribute 字尾。鑑於這是最常見的 特性用法,本書後續內容會進行適當地縮寫。 這三個特性都只能應用於引數,並且只有在應用於可選引數時才有用。其理念非常簡單:如 果呼叫點沒有提供實參,則編譯器可使用當前檔案、行數或成員名來作為實參,而不使用常規的 預設值。如果呼叫者提供了實參,編譯器則將忽略這些特性。

 1         static void Main(string[] args)
 2         {
 3             ShowInfo();
 4             ShowInfo("fileName", -10);
 5             Console.ReadKey();
 6         }
 7         static void ShowInfo([CallerFilePath] string file = null, [CallerLineNumber] int line = 0, [CallerMemberName]string
member = null) 8 { 9 Console.WriteLine("{0}:{1} - {2}", file, line, member); 10 }

當然,並不需要總是為這些引數提供虛擬值,但顯式傳遞還是很有用的,尤其是想使用同樣的特性來記錄當前方法呼叫者的時候。成員名特型適用於所有成員 ,但下列成員將使用特殊的名稱:
 靜態建構函式: .cctor ;
 建構函式: .ctor ;
 解構函式: Finalize 。
當欄位初始化器與欄位名稱相同時,該名稱將作為方法呼叫的一部分。

  在兩種情況下呼叫者成員資訊不會生效。其一是特性初始化。程式碼清單16-3給出了一個特性 示例,希望可以得到其應用到的成員名稱,但遺憾的是編譯器在這種情況下不會自動完成任何資訊的填充。

1     public class MemberDescriptionAttribute : Attribute
2     {
3         public string Member { get; set; }
4         public MemberDescriptionAttribute([CallerMemberName]string member = null)
5         {
6             Member = member;
7         }
8     }

  這本可以很有用。我曾多次見過開發者通過反射得到特性後,卻不得不自己維護一個數據結 構,以儲存成員名和特性之間對映的例子,而這本可以由編譯器自動完成。 特性對動態型別無效,這是可以原諒的。程式碼清單16-4展示了不能生效的情況。

 1         static void Main(string[] args)
 2         {
 3             dynamic x = new TypeUsedDynamically();
 4             x.ShowCaller();
 5             Console.ReadKey();
 6         }
 7         class TypeUsedDynamically
 8         {
 9             internal void ShowCaller([CallerMemberName] string caller = "Unknown")
10             {
11                 Console.WriteLine("Called by: {0}", caller);
12             }
13         }

  程式碼清單16-4只打印出了 Called by: Unknown ,仿若應用特性不存在一般。儘管看上去有點遺憾,但要想讓它生效,編譯器需在每個可能需要呼叫者資訊的動態呼叫處都內嵌上成員名、檔名和行數。總的來說,這對大多數開發者來說都是得不償失的。

16.2.2 日誌

  呼叫者資訊最明顯的用途莫過於寫入日誌檔案。以前記日誌時,通常需要構造一個堆疊跟蹤 (如使用 System.Diagnostics.StackTrace )來查詢日誌資訊的出處。雖然它通常隱藏在日誌 框架的後臺,但依然無法改變其醜陋的存在。此外,它還可能存在效能問題,並且在JIT編譯器 內聯時十分脆弱。

  不難想象日誌框架會如何使用這個新特性,來低廉地記錄呼叫者資訊,即使某些程式集可能 通過剝離除錯資訊或混淆操作來保護行數和成員名也無妨。當然,想記錄完整的堆疊跟蹤時,由 於該特性起不到什麼作用,因此需各位自行實現這一操作。

  截至本書編寫之時,還沒有日誌框架使用過該特性。首先它需要面向.NET 4.5進行構建, 或者像16.2.4節介紹的那樣,需要顯式宣告這些特性。不過為自己喜歡的日誌框架編寫一個包 裝類,並提供呼叫者資訊還是很容易的。隨著時間的推移,我敢肯定所有日誌框架最終都會提 供此種功能。

 1     [AttributeUsage(AttributeTargets.All)]
 2     public class MemberDescriptionAttribute : Attribute
 3     {
 4         public MemberDescriptionAttribute([CallerMemberName] string member = null)
 5         {
 6             Member = member;
 7         }
 8 
 9         public string Member { get; set; }
10     }
11 
12     [Description("Listing 16.3")]
13     [MemberDescription]
14     class MemberNames
15     {
16         static MemberNames()
17         {
18             Log("Static constructor");
19         }
20 
21         public event EventHandler DummyEvent
22         { 
23             add { Log("Event add"); }
24             remove { Log("Event remove"); }
25         }
26 
27         static string foo = Log("Static variable initializer (foo)");
28 
29         string bar = Log("Instance variable initializer (bar)");
30 
31         private string this[int x] { get { return Log("Indexer"); } }
32 
33         private string Property
34         { 
35             get { return Log("Property get"); } 
36             set { Log("Property set"); }
37         }
38     
39         private void Method() { Log("Method"); }
40 
41         MemberNames()
42         {
43             Log("Constructor");
44         }
45         
46         ~MemberNames()
47         {
48             Log("Finalizer");
49         }
50 
51         static void Main()
52         {
53             var instance = new MemberNames();
54             instance.Property = instance[10] + instance.Property;
55             EventHandler lambda = (sender, args) => Log("Lambda expression");
56             lambda(null, EventArgs.Empty);
57             instance.DummyEvent += lambda;
58             instance.DummyEvent -= lambda;
59             var attribute = (MemberDescriptionAttribute) typeof(MemberNames).GetCustomAttributes(typeof(MemberDescriptionAttribute), false)[0];
60             Console.WriteLine("Attribute on type: {0}", attribute.Member);
61 
62             instance = null;
63             GC.Collect();
64             GC.WaitForPendingFinalizers();
65         }
66 
67         static string Log(string message, [CallerMemberName] string member = null)
68         {
69             Console.WriteLine("{0}: {1}", message, member);
70             return null; // Just for the variable initializers
71         }
72     }
View Code

16.2.3 實現 INotifyPropertyChanged

  三大特性之一的 [CallerMemberName] 還有一個不太明顯的用途,不過如恰好需要經常實 現 INotifyPropertyChanged 的話,這種用法就顯而易見了。

  該介面十分簡單,只包含一個型別為 PropertyChangedEventHandler 的事件。其委託類 型簽名如下:

    public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);

  PropertyChangedEventArgs 包含單一的建構函式:

        public PropertyChangedEventArgs(string propertyName);

  在C# 5之前,通常按以下方式實現 INotifyPropertyChanged 。

 1     class OldPropertyNotifier : INotifyPropertyChanged
 2     {
 3         public event PropertyChangedEventHandler PropertyChanged;
 4 
 5         private int firstValue;
 6         public int FirstValue
 7         {
 8             get { return firstValue; }
 9             set
10             {
11                 if (value != firstValue)
12                 {
13                     firstValue = value;
14                     NotifyPropertyChanged("FirstValue");
15                 }
16             }
17         }
18 
19         // Other properties with the same pattern
20 
21         private void NotifyPropertyChanged(string propertyName)
22         {
23             PropertyChangedEventHandler handler = PropertyChanged;
24             if (handler != null)
25             {
26                 handler(this, new PropertyChangedEventArgs(propertyName));
27             }
28         }
29     }

  輔助方法可避免在每個屬性中都加入空驗證。當然,也可以將其實現為擴充套件方法,以避免在 每個實現類中都重複一遍。

  這不僅冗長(此點沒有改變),而且脆弱。問題在於屬性的名稱( FirstValue )指定為字 符串字面量,而如果將屬性名重構為其他名稱,則很可能會忘記修改字串字面量。幸運的話, 工具和測試會幫助我們找到錯誤,但這仍然很醜陋。

  在C# 5中,大部分程式碼仍然相同,但可在輔助方法中使用 CallerMemberName ,讓編譯器來 填充屬性名,如程式碼清單16-6所示。

 1     class NewPropertyNotifier : INotifyPropertyChanged
 2     {
 3         public event PropertyChangedEventHandler PropertyChanged;
 4 
 5         private int firstValue;
 6         public int FirstValue
 7         {
 8             get { return firstValue; }
 9             set
10             {
11                 if (value != firstValue)
12                 {
13                     firstValue = value;
14                     NotifyPropertyChanged();
15                 }
16             }
17         }
18 
19         // Other properties with the same pattern
20 
21         private void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
22         {
23             PropertyChangedEventHandler handler = PropertyChanged;
24             if (handler != null)
25             {
26                 handler(this, new PropertyChangedEventArgs(propertyName));
27             }
28         }
29     }

  此處只展示了發生變化的程式碼,就這麼簡單。現在如改變屬性的名稱,編譯器則可用新名稱 進行替代。這並不是驚天動地的大改進,但卻非常不錯。

16.2.4 在非.NET 4.5 環境下使用呼叫者資訊特性

  與擴充套件方法一樣,呼叫者資訊特性也只是請求編譯器在編譯過程中進行程式碼的轉換。該類特性並沒有使用我們無法提供的資訊,只是在使用時需格外小心。跟擴充套件方法一樣,我們也可以在早期.NET版本中使用它們,只需自己宣告這些特性即可,這就如同從MSDN中複製宣告一樣簡單。這些特性本身不包含任何引數,所以在類宣告中無須提供其他內容,但仍然要放在 System.Runtime.CompilerServices 名稱空間中。

  C#編譯器將按處理.NET 4.5中真正的呼叫者資訊特性那樣來處理使用者提供的特性。這麼做的 缺點是,用.NET 4.5編譯同樣的程式碼時會產生錯誤。此時只需移除手動建立的特性,以避免編譯 器產生混淆即可。

如果使用的是.NET 4、Silverlight 4/5或Windows Phone 7.5,還可使用 Microsoft.Bcl Nuget 包。包內提供了這些特性,以及其他期待中的有用型別。

  這就是有關C# 5的全部內容。