C#泛型基础

1.基本概念

.NET2.0新增的最大的特性是泛型。

我们先来看下定义在System.Collections.Generic下的List<T>:

public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable

List类后边的紧跟着一个<T>,T被称为类型参数(type parameter),是一个真实实参的占位符,表明该类未定义实际的数据类型。
实际使用的时候,需要指定T的具体类型,如:List<string>,此处的string被称为类型实参(type argument),List<T>下所有的T都会被替换成string类型。

2.泛型的优点:实现了编译时的类型安全和算法重用

在泛型出现以前,只能通过传递object类型的参数或者返回object类型来实现通用性的方法。
比如IComparable接口的CompareTo方法签名如下:

int CompareTo(object obj)

由于System.Object是所有对象的基类,存在任意类型到object的隐式转换,所以可以向CompareTo方法传递任意类型的参数。
以下是int的IComparable接口实现:

public int CompareTo(object value)
{
    if (value == null)
    {
        return 1;
    }
    if (!(value is int))
    {
        throw new ArgumentException(Environment.GetResourceString("Arg_MustBeInt32"));
    }
    int num = (int)value;
    if (this < num)
    {
        return -1;
    }
    if (this > num)
    {
        return 1;
    }
    return 0;
}


通过分析代码存在以下的问题:
1.编译时可以传递任意参数,但是实际上只有传递一个int类型的参数才有实际意义。假如传递一个非int类型的参数,会抛出异常。
2.当传递一个int类型的实参时,需要进行装箱和拆箱操作,会影响性能。

再看int对泛型接口IComparable<T>的实现:

public int CompareTo(int value)
{
    if (this < value)
    {
        return -1;
    }
    if (this > value)
    {
        return 1;
    }
    return 0;
}

只能向方法传递一个int类型,实现了编译时的安全同时避免了拆箱和装箱的操作。
而其他类型通过实现各自的IComparable<T>接口,同样实现了算法的重用。

再来举一个例子。

场景:向列表添加若干数字,计算数字的和。
先来看使用ArrayList的实现:

ArrayList list = new ArrayList();
list.Add(1);
list.Add(2);
list.Add(3.0);
int count = 0;
for (int i = 0; i < list.Count; i++)
{
    count += (int)list[i];
}

这段代码编译的时候能通过,运行时会报InvalidCastException异常,类型转换失败。

ArrayList的Add方法的签名为:Add(object value),所以每次使用Add方法向列表添加int对象时,先要进行装箱操作。
此处故意使用了for循环而不是foreach循环,就是为了清晰的体现其中的拆箱操作以及类型转换。

再来看使用List<T>的实现:

List<int> list = new List<int>();
list.Add(1);
list.Add(2);
//list.Add(3.0);
int count = 0;
for (int i = 0; i < list.Count; i++)
{
    count += list[i];
}

注释掉的那行在编译时报错,方法参数不匹配。

我们使用int来替换T,表示list中的所有T都替换为int类型。
List<T>的Add方法签名为:void Add(T item),鼠标移动到Add方法上,可以明显的看到方法的参数属性类型以及被替换为int,同理list[i]也是int类型。

3.泛型约束

假如你编译以下代码:

T Foo<T>()
{
    return new T();
}

编译的时候会提示错误:“变量类型“T”没有 new() 约束,因此无法创建该类型的实例 ”。
将代码修改如下可以通过编译:

T Foo<T>() where T : new()
{
    return new T();
}

此处通过where对泛型添加了约束。以下为可以添加的约束类型(MSDN):

约束

说明

T:struct

类型参数必须是值类型。Nullable can be specified.' data-guid="6eb132c88993822c7977723af20162ad">Nullable can be specified.'>Nullable can be specified.' data-guid="6eb132c88993822c7977723af20162ad">可以指定除 Nullable <T>以外的任何值类型。

T:class

类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。

T:new()

类型参数必须具有无参数的公共构造函数。new() constraint must be specified last.' data-guid="012d33600c2a5348f794e49b607a570e">new() constraint must be specified last.'>new() constraint must be specified last.' data-guid="012d33600c2a5348f794e49b607a570e">当与其他约束一起使用时,new() 约束必须最后指定。

T:<基类>

类型参数必须是指定的基类或派生自指定的基类(非密封类)。不能指定以下的类型:Object,Array,Delegate,MulticastDelegate,ValueType,Enum和Void

T:<接口>

类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。

T:U

为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。


假如没有泛型约束功能,只能对泛型实例赋值或者使用object定义的方法。
通过使用泛型约束限制T的类型范围,才能在T的类型或者实例上调用相应的方法。

示例:

T Foo<T>(T t1,T t2) where T :IComparable<T>
{
    if (t1.CompareTo(t2) > 0)
    {
        return t1;
    }
    return t2;
}
很显然的只有对象实现了IComparable<T>接口,才能在对象的实例上调用CompareTo方法。

4.泛型方法

泛型类中的方法定义了自己的类型参数时,才能作为一个泛型方法。
如下代码:只有方法二才是一个泛型方法,方法一不过是使用了泛型类的类型参数作为形参或者返回结果而已。

public class Test<T>
{
    public T GetInster()
    {
        return default(T);
    }

    public TValue GetFirstItem<TValue>(TValue[] list)
    {
        if (list == null)
        {
            throw new ArgumentNullException();
        }
        if (!list.Any())
        {
            throw new ArgumentException();
        }
        return list[0];
    }
}

编译器在使用泛型方法时,能够通过变量的数据类型自动推断要使用的类型,使用上面的GetFirstItem作为示例:

int[] list = new int[] { 1, 2, 3 };
GetFirstItem<int>(list);
GetFirstItem(list);

第三行的方法根据参数的类型,自动的推导出类型参数为int。

5.泛型的协变和逆变

泛型委托和泛型接口的类型参数可以标记为协变性量或者逆变量

逆变量:泛型类型参数可以从一个基类变为该类的派生类,使用in关键字标记类型参数,逆变量只能出现在输入位置:方法的参数或者set访问器中的方法。

协变量:泛型类型参数可以从一个派生类变为该类的基类,使用out关键字标记类型参数,协变量只能出现在输出位置:方法的返回值。

对以下的泛型委托类型定义:

delegate TResout Func<in T, out TResout>(T t);

T被申明为逆变量,TResout被申明为协变量。

MyFunc<object, string> fn1 = s => s.ToString();
MyFunc<string, object> f2 = fn1;

所以在需要一个object对象参数时,可以传递一个string类型对象。
返回一个string类型的,可以被视为object类型

6.其他

6.1泛型反射

查看

typeof(List<int>).ToString()

可以发现泛型的类型名为“System.Collections.Generic.List`1[System.Int32]”。
前面是List类的类型名,紧跟着一个`和一个数字,数字表示类型的参数个数,再接着参数的类型。

6.2泛型类的构造函数

针对不同的类型参数,泛型类只在第一次调用的时候实时编译,所以第一次调用一个静态类会造成性能损失,以后再调用这个参数就能直接获取了。同时所有的引用类型实参都只会编译一次,因为所有的引用类型都是指向托管堆的指针。假如在某个泛型类中定义了静态字段或者静态构造器,针对不同类型的泛型实现,都拥有各种的静态字段,

6.3泛型实例的默认值

可以通过default(T)来设置泛型实例的默认值,类似逻辑为T是typeof(T).IsValueType ? 0 : null(此处的0是指将值类型的所有位设置为0)。

posted @ 2012-07-15 14:22  YLWS  阅读(960)  评论(0编辑  收藏  举报