[分享] C# 小技巧系列 - 設計 [[ 用 IComparable 和 IComparer 來實作順序關係 ]] 我們通常需要比較兩件東西的大小關係,例如: //// using System; class MainClass { static void Main() { int a = 2; int b = 8; if (a < b) { Console.WriteLine("a is less than b"); } else if (a == b) { Console.WriteLine("a is equal to b"); } else if (a > b) { // or use "else" Console.WriteLine("a is greater than b"); } if (a.CompareTo(b) < 0) { Console.WriteLine("a is less than b"); } else if (a.CompareTo(b) == 0) { Console.WriteLine("a is equal to b"); } else if (a.CompareTo(b) > 0) { Console.WriteLine("a is greater than b"); } } } //// 這句 if (a < b) ,也可以改用 if ( a.CompareTo(b) < 0 ), 您可以參考 IComparable.CompareTo 的用法,大概是這樣: 1) 如果 a 小於 b,就傳回小於零的值 2) 如果 a 等於 b,就傳回零 3) 如果 a 大於 b,就傳回大於零的值 (Comparable指「可比較的」) 我們懂得如何比較 int 型別的大小,但其它型別呢? 假設您負責寫一個 Person類,您要定義大小關係, 例如年齡較大的為大,年齡較小為小。 這時您可以實作 IComparable介面(稍後再談 IComparable的用途): /// using System; public class Person : IComparable { private string m_Name; private int m_Age; public Person(string name, int age) { m_Name = name; m_Age = age; } public int CompareTo(object right) { Person aPerson = (Person) right; if (this.m_Age < aPerson.m_Age) { return -1; } else if (this.m_Age == aPerson.m_Age) { return 0; } else if (this.m_Age > aPerson.m_Age) { return 1; } return 0; } public override string ToString() { return "Name:" + m_Name +", Age:" + m_Age; } } class MainClass { static void Main() { Person tom = new Person("TOM", 20); Person sam = new Person("SAM", 30); if ( tom.CompareTo(sam) < 0 ) { Console.WriteLine("tom's age is less than sam"); } else if ( tom.CompareTo(sam) == 0 ) { Console.WriteLine("tom's age is equal to sam"); } else if ( tom.CompareTo(sam) > 0 ) { Console.WriteLine("tom's age is greater than sam"); } } } /// - 透過 IComparable.CompareTo 方法,傳回小於、等於或大於零的值, 來定義 Person類的物件大小關係。 - 當呼叫 tom.CompareTo(sam)時,入到 CompareTo(object right) 裡面, this表示左邊的tom ,參數 right 表示右邊的 sam。 (this是可以省略的,但這裡為了清楚而加入this) 現在來慢慢改良它,第一,因為 m_Age 是 int型別,而 int型別本身也提供了 CompareTo方法, 所以Person.CompareTo可以簡化成: public int CompareTo(object right) { Person aPerson = (Person) right; return m_Age.CompareTo( aPerson.m_Age ); } 第二,因為參數是 object型別,所以別人可以故亂輸入一些不相容於 Person的型別的參數, 例如: tom.CompareTo("string type") 所以您要用 is 來檢查型別是否相容(是 Person類或它的派生類): public int CompareTo(object right) { if ( !(right is Person) ) { throw new ArgumentException("Argument is not a Person", "right"); } Person aPerson = (Person) right; return m_Age.CompareTo( aPerson.m_Age ); } 第三,這裡有個效能問題,因為就算您呼叫時的參數型別是相容於 Person, 也要把參數 cast 成 Person,造成額外的開支。 解決辦法是把兩種情況分開,寫成兩個方法: public int CompareTo(Person right) { return m_Age.CompareTo( right.m_Age ); } int IComparable.CompareTo(object right) { if ( !(right is Person) ) { throw new ArgumentException("Argument is not a Person", "right"); } Person aPerson = (Person) right; return CompareTo( aPerson ); } 第一個是 Person.CompareTo(Person right),處理相容於 Person 型別的參數, 可以避免 cast。 第二個是 IComparable.CompareTo(object right),它是個explicit interface implementations, 只有interface instance才能呼叫它。 這樣做的效果是,如果用Person物件來呼叫CompareTo,就能避免不相容的參數型別: Person tom = Person(); // 湯姆 Car mybenz = Car(); // 我的賓士汔車 if ( tom.CompareTo( mybenz ) > 0 ) ... // 比較湯姆和我的賓士汔車的年齡 這能夠產生編譯錯誤,因為根本沒有宣告 Person.CompareTo(Car)方法。 如果真的想拿 Person和Car的物件來作比較,可以寫: Person tom = Person(); Car mybenz = Car(); // 賓士汔車 IComparable something = tom; if ( something.CompareTo( mybenz ) > 0 ) ... // 或者 if ( ( tom as IComparable ).CompareTo( mybenz ) > 0 ) ... 這樣會呼叫 Person 類內的 IComparable.CompareTo(object right), 因為 something 是個 interface instance。 介紹了IComparable,到底實作 IComparable有什麼用呢? 就是可以用在排序和搜索,因為排序涉及兩個動作:比較大小和交換位置。 Array.Sort和ArrayList.Sort,都會用到每個元素的 IComparable實作, 而所有實作都要有能力跟其餘每一個元素作比較。 例子: /// using System; using System.Collections; // 省略了 Person 類的內容 class MainClass { static void Main() { Person[] persons = { new Person("JOE", 40) ,new Person("SAM", 30) ,new Person("TOM", 20) }; Array.Sort(persons); for (int i = 0; i < persons.Length; i++){ Console.WriteLine("{0}=> {1}", i, persons[i].ToString()); } } } /// IComparable討論大概如此,現在討論它的好朋友 IComparer(比較器)。 之前只示範如何以 Person的年齡來排序,如果用 SQL 來表示就好像: order by person.age // 由小至大 但在現實世界,通常還需要: order by person.age desc // 由大至小 order by person.age, person.name // 多個排序鍵 order by person.age, person.name, car.age // 來自不同的類 這時可以用 IComparer 。當然,如果是太複雜的排序,用其它辦法例如DataView會比較簡單。 以下例子示範用 IComparer 來 order by person.name /// using System; using System.Collections; public class Person : IComparable { private string m_Name; private int m_Age; private static NameComparer m_NameComparer = null; public Person(string name, int age) { m_Name = name; m_Age = age; } public int CompareTo(Person right) { return m_Age.CompareTo( right.m_Age ); } int IComparable.CompareTo(object right) { if ( !(right is Person) ) { throw new ArgumentException("Argument is not a Person", "right"); } Person aPerson = (Person) right; return CompareTo( aPerson ); } public override string ToString() { return "Name:" + m_Name +", Age:" + m_Age; } public static IComparer ByNameComparer { get { if (m_NameComparer == null) { m_NameComparer = new NameComparer(); } return m_NameComparer; } } private class NameComparer : IComparer { int IComparer.Compare(object left, object right) { if ( !(left is Person) ) { throw new ArgumentException("Argument is not a Person", "left"); } if ( !(right is Person) ) { throw new ArgumentException("Argument is not a Person", "right"); } Person leftPerson = (Person) left; Person rightPerson = (Person) right; return leftPerson.m_Name.CompareTo( rightPerson.m_Name ); } } } class MainClass { static void Main() { Person[] persons = { new Person("JOE", 40) ,new Person("SAM", 30) ,new Person("TOM", 20) }; Console.WriteLine("using Person.CompareTo"); Array.Sort(persons); for (int i = 0; i < persons.Length; i++){ Console.WriteLine("{0}=> {1}", i, persons[i].ToString()); } Console.WriteLine("using NameComparer.Compare"); Array.Sort(persons, Person.ByNameComparer); for (int i = 0; i < persons.Length; i++){ Console.WriteLine("{0}=> {1}", i, persons[i].ToString()); } } } /// 要點: - Array.Sort 讓您輸入一個 IComparer型別參數,用來自訂排序方式。 - 透過定義多個不同的比較器,就能提供多種不同的排序方式。 - NameComparer被包在 Person內, 因為假設您負責寫Person類,而且它只會用來排序 Person物件, 所以不會被其它類用到。 不過,就算您沒有權去寫 Person類,您也可以寫比較器來定義它的排序方式, 因為寫Person類的作者可以提供public property給您。 以下示範 order by string[0] 和 order by string[1], 雖然您不能修改 string類,但仍然可以定義排序 string 的方式。 /// using System; using System.Collections; public class CharPositionComparer : IComparer { private int m_Position; public CharPositionComparer(int position) { m_Position = position; } int IComparer.Compare(object left, object right) { string leftValue = (string) left; string rightValue = (string) right; return leftValue[m_Position].CompareTo( rightValue[m_Position] ); } } class MainClass { static void Main() { string[] anArray = new string[]{"AZ", "BY", "CX", "DW", "EV"}; Console.WriteLine("sort by the [0] char"); Array.Sort(anArray, new CharPositionComparer(0) ); Console.WriteLine( String.Join("\n", anArray) ); Console.WriteLine("sort by the [1] char"); Array.Sort(anArray, new CharPositionComparer(1) ); Console.WriteLine( String.Join("\n", anArray) ); } } /// 最後,留一些問題給讀者: 1) 如果要 order by string[0], string[2], string[1] ,應該怎樣做? 例如 ABC 和 AAC ,先比較第0個字元,如果第0個相同,就比較第2個, 如果第2個相同,就比較第1個。 2) 如何進行反序排列?例如 order by string[0] desc [[ 愛用 interface,多於承繼 ]] 用interface 好還是 abstract class 好?還是兩者混合來用好? 先看一些例子。 例:承繼 System.Collections.CollectionBase 它的宣告是: public abstract class CollectionBase : IList, ICollection, IEnumerable 即 CollectionBase 是個 abstract class,它實作了IList, ICollection, IEnumerable介面。 它的主要用途是讓您建立 strongly type的集合。 例如您要建立一個好像 ArrayList的類,名叫 IntList,但只容許它儲存 int型別。 您可以承繼 CollectionBase: /// using System; using System.Collections; public class IntList : System.Collections.CollectionBase { protected override void OnInsert(int index, object value) { try { int newValue = System.Convert.ToInt32(value); Console.WriteLine("before inserting {0} at position {1}", newValue, index); } catch (Exception e) { throw new ArgumentException("Argument type is not an integer", "value", e); } } } class MainClass { static void Main() { IntList x = new IntList(); IList aList = x as IList; aList.Add(11); aList.Add(22); foreach (int i in aList) { Console.WriteLine(i); } aList.Add("not an integer"); } } /// - 當還未插入數值之前,CollectionBase.OnInsert會被呼叫。 您只要 override 它,就可以加入檢查型別的工作, 透過throw出 Exception來阻止插入元素。 例:用 interface 型別做參數。 /// using System; using System.Collections; class MainClass { static void Main() { int[] a = new int[]{11, 22}; ArrayList b = new ArrayList(); Hashtable c = new Hashtable(); b.Add(11); b.Add(22); c.Add(11, 111); c.Add(22, 222); PrintCollection(a); PrintCollection(b); PrintCollection(c); } public static void PrintCollection(IEnumerable c) { if (c is Hashtable) { foreach (DictionaryEntry de in c) { Console.WriteLine( de.Key + "=>" + de.Value ); } } else { foreach (object o in c) { Console.WriteLine(o.ToString()); } } Console.WriteLine(); } } /// 這裡的 PrintCollection 的參數型別是 interface : public static void PrintCollection(IEnumerable c) 它比用 CollectionBase 更能夠複用: public static void PrintCollection(CollectionBase c) 因為前者可以用於那些實作了 IEnumerable介面的類。 <續> 一些沒有承繼關係、甚至完全沒有關係的類,也可以實作同一個介面。 例如Employee,Customer和Vendor,三者沒有任何承斷關係, 但都可以實作 IContactInfo。 有什麼好處?例如我負責寫程式來顯示所有連聯資料(名稱和電話號碼), 這時,我只需要知道某件東西有沒有名稱和電話號碼, 我不需要知道它是什麼東西、有什麼承繼關係。 /// using System; public interface IContactInfo { string Name { get; } string PhoneNumber { get; } } public class Employee : IContactInfo { private string m_Name; private string m_PhoneNumber; public string Name { get { return m_Name; } set { m_Name = value; } } public string PhoneNumber { get { return m_PhoneNumber; } set { m_PhoneNumber = value; } } } class MainClass { static void Main() { Employee e = new Employee(); e.Name = "TOM"; e.PhoneNumber = "1234"; PrintContactInfo(e); } public static void PrintContactInfo(IContactInfo ic) { Console.WriteLine("Name:{0} , Phone:{1}", ic.Name , ic.PhoneNumber); } } /// 只要Customer 和 Vendor都實作 IContactInfo: public class Customer : IContactInfo public class Vendor : IContactInfo 那麼 PrintContactInfo(IContactInfo ic) 也能支援它們了。 例:傳回 interface ,可以限制客戶使用指定介面,提高保安。 (本例將會在「避免傳回內部物件的參考」一節討論) 例:使用interface,可以為struct節省一個unboxing的動作,提高效能。 (本例將會在「減少boxing和unboxing」一節討論) 看完以上例子,現在看abstract class和interface的差別, abstract class: - C#不容許多重承繼,即一個class最多只能承繼一個class,不能承繼多個class。 - 承繼是用來表達「是一個」(is-a)的關係,這是一個什麼物件。 - 為一個class hierarchy提供一個共同的祖先。 - 可以在abstract class 內提供一些實作,可以提供data member。 - 如果您在一個base class 加入一個method,那所有的派生類都會自動擁有這個method, 這樣伸展行為是很有效率的。 interface: - 一個class可以實作多於一個interface。 - 表達「行為好像」(behaves like)的關係。 - 描述一個功能集合,或一個合約,所有實作這個interface的type, 都必需實作該interface所定義的元素,例如methods, properties, indexers, events。 - 沒有關連的type,都可以實作同一個interface。 - 實作一個interface 比較容易,但承繼一個class就比較難。 - 不能在interface內提供實作,也不能提供data member。 - 如果您在一個interface 加入一個member,那所有實作該interface的類都會被破壞, 您需要在所有類加入這個member。 [[ 避免用 new 修飾詞 ]] 舉個例:假設現在有三個class,Person、Employee僱員和Manager主管, 您負責寫Employee,而Person和Manager是由其它人開發的。 您覺得Person很好用,所以就決定Employee承繼Person, 而Manager的作者也覺得您的Employee很好用,所以Manger也承繼Employee。 /// using System; public class Person { public void Eat() { Console.WriteLine("called Person.Eat()"); } } public class Employee : Person { public void Hello() { Console.WriteLine("called Employee.Hello()"); } } public class Manager : Employee { public void Manage() { Console.WriteLine("called Manager.Manage()"); } } class MainClass { static void Main() { Person p = new Person(); Employee e = new Employee(); Manager m = new Manager(); p.Eat(); e.Eat(); e.Hello(); m.Eat(); m.Hello(); m.Manage(); } } /// 隨著時間過去,Person的作者增強了它的功能,加入了 Hello() 方法, 發佈了一個新版本的 Person: public class Person { public void Eat() { Console.WriteLine("called Person.Eat()"); } public void Hello() { Console.WriteLine("called Person.Hello()"); } } 您當然不會錯過新版本,立即拿來建立Employee,誰知發生編譯錯誤 CS0108: The keyword new is required on 'Employee.Hello()' because it hides inherited member 'Person.Hello()' 原來Person和Employee都剛好有個一模一樣的Hello()方法。 假設您不能要求Person的作者改用其它方法名稱,怎麼辦? 方法一:如果您可以修改所有使用Employee的客戶,例如Manager, 那就修改Employee和Manager的方法名稱,因為長遠來說,這是比較好的做法。 方法二:如果您不能修改所有客戶,例如您之前已經在全球發佈了Employee, 那就沒可能要求所有客戶都修改。這時,new 修飾詞可以很輕易解決問題。 public class Employee : Person { public new void Hello() { Console.WriteLine("called Employee.Hello()"); } } 所有客戶都只會呼叫 Employee.Hello(),而不是 Person.Hello(), new 修飾詞是用來隱藏那些承繼得來的member,即 Person.Hello(), 所以Manager就看不到它。 雖然 new 可以解決問題,但長遠來說,可能會發生其它問題。 例如將來 Manager的作者也想用 Person.Hello(), 他發現Employee和Person 的Hello()都不是 virtual方法, 外表看起來是一模一樣,但實際上是不同的,他會覺得很混亂。 例: /// using System; public class Person { public virtual void Hello() { Console.WriteLine("called Person.Hello()"); } } public class Employee : Person { public override void Hello() { Console.WriteLine("called Employee.Hello()"); } } public class Manager : Employee { } class MainClass { static void Main() { Manager m = new Manager(); Person p = m as Person; p.Hello(); Employee e = m as Employee; e.Hello(); } } /// 如果是virtual方法,而且Person和Employee有承繼關係的話, 無論透過 Person或Employee參考,都會呼叫同一個方法 Employee.Hello(), 因為兩者都在用同一個物件 m。這是比較直觀的。 但如果是non-virtual方法,而且用了 new 修飾詞, 那麼p.Hello()會呼叫 Person.Hello(),而 e.Hello()會呼叫 Employee.Hello()。 明明在用同一個物件m,竟然呼叫了不同的方法,這是比較混亂的。 non-virtual方法是statically bound的,在原始碼裡呼叫 Person.Hello(), 它就一定是呼叫 Person.Hello(),不會變成呼叫它的派生類 Employee.Hello()。 相反,virtual方法是dynamically bound的,會跟據執行階段的型別來呼叫不同的方法, 例如雖然 p.Hello()和 e.Hello()分別透過 Person和Employee參考來呼叫 Hello(), 但p 和 e 的執行階段型別是 Manager,而Manager承繼Employee, 所以兩者都會呼叫 Employee.Hello()。 既然new 會產生那麼多問題,為何不把Person所有的方法都變成 virtual, 然後在Employee用override呢? 這樣又不好,因為一般來說,只有那些用在多型的方法才應該是virtual。 如果把所有方法都變成virtual的話,就會連那些不應該是多型的方法, 也可以被派生類override了。 總括來說,只有當遇到上述的特殊情況,才會用到new, 就算真的要用,也要想得很清楚。 輸入 interface 較有彈性,那傳回 interface又有什麼好處呢? 很多應用程式都會用 DataSet來傳送資料(將來再討論用DataSet的好處), 所以您可能會直接傳回DataSet: public class Person { private DataSet m_Info; public DataSet Info { get { return m_Info; } } } 但有個問題,假如我們將來要改用DataTable、DataView甚至其它物件, 就會破壞所有使用這個property的程式,因為它們仍然期望您傳回DataSet。 第二個問題比較嚴重,就是 DataSet 提供了一些public 方法, 傳回DataSet就會暴露所有這些public member(將來再深入討論這個問題), 例如讓其它人刪除DataTable、修改DataColumn、甚至取代裡面所有物件, 但這些通常不是您想要的效果。 解決辦法是傳回 interface: public IListSource Info { get { return m_Info as IListSource; } }