通過C#實現集合類縱覽.NET Collections及相關技術

來源:互聯網
上載者:User
概述:在真正的對象化開發項目中,我們通常會將常用的業務實體抽象為特定的類,如Employee、Customer、Contact等,而多數的類之間會存在著相應的關聯或依存關係,如Employee和Customer通過Contact而產生關聯、Contact是依賴於Employee和Customer而存在的。在實際的對象應用模組中,可能會有這樣的需求:獲得一組客戶對象(即Customers集合類的執行個體,如customers),指向其中一個Customer對象(如customers[i]),通過訪問這個Customer對象的屬性Name(customers[i].Name)和Contacts(如customers[i].Contacts)來查詢客戶的姓名和與該客戶的聯絡記錄,甚至遍曆Contacts對象,尋找該客戶的某次聯絡摘要(即customers.[i].contacts[x].Summary)。為滿足以上集合類的需求,對照.NET Framework 的平台實現,不難發現.NET在Collections命名空間下提供了一系列實現集合功能的類,並且根據適用環境的不同為開發人員提供靈活多樣的選擇性:如通過索引訪問使用廣泛的ArrayList 和 StringCollection;通常在檢索後被釋放的先進先出的Queue和後進先出Stack;通過元素鍵對其元素進行訪問Hashtable、SortedList、ListDictionary 和 StringDictionary;通過索引或通過元素鍵對其元素進行訪問的NameObjectCollectionBase 和 NameValueCollection;以及具有集合類的特性而被實現在System.Array下的Array類等。本文將通過實現具有代表性的 “集合類”的兩種典型途徑,分析對比不同實現方式的差異性與適用環境,讓大家瞭解和掌握相關的一些技術,希望為大家的學習和開發工作起到拋磚引玉的作用(註:作者的調試運行環境為.NET Framework SDK 1.1)。
1.採用從CollectionBase抽象基類繼承的方式實現Customers集合類:
首先需要建立為集合提供元素的簡單類Customer:

/// <summary>
/// 描述一個客戶基本資料的類
/// </summary>
public class Customer
{
/// <summary>
/// 客戶姓名
/// </summary>
public string Name;

/// <summary>
/// 描述所有客戶聯絡資訊的集合類
/// </summary>
//public Contacts Contacts=new Contacts();

/// <summary>
/// 不帶參數的Customer類建構函式
/// </summary>
public Customer()
{
System.Console.WriteLine("Initialize instance without parameter");
}

/// <summary>
/// 帶參數的Customer類建構函式
/// </summary>
public Customer(string name)
{
Name=name;
System.Console.WriteLine("Initialize instance with parameter");
}
}

以上就是Customer類的簡單架構,實用的Customer類可能擁有更多的欄位、屬性、方法和事件等。值得注意的是在Customer類中還以公用欄位形式實現了對Contacts集合類的內聯,最終可形成Customer.Contacts[i]的介面形式,但這並不是最理想的集合類關聯方式,暫時將它注釋,稍後將詳加分析,這個類的代碼重在說明一個簡單類(相對於集合類的概念範疇)的架構;另外,該類還對類建構函式進行了重載,為聲明該類的執行個體時帶name參數或不帶參數提供選擇性。
接下來看我們的第一種集合類實現,基於從CollectionBase類派生而實現的Customers類:
/// <summary>
/// Customers 是Customer的集合類實現,繼承自CollectionBase
/// </summary>
public class Customers: System.Collections.CollectionBase
{
public Customers()
{

}
/// <summary>
/// 自己實現的Add方法
/// </summary>
/// <param name="customer"></param>
public void Add(Customer customer)
{
List.Add(customer);
}
/// <summary>
/// 自己實現的Remove方法
/// </summary>
/// <param name="index"></param>
public void Remove(int index)
{
if (index > Count - 1 || index < 0)
{
System.Console.WriteLine("Index not valid!");
}
else
{
List.RemoveAt(index);
}
}
}

以Customers集合類為例,結合集合輔助技術,希望大家能瞭解掌握以下知識:
從CollectionBase繼承實現集合類
Customers類採用從CollectionBase繼承的方式,不再需要在類內聲明一個作為Customer集合容器的List對象,因為CollectionBase類已經內建了一個List對象,並已經實現了Count、Clear、RemoveAt等等IList的重要介面(具體請參照MSDN中的CollectionBase 成員),只需要使用者顯示實現Add、Remove、IndexOf、Insert等等介面,代碼中僅簡單實現了Add方法和Remove方法的整參數版本作為樣本。這種集合類的實現具有簡單高效的特點,CollectionBase已經實現了較為完善的功能,實施者只要在其基礎上擴充自己所需的功能即可。

索引器的簡單實現
我們慣於運算元組的形式通常為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屬性測試

Customer aCust=new Customer();//將調用不帶參數的建構函式
aCust.Name ="Peter";
custs.Add(aCust);//Add方法測試

System.Console.WriteLine(custs.Count.ToString());
System.Console.WriteLine(custs.Item[0].Name);//調用Item屬性得到
custs.Items(0).Name+="Hu";//調用Items方法得到
System.Console.WriteLine(custs[0].Name);//調用索引器得到

custs.Add(new Customer("Linnet"));//將調用帶name參數的建構函式
System.Console.WriteLine(custs.Count.ToString());
System.Console.WriteLine(custs.Items(1).Name);//調用Items方法得到
custs.Item[1].Name+="Li";//調用Items方法得到
System.Console.WriteLine(custs[1].Name);//調用索引器得到

custs.Remove(0);//Remove方法測試
System.Console.WriteLine(custs.Count.ToString());
System.Console.WriteLine(custs[0].Name);//Remove有效性驗證
custs[0].Name="Test passed" ;//調用索引器得到
System.Console.WriteLine(custs.Item[0].Name);
custs.Clear();
System.Console.WriteLine(custs.Count.ToString());//Clear有效性驗證

}
}
輸出結果為:
0
Initialize instance without parameter
1
Peter
PeterHu
Initialize instance with parameter
2
Linnet
LinnetLi
1
LinnetLi
Test passed
0

2.採用內建ArrayList對象的方式實現集合類:
或許有經驗的程式員們早已經想到,可以在一個類中內建一個數組對象,並在該類中通過封裝對該對象的訪問,一樣能夠實現集合類。以下是採用這種思路的Contact元素類和Contacts集合類的實現架構:

public class Contact
{
protected string summary;

/// <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被真正訪問時才進行,如果屬性一直沒有被讀取過,就不必要無意義地佔用網路、資料庫、記憶體和線程等資源了。但是也並不是初始化越晚越好,因為初始化是需要時間的,在使用前才進行初始化可能導致類的響應速度過慢,無法適應使用者的即時需求。所以在資源佔用和初始化耗時之間尋求一個平衡點,才是執行個體化的最佳時機。

總結
本文圍繞實現集合類的兩種途徑-從CollectionBase繼承實現和內建ArrayList對象實現,為大家展示了部分集合、索引器、屬性、特性的應用以及.NET環境中的類建構函式、對象最佳化、類關聯等其它相關知識。通過本文淺顯的樣本和闡述,希望可以啟發讀者的靈感,推出更加精闢合理的基礎理論和應用程式模型。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.