中间因为比较忙,空了那么多天,都感觉有点罪过了。话不多说,这一篇主要是要讲C#2.0提出的一个新特性,那就是泛型。(现在都C#6.0了。囧囧)
1、什么是泛型?
C#1.0中的委托特性使方法可作为其他方法的参数来传递,而C#2.0中提出的泛型特性则使类型可以被参数化,从而不必再为不同的类型提供特殊版本的方法实现。从字面上来的意思来讲,泛型代表的就是“通用类型”,它可以代替任意的数据类型,使类型参数化,从而达到只实现一个方法就可以操作多种数据类型的目的,将实现行为与方法操作的数据类型分离,实现了代码的重用。下面通过.NET类库中的泛型来说明到底什么是泛型。
clas program
{
static void Main(string[] args)
{
//用int作为实际参数来初始化泛型类型
List<int> intList=new List<int>();
//从int列表添加元素3
intList.Add(3);
//用string作为实际参数来初始化泛型类型
List<string> stringList=new List<string>();
//从string列表添加元素
stringList.Add("learninghard");
}
}
在以上代码中,List<T>是.NET类库中实现的泛型参数,T是泛型参数,可以理解为形参。如果想实例化一个泛型类型,必须传入实际的类型参数,如上面代码中的int和string,就是实际的额类型参数。同时你也可以自己实现泛型类型,下面会介绍泛型类型的实现方法。
2、C# 2.0为什么要引入泛型?
这个就需要用代码来解释了。
public class Compare
{
//比较两个整数大小的方法,方法返回较大的那个整数
public static int compareInt(int i1,int i2)
{
if(i1.CompareTo(i2)>0)
{
return i1;
}
else
{
return i2;
}
}
}
以上的代码实现对于int类型数据来说完全是没有问题的,但是当客户说我想实现两个字符串大小的比较,你就必须再添加一个比较字符串的大小了。如果客户说还想添加一个比较浮点数的大小,那你肯定又只能添加一个方法啦。显然,这种方式是一种比较low的方式,其实方法的代码都非常的相似,所以我们完全可以只定义一个比较方法就能比较所有不同类型的大小。
这时,泛型就派上用场了,就是为这种情况而生,所以,微软就在C#2.0中提出了泛型的特性。
//Compare<T>为泛型类,T为类型参数
public class Compare<T> where T:IComparable
{
//使用泛型实现的比较方法
public static T compareGeneric(T t1,T t2)
{
if(t1.CompareTo(t2)>0)
{
return t1;
}
else
{
return t2;
}
}
}
在以上的代码中,我们实现了一个自定义的泛型类,其中T是泛型的类型参数,compareGeneric是实现的泛型方法,代码中的where语句是类型参数的约束,它用来使类型参数可以适用于CompareTo方法。有了泛型之后,就不需要针对每种数据类型重复实现相似的比较方法了。
class Program
{
static void Main(string[] args)
{
//调用泛型方法
Console.WriteLine(Compare<int>.compareGeneric(3,4));
Console.WriteLine(Compare<string>.compareGeneric("abc","a"));
Console.ReadKey();
}
}
泛型除了可以实现代码重用外,还提供了更好的性能和类型安全特性。
3、全面解析泛型
3.1 类型参数
在前面的泛型代码中,就是类型参数。无论调用类型方法还是初始化泛型实例,都需要用真实类型来代替T,可以把T理解为类型的一个占位符,即告诉编译器,在调用泛型时必须为其指定一个实际类型。
根据泛型类型参数是否提供实际类型,又可把泛型分为两类:未绑定的泛型和已构造的泛型。如果没有为类型参数提供实际类型,此时的泛型称为未绑定的泛型;而如果已指定了实际类型作为参数,则此时的泛型称为已构造的泛型。
已构造泛型又可分为开放类型和密封类型。其中,开放类型是指包含类型参数的泛型,所有未绑定的泛型类型都属于开放类型;而封闭类型则是指那些已经为每一个类型参数都传递了实际数据类型的泛型。
用以下代码可以判断泛型类型是开放还是封闭的。
public class DictionaryStringKey<T>:Dictionary<string,T>{}
class Program
{
static void Main(string[] args)
{
//Dictionary<,>是一个开放类型,它有两个类型参数
Type t=typeof(Dictionary<,>);
Console.WriteLine("是否为开放类型:"+t.ContainsGenericParameters);
//DictionaryStringKey<>也是一个开放类型,但它只有一个类型参数
t=typeof(DictionaryStringKey<>);
Console.WriteLine("是否为开放类型:"+t.ContainGenericParameters);
//DictionaryStringKey<int>
t=typeof(DictionaryStringKey<int>);
Console.WriteLine("是否为开放类型:"+t.ContainGnericParameters);
}
}
3.2 泛型中的静态字段和静态函数问题
每个封闭的泛型类型中都有仅属于它自己的静态数据。
//泛型类型,具有一个类型参数
public static class TypeWithStaticField<T>
{
//静态字段
public static string field;
//静态构造函数
public static void OutField()
{
Console.WirteLine(field+":"+typeof(T).Name);
}
}
//非泛型类
public static class NoGenericTypeWithStaticField
{
public static string field;
public static void OutField()
{
Console.WriteLine(field);
}
}
class Program
{
static void Main(string[] args)
{
//使用不同类型实参来实例化泛型实例
TypeWithStaticField<int>.field="一";
TypeWithStaticField<string>.field="一";
TypeWithStaticField<Guid>.field="一";
//对于非泛型类型,此时field只会一个值,每次赋值都改变了原来的值
NoGenericTypeWithStaticField.field="非泛型类静态字段一";
NoGenericTypeWithStaticField.field="非泛型类静态字段二";
NoGenericTypeWithStaticField.field="非泛型类静态字段三";
NoGenericTypeWithStaticField.OutField();
TypeWithStaticField<int>.OutField();
TypeWithStaticField<string>.OutField();
TypeWithStaticField<Guid>.OutField();
Console.ReadKey();
}
}
结果如下:
从图中结果可以看出,每个封闭的泛型类型都有属于它的静态字段。这是因为,在使用实际类型参数代替泛型参数时,编译器会根据不同的类型实参,重新生成类型。
对于编译器来说,每个封闭泛型类型都是一个不一样的类型,所以它们都有属于它自己的静态字段。静态构造函数也一样,每个封闭的泛型类型都有一个静态构造函数。
3.3 类型参数的推断
class Program
{
static void Main(string[] args)
{
int n1=1;
int n2=2;
genericMethod(ref n1,ref n2);
Console.WriteLine("n1的值现在为:"+n1);
Console.WriteLine("n2的值现在为:"+n2);
}
//泛型方法
private static void genericMethod<T>(ref T t1,ref T t2)
{
T temp=t1;
t1=t2;
t2=temp;
}
}
在以上的代码中,编译器会根据传递的方法实参来判断传入的实际类型参数。如果编译器根据传入的参数不能推断出实际参数类型,就会出现编译时错误。
注意:类型推断只能用于泛型方法,它对泛型类则并不适用,因为编译器不能通过泛型类的构造函数推断出实际的类型参数。
3.4 类型参数的约束
不知大家是否注意过,前面的代码中曾经出现过where T:IComparable的代码,这个where语句用来使类型参数继承于IComparable接口,从而对类型参数进行约束。
前面的比大小中,如果没有进行约束,代码编译的时候就会出现错误,编译通不过,所以,对类型参数进行约束是必须的。
C#中有4种约束可以使用,它们的语法类似:约束要放在泛型方法或类型声明的末尾,并且要使用where关键字。
(1)引用类型约束
表现形式为T:class,它确保传递的类型实参必须是引用类型。
注意,约束的类型参数和类型本身没有关系,即在定义一个泛型结构体时,泛型类型一样可以被约束为引用类型。其中有几个特殊的引用类型是不能用的:System.Array、System.Delegate、System.MulticastDelegate、System.ValueType、System.Enum、System.Void。
下面代码定义的泛型类:
public class samplehere<T> where T:Stream
{
public void Test(T stream)
{
strem.Close();
}
}
从以上的代码中,类型参数T设置了引用类型约束。where T :Stream的意思是告诉编译器:传入的类型实参必须是System.IO.System,或者是从Stream派生的一个类型。
如果一个类型参数没有指定约束,则默认T为System.Object类型。但若你在代码中显式地指定了System.Object约束,则编译器会报错。
(2)值类型约束
值类型约束的表示形式为T:Struct,它确保传递的类型实参是值类型,包括枚举。但这里的值类型不包括可空类型。
public class samplevaluetype<T>:struct
{
public static T Test()
{
return new T();
}
}
在上面的代码中,new T()是可以通过编译的,因为T是一个值类型,而所有值类型都有一个公共的无参构造函数。但如果不对T进行约束,或约束为引用类型,上面的代码就会报错。
(3)构造函数类型约束
构造函数类型约束的表示形似为T:new(),如果类型参数有多个约束,则此约束必须最后指定。构造函数类型约束确保指定的类型参数有一个公共无参构造函数的非抽象类型。
这里需要注意一点:如果同时指定构造器约束和struct约束,C#编译器会认为这是一个错误。因为这样的指定是多余的,所有值类型都隐式地提供了一个无参公共构造函数。
(4)转换类型约束
转换类型约束的表示形式为T:基类名、T:接口名或T:U。T:基类名确保指定的类型实参必须是基类或派生自基类的子类;T:接口名确保指定的类型实参必须是接口或实现了该接口的类;而T:U则确保T提供的类型实参必须是U提供的类型实参或派生于U提供的类型实参,即前面一个类型实参必须是后面的类型实参或后面类型的实参子类。
(5)组合约束
组合约束是将多个不同种类的约束合并在一起的情况。这里就需要注意,没有任何一种类型既是引用类型,又是值类型,所以引用约束和值约束不能同时使用。如果存在多个转换类型约束,其其中一个是类,则必须放在接口的前面。不同类型参数可以有不同的约束,但每种类型参数必须分别使用一个单独的where关键字。
class Sample<T> where T:class,Disposable,new();
class Sample<T,U> where T:class where U:struct
总结:至此,泛型已经讲完了,应该有一个全面的认识了。不过还是要多敲代码,不断实践,才能提高。