程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 通過C#實現集合類縱覽.NET Collections及相關技術

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

編輯:關於C語言
概述:在真正的對象化開發項目中,我們通常會將常用的業務實體抽象為特定的類,如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環境中的類構造函數、對象優化、類關聯等其它相關知識。通過本文淺顯的示例和闡述,希望可以啟發讀者的靈感,推出更加精辟合理的基礎理論和應用模型。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved