泛型相比反射,委托等较为抽象的概念要更接地气得多,而且在平常工作时,我们几乎时刻都和泛型有接触。大部分人对泛型都是比较熟悉的。

泛型集合是类型安全的集合。相对于泛型System.Collections.Generic,我们有类型不安全的集合System.Collections,其中的成员均为Object类型。一个经典的例子是ArrayList。

在使用ArrayList时,我们可以插入任意类型的数据,如果插入值类型的数据,其都会装箱为Object类型。这造成类型不安全,我们不知道取出的数据是不是想要的类型。泛型(集合)的数据类型是统一的,是类型安全的,没有装箱和拆箱问题,提供了更好的性能。为泛型变量设置默认值时常使用default关键字进行:T temp = default(T)。如果T为引用类型,则temp为null,如果T为值类型,则temp为0。

ArrayList的泛型集合版本为List<T>。T称为类型参数。调用时指定的具体类型叫做实际参数(实参)。

面试必须知道的泛型三大好处:类型安全,增强性能,代码复用。

泛型集合的使用契机:几乎任何时候,都不考虑不用泛型集合代替泛型集合。很多非泛型集合也有了自己的泛型版本,例如栈,队列等。

泛型方法

泛型方法的使用契机一般为传入类型可能有很多种,但处理方式却相同的情境。这时我们可以不需要写很多个重载,而考虑用泛型方法达到代码复用的目的。配合泛型约束,可以写出更严谨的方法。泛型委托也可以看成是泛型方法的一种应用。

例如交换两个同类型变量的值:

  1. static void Swap<T>(ref T lhs, ref T rhs)
  2. {
  3. T temp;
  4. temp = lhs;
  5. lhs = rhs;
  6. rhs = temp;
  7. }

泛型约束

约束的作用是限制能指定成泛型实参(即T的具体类型)的数量。通过限制类型的数量,可以对这些类型执行更多的操作。例如下面的方法,T被约束为必须是实现了IComparable接口的类型。此时,传入的T除了拥有object类型的方法之外,还额外多了一个CompareTo方法。由于保证了传入的T必须是实现了IComparable接口的类型,就可以肯定T类型一定含有CompareTo方法。如果去掉约束,o1是没有CompareTo方法的。

  1. static int Compare<T>(T o1, T o2) where T : IComparable<T>
  2. {
  3. return o1.CompareTo(o2);
  4. }

此时如果将object类型的数据传入方法,则会报错。因为object没有实现IComparable<T>接口。

泛型约束分为如下几类:

  • 接口约束:泛型实参必须实现某个接口。接口约束可以有多个。
  • 基类型约束:泛型实参必须是某个基类的派生类。特别的,可以指定T : class / T : struct,此时T分别只能为引用类型或值类型。基类型约束必须放在其他约束之前。
  • 构造函数new()约束:泛型实参必须具有可访问的无参数构造函数(默认的也可)。new()约束出现在where子句的最后。

如果泛型方法没有任何约束,则传入的对象会被视为object。它们的功能比较有限。不能使用 != 和 == 运算符,因为无法保证具体类型参数能支持这些运算符。

协变和逆变

可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用。其对应的术语则是不变性(invariant)。

可变性

可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用。例如对普通继承中的可变性:若某方法声明返回类型为Stream,在实现时可以返回一个MemoryStream。可变性有两种类型:协变和逆变。

协变性:可以建立一个较为一般类型的变量,然后为其赋值,值是一个较为特殊类型的变量。例如:

  1. string str = "test";
  2. // An object of a more derived type is assigned to an object of a less derived type.
  3. object obj = str;

因为string肯定是一个object,所以这样的变化非常正常。

逆变性:在上面的例子中,我们无法将str和一个新的object对象画等号。如果强行要实现的话,只能这么干:

  1. string s = (string) new object();

但这样还是会在运行时出错。这也告诉我们,逆变性是很不正常的。

泛型的协变与逆变

协变性和out关键字搭配使用,用于向调用者返回某项操作的值。例如下面的接口仅有一个方法,就是生产一个T类型的实例。那么我们可以传入一个特定类型。如我们可以将IFactory<Pizza>视为IFactory<Food>。这也适用于Food的所有子类型。(即将其视为一个更一般类型的实现)

  1. interface IFactory<T>
  2. {
  3. T CreateInstance();
  4. }

