[分享] C# 小技巧系列 - C# 語言元素 [[關於本文]] C#小技巧系列,一冊有N章,每章有M節, 主要抽取 Effective C# 精華釀製而成, 以江湖失傳已久的斷章取義法加以提煉, 既能補腦、幫吸、戒殺、阻噴、扶大內, 又能避免身虛體弱者產生強列排斥現象, 因宗於程設之道:本功為主,補品乃輔, 此乃C#愛好者編程必備之最佳後備良藥。 [[ 永遠用 Property ]] 考慮以下程式: //// using System; public class Person { public string m_Name; } class MainClass { static void Main() { Person p = new Person(); p.m_Name = "Tom"; // set value Console.Write(p.m_Name); // get value } } //// 這個 Person 類的m_Name是 public,即暴露了m_Name 這個 data member到Person 類以外的地方, 違反了Encapsulation原則。應該改成: //// using System; public class Person { private string m_Name; public string GetName() { return m_Name; } public void SetName(string name) { m_Name = name; } } class MainClass { static void Main() { Person p = new Person(); p.SetName("Tom"); // set value Console.Write( p.GetName() ); // get value } } //// 現在: 1) m_Name 是 private,不會暴露到 Person 以外。 2) 改用了GetName 和 SetName 方法來存取 m_Name(又稱為getter和setter), 這樣能享有函數(指函數的概念)的好處。(本節暫不討論函數好處) 不過,用這些 Get/Set方法,就會令語法變得複雜,難以閱讀, 例如要把某人年齡加一,就要寫: p.SetAge( 1 + p.GetAge() ); // 年齡加一 而不能寫簡潔的: p.Age += 1; C# 提供了更好的語法,就是 Property,例如: //// using System; public class Person { public string Name { get { Console.WriteLine("called the get accessor of Name property"); return "anything"; } set { Console.WriteLine("called the set accessor of Name property"); Console.WriteLine("the value you want to set is: " + value); } } } class MainClass { static void Main() { Person p = new Person(); p.Name = "Tom"; // set value Console.Write( p.Name ); // get value } } //// Name 是個 Property,其中的 p.Name = "Tom"; 一行,它「看起來」好像把Name的值設成"Tom", 但實際上,它只是個 method ,不會儲存資料。 Property 的運作: 1) 當程式試圖取得 Property的值,例如執行 Console.Write( p.Name ),就會呼叫Name Property的 get accessor。 2) 當程式試圖設定 Property的值,例如執行 p.Name = "Tom" ,就會呼叫Name Property的 set accessor, 它會把 "Tom" 傳入 set accessor 內,裡面可以用隱性參數 value 來取得這個值, 但要怎樣處理這個值,就視乎set accessor 怎樣寫。 在以上例子,"Tom" 的值沒有儲存起來。 Property 的特點: 1) Property 只提供一種方法呼叫機制,原理就像上面的 GetName 和 SetName 函數一樣。 2) Property 本身不會儲存資料(正如SetName方法本身不會儲存資料,資料是儲存在 m_Name), 它透過呼叫方法,而您在方法內把資料存到某個變數,來達到儲存資料的效果。 以下例子示範把值儲存起來,它也是 Property 常見的用法。 //// using System; public class Person { private string m_Name; public string Name { get { return m_Name; } set { m_Name = value; } } } class MainClass { static void Main() { Person p = new Person(); p.Name = "Tom"; // set value Console.Write( p.Name ); // get value } } //// 這樣「看起來」是 p.Name 儲存了 "Tom",但實際上是儲存在 m_Name。 Property 的語法還可以伸展到 Indexer,兩者都只是個 method ,不會儲存資料的。 它們的分別在於 Property 看起來在存取一個 Field,而 Indexer就看起來在存取一個陣列: //// using System; public class MyClass { private int[] m_Array = new int[2]; public int this[int index] { get { return m_Array[index]; } set { m_Array[index] = value; } } public int this[string key] { get { int index = (key == "zero")? 0 : 1; return m_Array[index]; } set { int index = (key == "zero")? 0 : 1; m_Array[index] = value; } } } class MainClass { static void Main() { MyClass obj = new MyClass(); obj[1] = 101; // set obj["zero"] = 100; // set Console.WriteLine("[0]={0}, [1]={1}", obj[0], obj[1]); // get } } //// 跟據set accessor 內容,資料會儲存在m_Array,而不是obj本身。 雖然您見到 obj[1],但不要誤以為 obj 是個陣列。 還有 multi-dimensional indexer: public int this[int x, int y, int z] { get { return GetValue(x, y, z); } } public int this[int x, int y, string key] { get { return GetValue(x, y, key); } } 留意 Indexer 是用 this 來宣告的,您不能用其它名稱。 跟Get/Set方法比較,Property的好處是可以享用 Data Binding支援,例如: textBoxName.DataBindings.Add("Text", person, "Name"); 會把一個 textBoxName 的 "Text" Property,bind 到 person物件的 "Name" Property, textBoxName.Text <---bind---> person.Name 用 Property 就有這個好處,用 public data member 就沒有, 因為它不是個正當的物件導向技巧,所以 .Net 不支援。 用 Get/Set 也沒有,因為.Net 不會自動尋找這些方法名稱。 總括來說,Property 的好處是: 1) 簡化了存取的語法。 2) 可享用函數的好處。 3) 可享用 Data Binding 支援。 下期預告:愛用readonly,多於const。 [[ 使用 Constructor Chaining ]] 寫Constructor通常是一種重覆性的工作, 例如您想支援多種Constructor: public Person(string, int) public Person(string) public Person() 可能會寫: /// using System; public class Person { private string m_Name; private int m_Age; public Person(string name, int age) { Console.WriteLine("called Person(string, int)"); m_Name = name; m_Age = age; } public Person(string name) { Console.WriteLine("called Person(string)"); m_Name = name; m_Age = 0; } public Person() { Console.WriteLine("called Person()"); m_Name = "NO_NAME"; m_Age = 0; } public override string ToString() { return "name=" + m_Name + ";age=" + m_Age; } } class MainClass { static void Main() { Console.WriteLine("p1"); Person p1 = new Person("TOM", 20); Console.WriteLine("p2"); Person p2 = new Person("TOM"); Console.WriteLine("p3"); Person p3 = new Person(); Console.WriteLine("p1 : {0}", p1); Console.WriteLine("p2 : {0}", p2); Console.WriteLine("p3 : {0}", p3); } } /// 這樣容許不提供 age或name,它們的預設值分別是0和"NO_NAME"。 不過,以上程式出現程式碼重覆的問題,C++程式通常會用預設參數來解決這個問題, 但 C# 不支援預設參數。 所以,在C#應該用 Constructor Chaining: public class Person { private string m_Name; private int m_Age; public Person(string name, int age) { Console.WriteLine("called Person(string, int)"); m_Name = name; m_Age = age; } public Person(string name) : this(name, 0) { Console.WriteLine("called Person(string)"); } public Person() : this("NO_NAME") { Console.WriteLine("called Person()"); } public override string ToString() { return "name=" + m_Name + ";age=" + m_Age; } } 這句: public Person() : this("NO_NAME") 表示在執行 Person()的主體之前,先呼叫 Person("NO_NAME"),而: public Person(string name) : this(name, 0) 表示在執行 Person(name)的主體之前,先呼叫 Person(name, 0), 這樣就能做到預設值的效果。 還有一種解決辦法叫 Common Constructor,就是把重覆的敍述抽出, 寫成另一個 method(但不是Constructor): public class Person { private string m_Name; private int m_Age; public Person(string name, int age) { Console.WriteLine("called Person(string, int)"); CommonConstructor(name, age); } public Person(string name) { Console.WriteLine("called Person(string)"); CommonConstructor(name, 0); } public Person() { Console.WriteLine("called Person()"); CommonConstructor("NO_NAME", 0); } private void CommonConstructor(string name, int age) { Console.WriteLine("called CommonConstructor"); m_Name = name; m_Age = age; } public override string ToString() { return "name=" + m_Name + ";age=" + m_Age; } } 不過,與 Constructor Chaining 比較,它有以下缺點: - 效率較低。 - 不能賦值給 readonly 的field,例如 m_Name 現在要改成 readonly: private readonly string m_Name; 這樣只有在 Constructor之內才能賦值給它,所以用Constructor Chaining就沒問題。 用 CommonConstructor 就會出現編譯錯誤: CS0191: A readonly field cannot be assigned to (except in a constructor or a variable initializer)