泛型解决方案
C# 通过泛型避免强制类型转换,增强类型安全性,减少装箱量.泛型类和方法接受类型参数.它们指定了要操作的对象的类型
如下所示:
class Queue<T>
{
...
}
T就是类型参数,作为占位符使用,会在编译时被真正的类型取代.写代码实例化泛型Queue时,需要指定用于取代T的类型,在类中定义字段和方法时,也可以用同样的占位符指定这些项的类型.
类型参数不一定是简单类或值类型。比如,可以创建由整数队列构成的队列:
泛型类还可以指定多个类型的参数,比如Dictionary
…
…
对比泛型类和常规类
每次为泛型类指定类型参数时,实际都会造成编译器生成一个全新的类.它"恰好"具有泛型类定义的功能.可以想象泛型类定义了一个模板,编译器根据实际情况用该模板生成新的、有具体类型的类.
…
…
泛型和约束
有时候要确保泛型使用的类型参数是提供了特定方法的类型,这时可用约束来规定该条件.
比如:
class Queue<T> where T: IPrintable
{
}
这样用于替换T的类型必须实现了IPrintable接口,如果没有则会造成编译错误.
下面是一个使用泛型的简单二叉树类
public class Tree<TItem> where TItem: IComparable<TItem>
{
public TItem NodeData { get; set; }
public Tree<TItem> LeftTree { get; set; }
public Tree<TItem> RightTree { get; set; }
public Tree(TItem nodeValue) //构造器名称不能包含类型参数,它名为Tree
{
this.NodeData = nodeValue;
this.LeftTree = null;
this.RightTree = null;
}
public void Insert(TItem newItem)
{
TItem currentNodeValue = this.NodeData;
if(currentNodeValue.CompareTo(newItem) > 0) //比较当前节点的值和新项的值
{
if(this.LeftTree == null)
{
this.LeftTree = new Tree<TItem>(newItem);
}
else
{
this.LeftTree.Insert(newItem);
}
}
else //右子树
{
if (this.RightTree == null)
{
this.RightTree = new Tree<TItem>(newItem);
}
else
{
this.RightTree.Insert(newItem);
}
}
}
public string WalkTree() //树的遍历
{
string result = "";
if(this.LeftTree != null)
{
result = this.LeftTree.WalkTree();
}
result += $"{this.NodeData.ToString()}";
if(this.RightTree != null)
{
result += this.RightTree.WalkTree();
}
return result; }
}
使用上面的二叉树:
…
…
创建泛型方法
除了定义泛型类,还可以创建泛型方法 比如:
static void Swap<T>(ref T first, ref T second)
{
T temp = first;
first = second;
second = temp;
}
使用泛型方法:
我们可以使用参数数组结合泛型创建一个可以为刚才的二叉树添加大量数据的方法:
static void InsertIntoTree<TItem>(ref Tree<TItem> tree, params TItem[] data) where TItem:IComparable<TItem>
{
foreach(TItem datum in data)
{
if(tree == null)
{
tree = new Tree<TItem>(datum);
}
else
{
tree.Insert(datum);
}
}
}
…
…
…
可变性和泛型接口
我们用上面的类创建了一个实例,并用它包装一个字符串:
Wrapper<string> wrapper = new Wrapper<string>();
IWrapper<object> Iwrapper = wrapper;
其中的:
IWrapper<object> Iwrapper = wrapper;
是合法的吗?
显然是不合法的:
这个问题在于,虽然所有字符串都是对象,但并不是所有对象都是字符串。
其中IWrapper<T>
接口称为不变量,不能将IWrapper<A>
赋给`IWrapper类型的引用。
…
…
协变接口
interface IStoreWrapper<T>
{
void SetData(T data);
}
interface IRetrieveWrapper<T>
{
T GetData();
}
class Wrapper<T> : IStoreWrapper<T>, IRetrieveWrapper<T>
{
private T storeData;
T IRetrieveWrapper<T>.GetData()
{
return this.storeData;
}
void IStoreWrapper<T>.SetData(T data)
{
this.storeData = data;
}
}
Wrapper<string> wrapper = new Wrapper<string>();
IStoreWrapper<string> storedStringWrapper = wrapper;
storedStringWrapper.SetData("Hello");
IRetrieveWrapper<string> retrieveWrapper = wrapper;
IRetrieveWrapper<object> retrieveWrapper2 = wrapper;
上面这个代码合法吗?
可以看到是不合法的。但是对于泛型接口定义的方法,如果参数T仅在方法返回值中出现,就可以告诉编译器一些隐式转换是合法的,没必要在强制严格的类型安全性.
为此,可以在声明类型参数时指定out关键字
interface IRetrieveWrapper<out T>
{
T GetData();
}
这个功能称为协变性。只要存在从类型A到类型B的有效转换,或者类型A派生自类型B,就可以将IRetrieveWrapper<A>
对象赋给IRetrieveWrapper<B>
引用
可以看到现在就是合法的了.
只有作为方法返回类型指定的类型参数才能使用out限定符.如果类型参数作为方法的参数类型,添加out限定符则无法编译.另外,协变性只适合引用类型,因为值类型不能建立继承关系.
…
…
…
逆变接口
有协变性还有逆变性,它允许使用泛型接口,通过A类型的一个引用来引用B类型的一个对象,只要A从B派生或者说B的派生程度比A小.以下面为例子
实现该接口的类必须定义Compare方法,它比较由T类型参数指定的那种类型的两个对象。
class ObjectComparer : IComparer<object>
{
int IComparer<object>.Compare(object x, object y)
{
int xHash = x.GetHashCode();
int yHash = y.GetHashCode();
if (xHash == yHash) return 0;
if (xHash < yHash) return -1;
return 1;
}
}
表明上看,该语句似乎违反了类型安全性的一切规则.但是想想Icomparer所做的事情,就明白上面的语句是没有问题的
能比较object,自然就能比较String。但是,你可能会说编译器这么知道你不会在Compare方法的代码中执行依赖于特定类型的操作?造成用基于不同类型的接口调用方法时失败?
所有,必须让编译器检测,在Icomparer的定义中,在类型参数前添加了in限定符:
in关键字明确告诉C#编译器:程序员要么传递T作为方法的参数类型,要么传递T的派生类型,程序员不能将T用作任何方法的返回类型。
这就限定了通过泛型接口引用对象时,接口要么基于T,要么基于T的派生类型。