逆变性则相反,in关键字搭配使用,指的是API将会消费值,而不是生产值。此时一般类型出现在参数中:

  1. interface IPrint<T>
  2. {
  3. void Print(T value);
  4. }

这意味着如果我们实现了IPrint<Code>,我们就可以将其当做IPrint<CsharpCode>使用。(即将其视为一个更具体类型的实现)

如果存在双向的传递,则什么也不会发生。这种类型是不变体(invariant)。

  1. interface IStorage<T>
  2. {
  3. byte[] Serialize(T value);
  4. T Deserialize(byte[] data);
  5. }

这个接口是不变体。我们不能将它视为一个更具体或更一般类型的实现。

假设有如下继承关系People –> Teacher,People –> Student。

如果我们以协变的方式使用(假设你建立了一个IStorage< Teacher >的实例,并将其视为IStorage<People>)则我们可能会在调用Serialize时产生异常,因为Serialize方法不支持协变(如果参数是People的其他子类,例如Student,则IStorage< Teacher >将无法序列化Student)。

如果我们以逆变的方式使用(假设你建立了一个IStorage<People>的实例,并将其视为IStorage< Teacher >),则我们可能会在调用Deserialize时产生异常,因为Deserialize方法不支持逆变,它只能返回People不能返回Teacher。

使用in和out表示可变性

如果类型参数用于输出,就使用out,如果用于输入,就使用in。注意,协变和逆变性体现在泛型类T和T的派生类。目前out 和in 关键字只能在接口和委托中使用。

IEnumerable<out T>支持协变性

IEnumerable<T>支持协变性,它允许一个类似下面签名

  1. void 方法(IEnumerable<T> anIEnumberable)

的方法,该方法传入更具体的类型(T的派生类),但在方法内部,类型会被看成IEnumerable<T>。注意out关键字。

下面的例子演示了协变性。我们利用IEnumerable<T>的协变性,传入较为具体的类型Circle。编译器会将其看成较为抽象的类型Shape。

  1. public class Program
  2. {
  3. public static void Main(string[] args)
  4. {
  5. var circles = new List<Circle>
  6. {
  7. new Circle(new Point(, ), ),
  8. new Circle(new Point(, ), ),
  9. };
  10. var list = new List<IShape>();
  11.  
  12. //泛型的协变:
  13. //AddRange传入的是特殊的类型List<Circle>,但要求是一般的类型List<IShape>
  14. //AddRange方法签名:void AddRange(IEnumerable<T> collection)
  15. //IEnumerable<out T>允许协变(对于LINQ来说,协变尤其重要,因为很多API都表示为IEnumerable<T>)
  16. list.AddRange(circles);
  17.  
  18. //C# 4.0之前只能这么做
  19. list.AddRange(circles.Cast<IShape>());
  20. }
  21. }
  22.  
  23. public sealed class Circle : IShape
  24. {
  25. private readonly Point center;
  26. public Point Center { get { return center; } }
  27.  
  28. private readonly double radius;
  29. public double Radius { get { return radius; } }
  30.  
  31. public Circle(Point center, int radius)
  32. {
  33. this.center = center;
  34. this.radius = radius;
  35. }
  36.  
  37. public double Area
  38. {
  39. get { return Math.PI * radius * radius; }
  40. }
  41. }
  42.  
  43. public interface IShape
  44. {
  45. double Area { get; }
  46. }

IComparer<in T>支持逆变性

