C#之泛型詳解
泛型不僅是C#的一部分,而且與程序集中的IL代碼緊密地集成。有了泛型,就可以創(chuàng)建獨立于被包含類型的類和方法。這樣就可以不必給不同的類型編寫功能相同的許多方法或類,只創(chuàng)建一個方法或類即可。
另一個減少代碼的選項是使用Object類,因為Object類是不安全的。
泛型類使用泛型類型,并可以根據(jù)需要用特定的類型替換泛型類型。這就保證了類型安全性:如果某個類型不支持泛型類,編譯器就會出現(xiàn)錯誤。
泛型不僅限于類,接口和方法也可以使用泛型。
泛型并不是一個全新的結(jié)構(gòu),在C++中模版就和泛型相似。泛型不僅是C#的一種結(jié)構(gòu),而且是CLR定義的。所以,即使泛型類在C#中定義,也可以VB中用一個特定的類型實例化該泛型。
一.泛型的特性
1.性能
對值類型使用非泛型集合類,在把值類型轉(zhuǎn)換為引用類型,和把引用類型轉(zhuǎn)換為值類型時,需要進行裝箱和拆箱操作。雖然操作很容易,但性能損失比較大:
var list = new ArrayList();
list.Add(4);//裝箱操作,ArrayList的Add方法參數(shù)是Object
foreach(int i in list)
{
Console.WriteLine(i);//拆箱
}System.Collections和System.Collections.Generic是非泛型和泛型集合類。System.Collections.Generic中的List<T>類不使用對象,而是在使用時定義類型。
var list = new List<int>();
list.Add(4);
foreach(int i in list)
{
Console.WriteLine(i);
}2.類型安全
使用ArrayList類在集合中可以添加任意類型。
var list = new ArrayList(); list.Add(4); list.Add(“66”);
在遍歷時就會出現(xiàn)異常,因為不是所有元素都可以強制裝換為INT類型:
foreach(int i in list)
{
Console.WriteLine(i);
}錯誤應(yīng)盡早發(fā)現(xiàn)。使用泛型類List<T>,泛型類型T定義了允許使用特定的類型。有了List<int>的定義,就只能把整數(shù)類型添加到集合中。
var list = new ArrayList(); list.Add(4); list.Add(66);
3.二進制代碼的重用
泛型允許更好的重用二進制代碼。泛型類可以定義一次,并且可以用許多不同的類型實例化。例如List<T>類可以用int,string,自定義類來實例化。
泛型類可以在一種語言中定義,在任何其它.NET語言中使用。
4.代碼的擴展
因為泛型類的定義放在程序集中,所以用特定類型實例化泛型類不會在IL代碼中復制這些類。但是,在JIT編譯器把泛型類編譯為本地代碼時,會給每個值類型創(chuàng)建一個新類。引用類型共享一個本地類的所有相同的實現(xiàn)代碼。這是因為引用類型在實例化的泛型類中只需要4個字節(jié)的內(nèi)存地址(32為系統(tǒng)),就可以引用一個引用類型。值類型包含在實例化的泛型類的內(nèi)存中,同時因為每個值類型對內(nèi)存的要求不同,所以要為每個值類型實例化一個新類。
5.命名約定
在程序中使用泛型,在區(qū)分泛型類型和非泛型類型時使用命名約定就會有一定幫助。
泛型類型的命名規(guī)則:
- *泛型類型的名稱用字母T作為前綴。
- *如果沒有特殊的要求,泛型類型允許用任意類代替,且如果只能使用一個泛型類型,就可以用T作為泛型類型的名稱。
Public class Person{} - *如果泛型類型有特定的要求(比如它必須實現(xiàn)某個接口或派生自基類),或者使用了兩個或多個泛型類型,就應(yīng)給泛型類型使用描述性的名稱:
public class SortedList{}
二.使用類型
1.先創(chuàng)建一個非泛型的簡化鏈表類。
public class LinkedListNode
{
public LinkedListNode(object value)
{
this.Value = value;
}
public object Value { get; private set; }//value對外密封,只可以讀取,設(shè)置值通過構(gòu)造函數(shù)
public LinkedListNode Next { get; internal set; }//記錄當前節(jié)點的下一節(jié)點
public LinkedListNode Prev { get; internal set; }//記錄當前節(jié)點的上一節(jié)點
}
public class LinkedList : IEnumerable
{
public LinkedListNode First { get; private set; }//記錄鏈表的第一個節(jié)點,外部只可以讀取,設(shè)置值通過內(nèi)部方法
public LinkedListNode Last { get; private set; }
public LinkedListNode AddLast(object node)
{
//實例化一個LinkedListNode
var newNode = new LinkedListNode(node);
//判斷鏈表是否含有元素,如果沒有就把newNode設(shè)置為鏈表的第一個元素
if (First == null)
{
First = newNode;
Last = newNode;
}
else
{
LinkedListNode previous = Last;//獲取鏈表的最后一個元素
Last.Next = newNode;//將newNode賦予最后一個元素的下一個元素
Last = newNode;//重新設(shè)置鏈表的最后一個元素
Last.Prev = previous;//newNode成為鏈表的最后一個元素,將原來的最后一個元素設(shè)置為newNode的上一個元素
//注意上述順序
}
return newNode;
}
//實現(xiàn)IEnumerable接口的GetEnumerator()方法,這樣外部代碼就可以用foreach語句遍歷鏈表
public IEnumerator GetEnumerator()
{
LinkedListNode current = First;
while (current != null)
{
yield return current.Value;//yield創(chuàng)建一個枚舉器的狀態(tài)機
current = current.Next;
}
}
}客戶端代碼:
var list1 = new LinkedList();
list1.AddLast(2);
list1.AddLast(4);
list1.AddLast("22");
foreach (object i in list1)
{
Console.WriteLine(i.ToString());
}需要進行裝箱拆箱操作。
2.下面編寫一個泛型版本
public class LinkedListNode<T>
{
public LinkedListNode(T value)
{
this.Value = value;
}
public T Value { get; private set; }//value對外密封,只可以讀取,設(shè)置值通過構(gòu)造函數(shù)
public LinkedListNode<T> Next { get; internal set; }//記錄當前節(jié)點的下一節(jié)點
public LinkedListNode<T> Prev { get; internal set; }//記錄當前節(jié)點的上一節(jié)點
}
//該類實現(xiàn)IEnumerable<T>接口,IEnumerable<T>繼承自IEnumerable接口,所以需要實現(xiàn)GetEnumerator()方法和IEnumerator<T> GetEnumerator()方法
public class LinkedList<T> : IEnumerable<T>
{
public LinkedListNode<T> First { get; private set; }//記錄鏈表的第一個節(jié)點,外部只可以讀取,設(shè)置值通過內(nèi)部方法
public LinkedListNode<T> Last { get; private set; }
public LinkedListNode<T> AddLast(T node)
{
//實例化一個LinkedListNode
var newNode = new LinkedListNode<T>(node);
//判斷鏈表是否含有元素,如果沒有就把newNode設(shè)置為鏈表的第一個元素
if (First == null)
{
First = newNode;
Last = newNode;
}
else
{
LinkedListNode<T> previous = Last;//獲取鏈表的最后一個元素
Last.Next = newNode;//將newNode賦予最后一個元素的下一個元素
Last = newNode;//重新設(shè)置鏈表的最后一個元素
Last.Prev = previous;//newNode成為鏈表的最后一個元素,將原來的最后一個元素設(shè)置為newNode的上一個元素
//注意上述順序
}
return newNode;
}
//IEnumerable<T>接口繼承IEnumerable
//實現(xiàn)IEnumerable<T>接口的GetEnumerator()方法,這樣外部代碼就可以用foreach語句遍歷鏈表
public IEnumerator<T> GetEnumerator()
{
LinkedListNode<T> current = First;
while (current != null)
{
yield return current.Value;//yield創(chuàng)建一個枚舉器的狀態(tài)機
current = current.Next;
}
}
//實現(xiàn)IEnumerable接口的GetEnumerator()方法
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}客戶端代碼:
var list2 = new LinkedList<int>();
list2.AddLast(2);
list2.AddLast(4);
list2.AddLast(44);
foreach (int i in list2)
{
Console.WriteLine(i);
}三.泛型類的功能
在創(chuàng)建泛型類時,有時需要一些C#關(guān)鍵字。
通過一個例子來介紹這些關(guān)鍵字:
public class DocumentManager<TDocument>
where TDocument : IDocument
{
private readonly Queue<TDocument> documentQueue = new Queue<TDocument>();
public void AddDocument(TDocument doc)
{
lock (this)
{
documentQueue.Enqueue(doc);
}
}
public bool IsDocumentAvailable
{
get { return documentQueue.Count > 0; }
}
public void DisplayAllDocuments()
{
foreach (TDocument doc in documentQueue)
{
Console.WriteLine(doc.Title);
}
}
public TDocument GetDocument()
{
TDocument doc = default(TDocument);
lock (this)
{
doc = documentQueue.Dequeue();
}
return doc;
}
}1.默認值
在GetDocument()方法中,需要把TDocument指定為null。但是,不能把null賦予泛型類型。因為泛型類型也可以實例化為值類型,而null只能用于引用類型。為了解決這個問題,可以使用default關(guān)鍵字。通過default,將null賦予引用類型,將0賦予值類型。
2.約束
如果泛型類需要調(diào)用泛型類型中的方法,就必須添加約束。
public interface IDocument
{
string Title { get; set; }
string Content { get; set; }
}
public class Document : IDocument
{
public Document()
{
}
public Document(string title, string content)
{
this.Title = title;
this.Content = content;
}
public string Title { get; set; }
public string Content { get; set; }
}在方法中遍歷顯示時,需要使用Document類的屬性Title,這就需要約束泛型類型TDocument中包含這個屬性,這里使用泛型類型TDocument必須實現(xiàn)IDocument接口。where子句指定了該約束。
泛型支持的約束類型:
| 約束 | 說明 |
|---|---|
| where T:struct | 對于結(jié)構(gòu)約束,類型T必須是值類型 |
| where T:class | 類約束指定類型T必須是引用類型 |
| where T:IFoo | 指定類型T必須實現(xiàn)接口IFoo |
| where T:Foo | 指定類型T必須派生自Foo |
| where T:new() | 構(gòu)造函數(shù)約束,指定類型T必須有一個默認構(gòu)造函數(shù) |
| where T1:T2 | 這個約束指定類型T1派生自泛型類型T2。該約束也稱為裸類型約束。 |
使用泛型類型也可以合并多個約束。where T:IFoo,new(),MyClass
3.繼承
泛型類型可以實現(xiàn)泛型接口,也可以派生自一個類。泛型類可以派生自泛型基類。要求是必須重復接口的泛型類型,或者必須指定基類的類型。
派生類可以是泛型類或非泛型類。
4.靜態(tài)成員
泛型類的靜態(tài)成員和非泛型類的靜態(tài)成員有區(qū)別,泛型類的靜態(tài)成員只能在類的一個實例中共享:
public class StaticDemo<T>
{
public static int x;
}客戶端代碼:
StaticDemo<string>.x = 4; StaticDemo<int>.x = 5; //存在兩組靜態(tài)字段。
四.泛型接口
使用泛型可以定義接口,在接口中定義的方法可以帶泛型參數(shù)。
五.泛型結(jié)構(gòu)
泛型結(jié)構(gòu)類似于泛型類,只是沒有繼承特性。
六.泛型方法
在泛型方法中,泛型類型用方法來定義。泛型方法可以在非泛型類中定義。
void Swap<T>(ref T x,ref T y)
{
T temp;
temp = x;
x=y;
y=temp;
}
int i=4;
int j = 5;
Swap<int>(ref i,ref j);因為C#編譯器會通過調(diào)用Swap()方法來獲取參數(shù)的類型,所以不需要把泛型類型賦予方法調(diào)用:
int i=4; int j = 5; Swap(ref i,ref j);
1.帶約束的泛型方法
public static class Algorithm
{
public static decimal Accumulate<TAccount>(IEnumerable<TAccount> source)
where TAccount : IAccount
{
decimal sum = 0;
foreach (TAccount a in source)
{
sum += a.Balance;
}
return sum;
}
}
public interface IAccount
{
decimal Balance { get; }
string Name { get; }
}
public class Account : IAccount
{
public string Name { get; private set; }
public decimal Balance { get; private set; }
public Account(string name, Decimal balance)
{
this.Name = name;
this.Balance = balance;
}
}
客戶端代碼:
var accounts = new List<Account>()
{
new Account("Christian", 1500),
new Account("Stephanie", 2200),
new Account("Angela", 1800),
new Account("Matthias", 2400)
};
decimal amount = Algorithm.Accumulate<Account>(accounts);2.帶委托的泛型方法
public static class Algorithm
{
public static T2 Accumulate<T1, T2>(IEnumerable<T1> source, Func<T1, T2, T2> action)
{
T2 sum = default(T2);
foreach (T1 item in source)
{
sum = action(item, sum);
}
return sum;
}
}
decimal amount = Algorithm.Accumulate<Account, decimal>(accounts, (item, sum) => sum += item.Balance);3.泛型方法規(guī)范
泛型方法可以重載,為特定的類型定義規(guī)范。這樣適用于帶參數(shù)的方法。
public class MethodOverloads
{
public void Foo<T>(T obj)
{
Console.WriteLine("Foo<T>(T obj), obj type: {0}", obj.GetType().Name);
}
public void Foo(int x)
{
Console.WriteLine("Foo(int x)");
}
public void Bar<T>(T obj)
{
Foo(obj);
}
}如果傳遞了一個int,就選擇帶int參數(shù)的方法。對于其它參數(shù)類型,編譯器會選擇泛型方法。
var test = new MethodOverloads();
test.Foo(33);
test.Foo("abc");輸出:
Foo(int x) Foo<T>(T obj), obj type: String
所調(diào)用的方法是在編譯期間定義的,而不是運行期間。
test.Bar(44);
輸出:
Foo<T>(T obj), obj type: Int32
Bar()方法選擇了泛型Foo()方法,而不是int參數(shù)重載的方法。因為編譯器是在編譯期間選擇Bar()方法調(diào)用的Foo()方法。由于Bar()方法定義了一個泛型參數(shù),而且泛型Foo()方法匹配這個類型,所以調(diào)用Foo()方法。
到此這篇關(guān)于C#之泛型的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C# 文件操作函數(shù) 創(chuàng)建文件 判斷存在
本文列舉了C#中文件操作中常用的函數(shù),創(chuàng)建文件和判斷文件存不存在的基本使用,簡單實用,希望能幫到大家。2016-05-05
C#基礎(chǔ):基于const與readonly的深入研究
本篇文章是對c#中const與readonly進行了詳細的分析介紹,需要的朋友參考下2013-05-05

