以上就是Customer類的簡單框架,實用的Customer類可能擁有更多的字段、屬性、方法和事件等。值得注意的是在Customer類中還以公共字段形式實現(xiàn)了對Contacts集合類的內(nèi)聯(lián),最終可形成Customer.Contacts[i]的接口形式,但這并不是最理想的集合類關(guān)聯(lián)方式,暫時將它注釋,稍后將詳加分析,這個類的代碼重在說明一個簡單類(相對于集合類的概念范疇)的框架;另外,該類還對類構(gòu)造函數(shù)進行了重載,為聲明該類的實例時帶name參數(shù)或不帶參數(shù)提供選擇性。
Customers類采用從CollectionBase繼承的方式,不再需要在類內(nèi)聲明一個作為Customer集合容器的List對象,因為CollectionBase類已經(jīng)內(nèi)置了一個List對象,并已經(jīng)實現(xiàn)了Count、Clear、RemoveAt等等IList的重要接口(具體請參照MSDN中的CollectionBase 成員),只需要用戶顯示實現(xiàn)Add、Remove、IndexOf、Insert等等接口,代碼中僅簡單實現(xiàn)了Add方法和Remove方法的整參數(shù)版本作為示例。這種集合類的實現(xiàn)具有簡單高效的特點,CollectionBase已經(jīng)實現(xiàn)了較為完善的功能,實施者只要在其基礎(chǔ)上擴展自己所需的功能即可。
我們慣于操作數(shù)組的形式通常為array[i],集合類可以看作是“對象的數(shù)組”,在C#中,幫助集合類實現(xiàn)數(shù)組式索引功能的就是索引器:
將以上代碼加入到Customers類后,就實現(xiàn)了以整形index為參數(shù),以List[index]強制類型轉(zhuǎn)換后的Customer類型返回值的Customers類只讀索引器,使用者以Customers[i].Name的方式,就可以訪問Customers集合中第i個Customer對象的姓名字段,是不是很神奇呢?文中的索引器代碼并未考慮下標越界的問題,越界的處理方式應參照與之類似的Remove方法。作者在此只實現(xiàn)了索引器的get訪問,沒有實現(xiàn)set訪問的原因?qū)⒃谙挛闹杏懻摗?/DIV>
Item的兩種實現(xiàn)方式
用過VB的朋友們一定都很熟悉Customers.Itme(i).Name的形式,它實現(xiàn)了與索引器相同的作用,即通過一個索引值來訪問集合體中的特定對象,但Item在C#當中應該以怎樣的形式實現(xiàn)呢?首先想到的實現(xiàn)途徑應該是屬性,但你很快就會發(fā)現(xiàn)C#的屬性是不支持參數(shù)的,所以無法把索引值作為參數(shù)傳入,折中的辦法就是以方法來實現(xiàn):
public Customer Item (int Index)
{
return (Customer) List[Index];
}
這個Item方法已經(jīng)可以工作了,但為什么說是折中的辦法呢,因為對Item的訪問將是采用Customers.Item(i).Name的語法形式,與C#‘[]’作數(shù)組下標的風格不統(tǒng)一,顯的有些突兀,但如果希望在語法上做到統(tǒng)一,哪怕是性能受一些影響也無所謂的話有沒有解決之道呢?請看以下代碼:
public Customers Item
{
get
{
return this;
}
}
這是以屬性形式實現(xiàn)的Item接口,但是由于C#的屬性不支持參數(shù),所以我們返回Customers對象本身,也就是在調(diào)用Customers對象Item屬性時會引發(fā)對Customers索引器的調(diào)用,性能有所下降,但是的確實現(xiàn)了Customers.Item[i].Name的語法風格統(tǒng)一。對比這兩種Item的實現(xiàn),不難得出結(jié)論:以不帶參數(shù)的屬性形式實現(xiàn)的Item依賴于類的索引器,如果該類沒有實現(xiàn)索引器,該屬性將無法使用;并且由于對Item的訪問重定向到索引器性能也會下降;唯一的理由是:統(tǒng)一的C#索引下標訪問風格;采用方法實現(xiàn)的裨益正好與之相反,除了語法風格較為別扭外,不存在依賴索引器、性能下降的問題。魚與熊掌難以兼得,如何取舍應依據(jù)開發(fā)的實際需求決定。
中間語言的編譯缺省與Attribute的應用
如果你既實現(xiàn)了標準的索引器,又想提供名為“Item”的接口,編譯時就會出現(xiàn)錯誤“類‘WindowsApplication1.Customers’已經(jīng)包含了“Item”的定義”,但除了建立索引器外,你什么也沒有做,問題到底出在哪里?我們不得不從.NET中間語言IL來尋找答案了,在.NET命令行環(huán)境或Visual Studio .NET 命令提示環(huán)境下,輸入ILDASM,運行.NET Framework MSIL 反匯編工具,通過主菜單中的‘打開’加載只有索引器沒有Item接口實現(xiàn)的可以編譯通過的.NET PE執(zhí)行文件,通過直觀的樹狀結(jié)構(gòu)圖找到Customers類,你將意外地發(fā)現(xiàn)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的屬性,與我們期望實現(xiàn)的Item接口正好重名,所以出現(xiàn)上述的編譯錯誤也就在所難免。那么,我們有沒有方法告知編譯器,不要將索引器命名為缺省Item呢?答案是肯定的。
解決方法就是在索引器實現(xiàn)之前聲明特性:
[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語言的保留關(guān)鍵字就可以。經(jīng)過了給索引器命名,你就可以自由地加入名為“Item”的接口實現(xiàn)了。
以下為Customer類和Customers類的調(diào)試代碼,在作者的Customers類中,為說明問題,同時建立了以item為特性名的索引器、一個Items方法和一個Item屬性來實現(xiàn)對集合元素的三種不同訪問方式,實際的項目開發(fā)中,一個類的索引功能不需要重復實現(xiàn)多次,可能只實現(xiàn)索引器或一個索引器加上一種形式的Item就足夠了:
public class CallTest
{
public static void Main()
{
Customers custs=new Customers();
System.Console.WriteLine(custs.Count.ToString());//Count屬性測試
Customer aCust=new Customer();//將調(diào)用不帶參數(shù)的構(gòu)造函數(shù)
aCust.Name ="Peter";
custs.Add(aCust);//Add方法測試
System.Console.WriteLine(custs.Count.ToString());
System.Console.WriteLine(custs.Item[0].Name);//調(diào)用Item屬性得到
custs.Items(0).Name+="Hu";//調(diào)用Items方法得到
System.Console.WriteLine(custs[0].Name);//調(diào)用索引器得到
custs.Add(new Customer("Linnet"));//將調(diào)用帶name參數(shù)的構(gòu)造函數(shù)
System.Console.WriteLine(custs.Count.ToString());
System.Console.WriteLine(custs.Items(1).Name);//調(diào)用Items方法得到
custs.Item[1].Name+="Li";//調(diào)用Items方法得到
System.Console.WriteLine(custs[1].Name);//調(diào)用索引器得到
custs.Remove(0);//Remove方法測試
System.Console.WriteLine(custs.Count.ToString());
System.Console.WriteLine(custs[0].Name);//Remove有效性驗證
custs[0].Name="Test passed" ;//調(diào)用索引器得到
System.Console.WriteLine(custs.Item[0].Name);
custs.Clear();
System.Console.WriteLine(custs.Count.ToString());//Clear有效性驗證
}
}
輸出結(jié)果為:
0
Initialize instance without parameter
1
Peter
PeterHu
Initialize instance with parameter
2
Linnet
LinnetLi
1
LinnetLi
Test passed
0
2.采用內(nèi)建ArrayList對象的方式實現(xiàn)集合類:
或許有經(jīng)驗的程序員們早已經(jīng)想到,可以在一個類中內(nèi)建一個數(shù)組對象,并在該類中通過封裝對該對象的訪問,一樣能夠?qū)崿F(xiàn)集合類。以下是采用這種思路的Contact元素類和Contacts集合類的實現(xiàn)框架:
public class Contact
{
protected string summary;
/// <summary>
/// 客戶聯(lián)系說明
/// </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();
}
}
通過這兩個類的實現(xiàn),我們可以總結(jié)以下要點:
采用ArrayList的原因
在Contacts實現(xiàn)內(nèi)置集合對象時,使用了ArrayList類,而沒有使用大家較為熟悉的Array類,主要的原因有:在現(xiàn)有的.NET v1.1環(huán)境中,Array雖然已經(jīng)暴露了IList.Add、IList.Insert、IList.Remove、IList.RemoveAt等典型的集合類接口,而實際上實現(xiàn)這些接口總是會引發(fā) NotSupportedException異常,Microsoft是否在未來版本中實現(xiàn)不得而知,但目前版本的.NET顯然還不支持動態(tài)數(shù)組,在MS推薦的更改Array大小的辦法是,將舊數(shù)組通過拷貝復制到期望尺寸的新數(shù)組后,刪除舊數(shù)組,這顯示是費時費力地在繞彎路,無法滿足集合類隨時添加刪除元素的需求;ArrayList已經(jīng)實現(xiàn)了Add、Clear、Count、IndexOf、Insert、Remove、RemoveAt等集合類的關(guān)鍵接口,并且有支持只讀集合的能力,在上邊的Contacts類中,只通過極少的封裝代碼,就輕松地實現(xiàn)了集合類。另一個問題是我們?yōu)槭裁床徊捎门cCustomers類似的從System.Collections.ArrayList繼承的方式實現(xiàn)集合類呢?主要是由于將ArrayList對象直接暴露于類的使用者,將導致非法的賦值,如用戶調(diào)用arraylist.Add方法,無論輸入的參數(shù)類型是否為Contact,方法都將被成功執(zhí)行,類無法控制和檢查輸入對象的類型與期望的一致,有悖該類只接納Contact類型對象的初衷,也留下了極大的安全隱患;并且在Contact對象獲取時,如不經(jīng)過強制類型轉(zhuǎn)換,Contacts元素也無法直接以Contact類型形式來使用。
集合類中的Set
在集合類的實現(xiàn)過程中,無論是使用索引器還是與索引器相同功能的“Item”屬性,無可避免地會考慮是只實現(xiàn)getter形成只讀索引器,還是同時實現(xiàn)getter和setter形成完整的索引器訪問。在上文的示例類Customers中就沒有實現(xiàn)索引器的setter,形成了只讀索引器,但在Customer類和Customers類的調(diào)試代碼,作者使用了容易令人迷惑的“custs[0].Name="Test passed"”的訪問形式,事實上,以上這句并不會進入到Customers索引器的setter而是會先執(zhí)行Customers索引器的getter得到一個Customer對象,然后設(shè)置這個Customer的Name字段(如果Name元素為屬性的話,將訪問Customer類Name屬性的setter)。那么在什么情況下索引器的setter才會被用到呢?其實只有需要在運行時動態(tài)地覆蓋整個元素類時,集合類的setter才變得有意義,如“custs [i]=new Customer ()”把一個全新的Customer對象賦值給custs集合類的已經(jīng)存在的一個元素,這樣的訪問形式將導致Customers的setter被訪問,即元素對象本身進行了重新分配,而不僅僅是修改現(xiàn)有對象的一些屬性。也就是說,由于Customers類沒有實現(xiàn)索引器的setter 所以Customers類對外不提供“覆蓋”客戶集合中既有客戶的方法。與此形成鮮明對照的是Contacts類的索引器既提供對集合元素的getter,又提供對集合元素的setter,也就是說Contacts類允許使用者動態(tài)地更新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);
}
}
理所當然的輸出結(jié)果為:
indexer setter access
indexer getter access
setter access
indexer getter access
getter access
mail contact about ticket
明確認識到了索引器setter的作用后,在類的實現(xiàn)中就應當綜合實際業(yè)務特點、存取權(quán)限控制和安全性決定是否為索引器建立setter機制。
屬性-強大靈活的字段 合二為一的方法
在最初實現(xiàn)Customer類時,我們使用了一個公共字段Name,用作存取客戶的姓名信息,雖然可以正常的工作,但我們卻缺乏對Name字段的控制能力,無論類的使用者是否使用了合法有效的字段賦值,字段的值都將被修改;并且沒有很好的機制,在值改變時進行實時的同步處理(如數(shù)據(jù)存儲,通知相關(guān)元素等);另外,字段的初始化也只能放在類的構(gòu)造函數(shù)中完成,即使在整個對象生命周期內(nèi)Name字段都從未被訪問過。對比我們在Contact類中實現(xiàn)的Summary屬性,不難發(fā)現(xiàn),屬性所具有的優(yōu)點:屬性可以在get時再進行初始化,如果屬性涉及網(wǎng)絡(luò)、數(shù)據(jù)庫、內(nèi)存和線程等資源占用的方式,推遲初始化的時間,將起到一定的優(yōu)化作用;經(jīng)過屬性的封裝,真正的客戶聯(lián)系說明summary被很好地保護了起來,在set時,可以經(jīng)過有效性驗證再進行賦值操作;并且在getter和setter前后,可以進行數(shù)據(jù)存取等相關(guān)操作,這一點用字段是不可能實現(xiàn)的。所以我們可以得出結(jié)論,在字段不能滿足需求的環(huán)境中,屬性是更加強大靈活的替代方式。
另外,屬性整合了“get”和“set”兩個“方法”,而采用統(tǒng)一自然的接口名稱,較之JAVA語言的object.getAnything和object.setAnything語法風格更加親和(事實上,C#中的屬性只不過是對方法的再次包裝,具有g(shù)etter和setter的Anything屬性在.NET IL中,依然會被分解成一個由Anything屬性調(diào)用的get_Anything和set_Anything兩個方法)。
集合類內(nèi)聯(lián)的方式
在文章最初的Customer類中使用了公共字段public Contacts Contacts=new Contacts()實現(xiàn)了customer. Contacts[]形式的集合類內(nèi)聯(lián)接口,這是一種最為簡單但缺乏安全性保護的集合類集成方式,正如以上所述屬性的一些優(yōu)點,采用屬性形式暴露一個公共的集合類接口,在實際存取訪問時,再對受封狀保護的集合類進行操作才是更為妥當完善的解決方案,如可以把Customer類內(nèi)聯(lián)的集合Contacts的接口聲明改為:
protected Contacts cons; //用于類內(nèi)封裝的真正Contacts對象
public Contacts Contacts//暴露在類外部的Contacts屬性
{
get
{
if (cons == null) cons=new Contacts();
return cons;
}