索引器的簡單實現 我們慣於操作數組的形式通常為array[i],集合類可以看作是“對象的數組”,在C#中,幫助集合類實現數組式索引功能的就是索引器: public Customer this[int index] { get { return (Customer) List[index]; } } 將以上代碼加入到Customers類後,就實現了以整形index為參數,以List[index]強制類型轉換後的Customer類型返回值的Customers類只讀索引器,使用者以Customers[i].Name的方式,就可以訪問Customers集合中第i個Customer對象的姓名字段,是不是很神奇呢?文中的索引器代碼並未考慮下標越界的問題,越界的處理方式應參照與之類似的Remove方法。作者在此只實現了索引器的get訪問,沒有實現set訪問的原因將在下文中討論。
Item的兩種實現方式 用過VB的朋友們一定都很熟悉Customers.Itme(i).Name的形式,它實現了與索引器相同的作用,即通過一個索引值來訪問集合體中的特定對象,但Item在C#當中應該以怎樣的形式實現呢?首先想到的實現途徑應該是屬性,但你很快就會發現C#的屬性是不支持參數的,所以無法把索引值作為參數傳入,折中的辦法就是以方法來實現: public Customer Item (int Index) { return (Customer) List[Index]; } 這個Item方法已經可以工作了,但為什麼說是折中的辦法呢,因為對Item的訪問將是采用Customers.Item(i).Name的語法形式,與C#‘[]’作數組下標的風格不統一,顯的有些突兀,但如果希望在語法上做到統一,哪怕是性能受一些影響也無所謂的話有沒有解決之道呢?請看以下代碼: public Customers Item { get { return this; } } 這是以屬性形式實現的Item接口,但是由於C#的屬性不支持參數,所以我們返回Customers對象本身,也就是在調用Customers對象Item屬性時會引發對Customers索引器的調用,性能有所下降,但是的確實現了Customers.Item[i].Name的語法風格統一。對比這兩種Item的實現,不難得出結論:以不帶參數的屬性形式實現的Item依賴於類的索引器,如果該類沒有實現索引器,該屬性將無法使用;並且由於對Item的訪問重定向到索引器性能也會下降;唯一的理由是:統一的C#索引下標訪問風格;采用方法實現的裨益正好與之相反,除了語法風格較為別扭外,不存在依賴索引器、性能下降的問題。魚與熊掌難以兼得,如何取捨應依據開發的實際需求決定。 中間語言的編譯缺省與Attribute的應用 如果你既實現了標准的索引器,又想提供名為“Item”的接口,編譯時就會出現錯誤“類‘WindowsApplication1.Customers’已經包含了“Item”的定義”,但除了建立索引器外,你什麼也沒有做,問題到底出在哪裡?我們不得不從.NET中間語言IL來尋找答案了,在.NET命令行環境或Visual Studio .NET 命令提示環境下,輸入ILDASM,運行.NET Framework MSIL 反匯編工具,通過主菜單中的‘打開’加載只有索引器沒有Item接口實現的可以編譯通過的.Net PE執行文件,通過直觀的樹狀結構圖找到Customers類,你將意外地發現C#的索引器被解釋成了一個名為Item的屬性,以下是IL反編譯後的被定義為Item屬性的索引器代碼: .property instance class WindowsApplication1.Customer Item(int32) { .get instance class WindowsApplication1.Customer WindowsApplication1.Customers::get_Item(int32) } // end of property Customers::Item 問題總算水落石出,就是C#編譯器‘自作聰明’地把索引器解釋成了一個名為Item的屬性,與我們期望實現的Item接口正好重名,所以出現上述的編譯錯誤也就在所難免。那麼,我們有沒有方法告知編譯器,不要將索引器命名為缺省Item呢?答案是肯定的。 解決方法就是在索引器實現之前聲明特性: [System.Runtime.CompilerServices.IndexerName("item")] 定義這個IndexerName特性將告知CSharp編譯器將索引器編譯成item而不是默認的Item ,修改之後的索引器IL反匯編代碼為: .property instance class WindowsApplication1.Customer item(int32) { .get instance class WindowsApplication1.Customer WindowsApplication1.Customers::get_item(int32) } // end of property Customers::item 當然你可以將索引器的生成屬性名定義成其它名稱而不僅限於item,只要不是IL語言的保留關鍵字就可以。經過了給索引器命名,你就可以自由地加入名為“Item”的接口實現了。
以下為Customer類和Customers類的調試代碼,在作者的Customers類中,為說明問題,同時建立了以item為特性名的索引器、一個Items方法和一個Item屬性來實現對集合元素的三種不同訪問方式,實際的項目開發中,一個類的索引功能不需要重復實現多次,可能只實現索引器或一個索引器加上一種形式的Item就足夠了: public class CallTest { public static void Main() { Customers custs=new Customers(); System.Console.WriteLine(custs.Count.ToString());//Count屬性測試
/// <summary> /// 客戶聯系說明 /// </summary> public string Summary { get { System.Console.WriteLine("getter Access"); return summary;//do something, as get data from data source } set { System.Console.WriteLine("setter Access"); summary=value;// do something , as check validity or Storage } }
public Contact() {
} }
public class Contacts { protected ArrayList List;
public void Add(Contact contact) { List.Add(contact); }
public void Remove(int index) { if (index > List.Count - 1 || index < 0) { System.Console.WriteLine("Index not valid!"); } else { List.RemoveAt(index); } }
public int Count { get { return List.Count; } }
public Contact this[int index] { get { System.Console.WriteLine("indexer getter Access"); return (Contact) List[index]; } set { List[index]=value; System.Console.WriteLine("indexer setter Access "); }
}
public Contacts() { List=new ArrayList(); } } 通過這兩個類的實現,我們可以總結以下要點: 采用ArrayList的原因 在Contacts實現內置集合對象時,使用了ArrayList類,而沒有使用大家較為熟悉的Array類,主要的原因有:在現有的.NET v1.1環境中,Array雖然已經暴露了IList.Add、IList.Insert、IList.Remove、IList.RemoveAt等典型的集合類接口,而實際上實現這些接口總是會引發 NotSupportedException異常,Microsoft是否在未來版本中實現不得而知,但目前版本的.NET顯然還不支持動態數組,在MS推薦的更改Array大小的辦法是,將舊數組通過拷貝復制到期望尺寸的新數組後,刪除舊數組,這顯示是費時費力地在繞彎路,無法滿足集合類隨時添加刪除元素的需求;ArrayList已經實現了Add、Clear、Count、IndexOf、Insert、Remove、RemoveAt等集合類的關鍵接口,並且有支持只讀集合的能力,在上邊的Contacts類中,只通過極少的封裝代碼,就輕松地實現了集合類。另一個問題是我們為什麼不采用與Customers類似的從System.Collections.ArrayList繼承的方式實現集合類呢?主要是由於將ArrayList對象直接暴露於類的使用者,將導致非法的賦值,如用戶調用arraylist.Add方法,無論輸入的參數類型是否為Contact,方法都將被成功執行,類無法控制和檢查輸入對象的類型與期望的一致,有悖該類只接納Contact類型對象的初衷,也留下了極大的安全隱患;並且在Contact對象獲取時,如不經過強制類型轉換,Contacts元素也無法直接以Contact類型形式來使用。 集合類中的Set 在集合類的實現過程中,無論是使用索引器還是與索引器相同功能的“Item”屬性,無可避免地會考慮是只實現getter形成只讀索引器,還是同時實現getter和setter形成完整的索引器訪問。在上文的示例類Customers中就沒有實現索引器的setter,形成了只讀索引器,但在Customer類和Customers類的調試代碼,作者使用了容易令人迷惑的“custs[0].Name="Test passed"”的訪問形式,事實上,以上這句並不會進入到Customers索引器的setter而是會先執行Customers索引器的getter得到一個Customer對象,然後設置這個Customer的Name字段(如果Name元素為屬性的話,將訪問Customer類Name屬性的setter)。那麼在什麼情況下索引器的setter才會被用到呢?其實只有需要在運行時動態地覆蓋整個元素類時,集合類的setter才變得有意義,如“custs [i]=new Customer ()”把一個全新的Customer對象賦值給custs集合類的已經存在的一個元素,這樣的訪問形式將導致Customers的setter被訪問,即元素對象本身進行了重新分配,而不僅僅是修改現有對象的一些屬性。也就是說,由於Customers類沒有實現索引器的setter 所以Customers類對外不提供“覆蓋”客戶集合中既有客戶的方法。與此形成鮮明對照的是Contacts類的索引器既提供對集合元素的getter,又提供對集合元素的setter,也就是說Contacts類允許使用者動態地更新Contact元素。通過對Contacts和Contact兩個類運行以下測試可以很明確說明這個問題: public class CallTest { public static void Main() { Contacts cons=new Contacts(); cons.Add(new Contact()); cons[0]=new Contact();//trigger indexer setter cons[0].Summary="mail contact about ticket"; System.Console.WriteLine(cons[0].Summary); } } 理所當然的輸出結果為: indexer setter Access indexer getter Access setter Access indexer getter Access getter Access mail contact about ticket 明確認識到了索引器setter的作用後,在類的實現中就應當綜合實際業務特點、存取權限控制和安全性決定是否為索引器建立setter機制。 屬性-強大靈活的字段 合二為一的方法 在最初實現Customer類時,我們使用了一個公共字段Name,用作存取客戶的姓名信息,雖然可以正常的工作,但我們卻缺乏對Name字段的控制能力,無論類的使用者是否使用了合法有效的字段賦值,字段的值都將被修改;並且沒有很好的機制,在值改變時進行實時的同步處理(如數據存儲,通知相關元素等);另外,字段的初始化也只能放在類的構造函數中完成,即使在整個對象生命周期內Name字段都從未被訪問過。對比我們在Contact類中實現的Summary屬性,不難發現,屬性所具有的優點:屬性可以在get時再進行初始化,如果屬性涉及網絡、數據庫、內存和線程等資源占用的方式,推遲初始化的時間,將起到一定的優化作用;經過屬性的封裝,真正的客戶聯系說明summary被很好地保護了起來,在set時,可以經過有效性驗證再進行賦值操作;並且在getter和setter前後,可以進行數據存取等相關操作,這一點用字段是不可能實現的。所以我們可以得出結論,在字段不能滿足需求的環境中,屬性是更加強大靈活的替代方式。 另外,屬性整合了“get”和“set”兩個“方法”,而采用統一自然的接口名稱,較之Java語言的object.getAnything和object.setAnything語法風格更加親和(事實上,C#中的屬性只不過是對方法的再次包裝,具有getter和setter的Anything屬性在.NET IL中,依然會被分解成一個由Anything屬性調用的get_Anything和set_Anything兩個方法)。 集合類內聯的方式 在文章最初的Customer類中使用了公共字段public Contacts Contacts=new Contacts()實現了customer. Contacts[]形式的集合類內聯接口,這是一種最為簡單但缺乏安全性保護的集合類集成方式,正如以上所述屬性的一些優點,采用屬性形式暴露一個公共的集合類接口,在實際存取訪問時,再對受封狀保護的集合類進行操作才是更為妥當完善的解決方案,如可以把Customer類內聯的集合Contacts的接口聲明改為: protected Contacts cons; //用於類內封裝的真正Contacts對象 public Contacts Contacts//暴露在類外部的Contacts屬性 { get { if (cons == null) cons=new Contacts(); return cons; } set { cons=value; } } 最終,customers[i].Contacts[x].Summary的形式就被成功地實現了。 實例化的最佳時機 .NET的類型系統是完全對象化的,所有的類型都是從System.Object派生而來,根據類型的各自特點,可以分為值類型和引用類型兩大陣營。值類型包括結構(簡單的數值型和布爾型也包括在內)和枚舉,引用類型則包括了類、數組、委托、接口、指針等,對象化的一個特點是直到對象實例化時才為對象分配系統資源,也就是說靈活適時地實例化對象,對系統資源的優化分配將產生積極意義。在一些文章中所建議的“Lazy initialization”倡導在必要時才進行對象的實例化,本著這樣的原則,從類的外部來看,類可以在即將被使用時再進行初始化;在類的內部,如屬性之類的元素,也可以不在構造函數中初始化,而直到屬性的getter被真正訪問時才進行,如果屬性一直沒有被讀取過,就不必要無意義地占用網絡、數據庫、內存和線程等資源了。但是也並不是初始化越晚越好,因為初始化是需要時間的,在使用前才進行初始化可能導致類的響應速度過慢,無法適應使用者的實時需求。所以在資源占用和初始化耗時之間尋求一個平衡點,才是實例化的最佳時機。