C#与CLR学习笔记(7)—— 泛型与泛型约束

19 篇文章 3 订阅

1 泛型概述

1.1 含义

使用泛型的主要目的有三个:(1)实现代码复用;(2)避免使用Object类,在实例化一个泛型类时,我们需要指定T的实际类型(类型实参),这样保证了类型安全;(3)减少了 Object 造成的装箱拆箱,提高性能(原理见下文)。
对于编译器而言,泛型 T 本质上就是一个 类型参数(Type parameter),所谓参数其实就是一个特殊的占位符。泛型被定义在程序集中,因此当代码被编译成 IL 放到程序集中时,T 仍然存在,例如以下案例:

Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));
//以下是输出
System.Collections.Generic.List`1[T]
System.Collections.Generic.Dictionary`2[TKey,TValue]

其中,后单引号(`)后面的数字代表类型参数的个数。
在代码运行时,即在 JIT 阶段,IL 被翻译成 本机语言时,泛型类型参数 T 会被替换成具体的类型。例如,如果你使用了 List<int> 类,那么代码中的 T 是在 JIT 翻译之后才变成了 int 类型。因此,不存在类型转换、装箱拆箱,这就是性能更好的原因。

需要注意,未指定类型实参的泛型类型是不能实例化的,因为它是一种 开放类型,这与你不能实例化一个接口一样。传递一个 类型实参 (Type argument)后,变成 封闭类型,才可以实例化。案例:

Object o;
o = Activator.CreateInstance(typeof(List<int>));//OK
o = Activator.CreateInstance(typeof(List<>));//抛出ArgumentException,因为含有泛型参数

泛型最常用的地方是集合类。微软建议使用泛型集合,不建议使用非泛型集合,除了上文中提到的类型安全、性能高之外,泛型集合类中的虚方法更少,从而进一步提高执行性能;另外,泛型集合一般拥有更多的扩展方法,使用更方便。

1.2 泛型的继承

1.2.1 泛型类型的继承

泛型类可以派生自一个泛型基类,但是,泛型子类必须重复泛型基类的泛型类型,或者必须指定基类的泛型类型。
例如,考察以下代码:

public class ChildGeneric : Generic<T>{} //编译失败
public class ChildGeneric<T> : Generic<T>{} //编译通过
public class ChildGeneric : Generic<int>{} //编译通过

1.2.2 泛型的类型参数的继承

考察两个泛型类型 List<MyBaseClass>List<MyChildClass>,其中 MyChildClass 派生自 MyBaseClass,那么 List<MyBaseClass>List<MyChildClass> 之间有什么关系吗?答案是没有关系。

因为 类型参数 的继承关系 不改变 泛型类型的 继承关系,或者说,泛型类型 破坏了 泛型类型参数的继承关系

更具体一点,指定类型实参并不影响层次结构。List<T> 派生自 Object,那么 List<MyBaseClass>List<MyChildClass> 都是从 Object 派生,二者是 “平辈” 的。指定类型实参只是在 JIT 时拿指定的类型替换 T,这两个 List 是两个不同的类。

由此引出另一个话题,即 逆变协变,详见我的另一篇文章《C#与CLR学习笔记(6)—— 轻松理解协变与逆变

2 泛型约束

2.1 编译器对泛型参数的验证

由于泛型参数 T 在定义时并未指定,因此,为了安全性,编译器会在编译时进行分析,确保代码适用于未来可能指定的任何泛型类型实参。
例如,考察如下代码:

private static T Min<T>(T o1, T o2)
{
    if (o1.CompareTo(o2) < 0)
        return o1;
    return o2;
}

上述代码编译失败,因为并非所有类型都实现了 IComparable 接口,因此 CompareTo()方法有无法执行的风险。

所以从表面上看,使用泛型似乎做不了太多事情,只能使用 Object 中定义的方法。显然,实际情况并不是这样,因为有 泛型约束 机制,它使得泛型变得有用。

上面的案例改进一下,就可以顺利编译:

private static T Min<T>(T o1, T o2) where T : IComparable<T>
{
    if (o1.CompareTo(o2) < 0)
        return o1;
    return o2;
}

约束 可应用于 泛型类型泛型方法 。如果 基类 或者 被重写/实现的方法 拥有泛型约束,那么子类或其方法必须应用同样的泛型约束。
例如:

public class Generic<T> where T : struct
{}
public class ChildGeneric<T> : Generic<T> //编译失败
{}
public class ChildGeneric<T> : Generic<T> where T : struct //编译通过
{}


public interface IGeneric2
{
     string GetTInfo<T>() where T : struct;
}
class ChildGeneric : IGeneric2
{
     public string GetTInfo<T>() where T : class //编译失败
     {
          return "";
     }
}
class ChildGeneric : IGeneric2
{
     public string GetTInfo<T>() where T : struct//编译通过
     {
          return "";
     }
}

2.2 泛型约束的类型

分类形式含义
主要约束
(最多指定一个)
where T : structT 必须是值类型
Nullable<T>除外,详见参考文献)
where T : classT 必须是引用类型
where T : FooT 必须派生自 Foo 类
次要约束
(可以指定多个)
where T : IFooT 必须实现 IFoo 接口
where T1 : T2T1 派生自 泛型类型 T2
构造器约束where T : new()T 是拥有公共无参构造函数的非抽象类

2.3 其他验证问题

(1)类型转换问题

泛型参数进行类型转换,要保证符合约束。尽量使用 as 进行转换。

public void DoSomething<T>(T obj)
{
    int x = (int)obj; //编译错误
    int x = (int)(Object)obj; //编译通过,但运行时可能抛 InvalidCastException
    stirng x = obj as string; //推荐
}

(2)将泛型类型转为默认值

推荐使用 default 关键字。

public void DoSomething<T>()
{
    T temp = null; //编译错误,除非指定 T : class
    T temp = default(T); //推荐
}

(3)两个泛型参数对比

需指定泛型约束。

public void DoSomething<T>(T o1, T o2)
{
    if (o1 == o2){} //编译失败,因为若 T 是值类型,那么可能没有重载 == 运算符。
}

(4)不能将泛型约束为具体的值类型,一是因为值类型是隐式密封的,无法被继承;二是因为这种情况使用泛型就没有意义。

参考文献

[1] 《CLR via C#》 第四版

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值