IComparer支持逆变性。我们可以简单的实现一个可以比较任何图形面积的方法,传入的输入类型(in是最General的类型IShape。之后,在使用时,我们获得的结果是较为具体的类型Circle。因为任何图形都可以比较面积,圆形当然也可以。

注意IComparer的签名是public interface IComparer<in T>。

  1. public class Program
  2. {
  3. public static void Main(string[] args)
  4. {
  5. var circles = new List<Circle>
  6. {
  7. new Circle(new Point(, ), ),
  8. new Circle(new Point(, ), ),
  9. };
  10.  
  11. //泛型的逆变:
  12. //AreaComparer可以比较任意图形的面积,但我们可以传入具体的图形例如圆或正方形
  13. //Compare方法签名:Compare(IShape x, IShape y)
  14. //IComparer<in T>支持逆变
  15. //传入的是圆形Circle,但要求的输入是IShape
  16. circles.Sort(new AreaComparer());
  17. }
  18. }
  19.  
  20. class AreaComparer : IComparer<IShape>
  21. {
  22. public int Compare(IShape x, IShape y)
  23. {
  24. return x.Area.CompareTo(y.Area);
  25. }
  26. }

C#中泛型可变性的限制

1. 不支持类的类型参数的可变性。只有接口和委托可以拥有可变的类型参数。in out 修饰符只能用来修饰泛型接口和泛型委托。

2. 可变性只支持引用转换。可变性只能用于引用类型,禁止任何值类型和用户定义的转换,如下面的转换是无效的:

  • 将 IEnumerable<int> 转换为 IEnumerable<object> ——装箱转换
  • 将 IEnumerable<short> 转换为 IEnumerable<int> ——值类型转换
  • 将 IEnumerable<string> 转换为 IEnumerable<XName> ——用户定义的转换

3. 类型参数使用了 out 或者 ref 将禁止可变性。对于泛型类型参数来说,如果要将该类型的实参传给使用 out 或者 ref 关键字的方法,便不允许可变性,如:

  1. delegate void someDelegate<in T>(ref T t)

这段代码编译器会报错。

4. 可变性必须显式指定。从实现上来说编译器完全可以自己判断哪些泛型参数能够逆变和协变,但实际却没有这么做,这是因为C#的开发团队认为:必须由开发者明确的指定可变性,因为这会促使开发者考虑他们的行为将会带来什么后果,从而思考他们的设计是否合理。

5. 多播委托与可变性不能混用。下面的代码能够通过编译,但是在运行时会抛出 ArgumentException 异常:

  1. Func<string> stringFunc = () => "";
  2. Func<object> objectFunc = () => new object();
  3. Func<object> combined = objectFunc + stringFunc;

这是因为负责链接多个委托的 Delegate.Combine方法要求参数必须为相同的类型,而上面的两个泛型委托的输出一个为字符串,另一个为object。上面的示例我们可以修改成如下正确的代码:

  1. Func<string> stringFunc = () => "";
  2. Func<object> defensiveCopy = new Func<object>(stringFunc);
  3. Func<object> objectFunc = () => new object();
  4. Func<object> combined = objectFunc + defensiveCopy;

此时两个泛型委托的输出均为object。

协变与逆变的相互作用

以下的代码中,接口IBar中有一个方法,其接受另一个接口IFoo作为参数。IFoo是支持协变的。这样会出现一个问题。

  1. interface IFoo<in T>
  2. {
  3.  
  4. }
  5.  
  6. interface IBar<in T>
  7. {
  8. void Test(IFoo<T> foo);
  9. }

假设T为字符串类型。则如果有一类Bar <T>: IBar<T>,另一类Foo<T>:IFoo<T>,则Bar的某个实例应该可以这样调用方法:aBar.Test (foo)。

  1. class Bar<T> : IBar<T>
  2. {
  3. public void Test(IFoo<T> foo)
  4. {
  5. throw new NotImplementedException();
  6. }
  7. }
  8.  
  9. class Foo<T> : IFoo<T>
  10. {
  11.  
  12. }
  13.  
  14. class Program
  15. {
  16. public static void Main()
  17. {
  18. Bar<string> aBar = new Bar<string>();
  19. Foo<object> foo = new Foo<object>();
  20. aBar.Test(foo);
  21. }
  22. }

当调用方法之后,传入的参数类型是Foo<object>。我们再看看方法的签名:

  1. interface IBar<in T>
  2. {
  3. void Test(IFoo<T> foo);
  4. }

现在我们的aBar的类型参数T是string,所以,我们期待的Test方法的传入类型也应该是IFoo<string>,或者能够变化成IFoo<string>的类型,但传入的却是一个object。所以,这两个接口的方法的写法是有问题的。

  1. interface IFoo<out T>
  2. {
  3.  
  4. }

当把IFoo接口的签名改用out修饰之后,问题就解决了。此时由于允许逆变,Foo<object>就可以变化成IFoo<string>了。不过本人眼光短浅,目前还没发现这个特点在实际工作中有什么应用。

参考资料

http://www.cnblogs.com/LoveJenny/archive/2012/03/13/2392747.html

http://www.cnblogs.com/xinchufa/p/3524452.html

http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

.NET面试题系列[8] - 泛型的更多相关文章

  1. .NET面试题系列[0] - 写在前面

    .NET面试题系列目录 .NET面试题系列[1] - .NET框架基础知识(1) .NET面试题系列[2] - .NET框架基础知识(2) .NET面试题系列[3] - C# 基础知识(1) .NET ...

  2. .NET面试题系列[15] - LINQ:性能

    .NET面试题系列目录 当你使用LINQ to SQL时,请使用工具(比如LINQPad)查看系统生成的SQL语句,这会帮你发现问题可能发生在何处. 提升性能的小技巧 避免遍历整个序列 当我们仅需要一 ...

  3. .NET面试题系列[14] - LINQ to SQL与IQueryable

    .NET面试题系列目录 名言警句 "理解IQueryable的最简单方式就是,把它看作一个查询,在执行的时候,将会生成结果序列." - Jon Skeet LINQ to Obje ...

  4. .NET面试题系列[12] - C# 3.0 LINQ的准备工作

    "为了使LINQ能够正常工作,代码必须简化到它要求的程度." - Jon Skeet 为了提高园子中诸位兄弟的英语水平,我将重要的术语后面配备了对应的英文. .NET面试题系列目录 ...

  5. .NET面试题系列[11] - IEnumerable<T>的派生类

    “你每次都选择合适的数据结构了吗?” - Jeffery Zhao .NET面试题系列目录 ICollection<T>继承IEnumerable<T>.在其基础上,增加了Ad ...

  6. .NET面试题系列[10] - IEnumerable的派生类

    .NET面试题系列目录 IEnumerable分为两个版本:泛型的和非泛型的.IEnumerable只有一个方法GetEnumerator.如果你只需要数据而不打算修改它,不打算为集合插入或删除任何成 ...

  7. .NET面试题系列[9] - IEnumerable

    .NET面试题系列目录 什么是IEnumerable? IEnumerable及IEnumerable的泛型版本IEnumerable<T>是一个接口,它只含有一个方法GetEnumera ...

  8. 【转载】.NET面试题系列[0] - 写在前面

    原文:.NET面试题系列[0] - 写在前面 索引: .NET框架基础知识[1] - .NET框架基础知识(1) http://www.cnblogs.com/haoyifei/p/5643689.h ...

  9. .NET面试题系列

    索引: .NET框架基础知识[1] - http://www.cnblogs.com/haoyifei/p/5643689.html .NET框架基础知识[2] - http://www.cnblog ...

随机推荐

  1. ASP.NET Core MVC/WebAPi 模型绑定探索

    前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用 ...

  2. PHP实现RTX发送消息提醒

    RTX是腾讯公司推出的企业级即时通信平台,大多数公司都在使用它,但是我们很多时候需要将自己系统或者产品的一些通知实时推送给RTX,这就需要用到RTX的服务端SDK,建议先去看看RTX的SDK开发文档( ...

  3. IE的F12开发人员工具不显示问题

    按下F12之后,开发人员工具在桌面上看不到,但是任务栏里有显示.将鼠标放在任务栏的开发人员工具上,出现一片透明的区域,选中之后却出不来.将鼠标移动到开发人员工具的缩略图上,右键-最大化,工具就全屏出现 ...

  4. Java学习之反射机制及应用场景

    前言: 最近公司正在进行业务组件化进程,其中的路由实现用到了Java的反射机制,既然用到了就想着好好学习总结一下,其实无论是之前的EventBus 2.x版本还是Retrofit.早期的View注解框 ...

  5. Oracle学习之路-- 案例分析实现行列转换的几种方式

    注:本文使用的数据库表为oracle自带scott用户下的emp,dept等表结构. 通过一个例子来说明行列转换: 需求:查询每个部门中各个职位的总工资 按我们最原始的思路可能会这么写:       ...

  6. Javascript 代理模式模拟一个文件同步功能

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. php注册审核

    通过注册审核,判断刚创建的账户是否可以使用. 后台管理员审核通过后,账号可以使用. 通过session 设置只能通过登录入口进入网页. 原理:通过数据库设置账号的一个字段状态,例: isok:1, i ...

  8. WPF 普通属性变化通知

    问题描述:使用ObservableCollection<OrderItem> source 给Datagrid.ItemsSource赋值,在后台更新source集合后,前台Datagri ...

  9. QQ空间动态爬虫

    作者:虚静 链接:https://zhuanlan.zhihu.com/p/24656161 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 先说明几件事: 题目的意 ...

  10. 关于DDD的学习资料汇总

    DDD(Domain-Driven Design)领域驱动设计,第一次看到DDD是在学习ABP时,在其中的介绍中看到的.what,DDD是个什么鬼,我不是小白,是大白,没听过.于是乎,度娘查查查,找到 ...