.NET面试题系列[8] - 泛型

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

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

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

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

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

泛型集合的使用契机:几乎任什么时候候,都不考虑不用泛型集合代替泛型集合。不少非泛型集合也有了本身的泛型版本,例如栈,队列等。性能

泛型方法

泛型方法的使用契机通常为传入类型可能有不少种,但处理方式却相同的情境。这时咱们能够不须要写不少个重载,而考虑用泛型方法达到代码复用的目的。配合泛型约束,能够写出更严谨的方法。泛型委托也能够当作是泛型方法的一种应用。this

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

static void Swap<T>(ref T lhs, ref T rhs)
{
    T temp;
    temp = lhs;
    lhs = rhs;
    rhs = temp;
}

泛型约束

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

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

此时若是将object类型的数据传入方法,则会报错。由于object没有实现IComparable<T>接口。

泛型约束分为以下几类:

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

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

协变和逆变

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

可变性

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

协变性:能够创建一个较为通常类型的变量,而后为其赋值,值是一个较为特殊类型的变量。例如:

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

由于string确定是一个object,因此这样的变化很是正常。 

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

string s = (string) new object();

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

 

泛型的协变与逆变

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

    interface IFactory<T>
    {
        T CreateInstance();
    }

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

    interface IPrint<T>
    {
        void Print(T value);
    }

这意味着若是咱们实现了IPrint<Code>,咱们就能够将其当作IPrint<CsharpCode>使用。(即将其视为一个更具体类型的实现)

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

     interface IStorage<T>
    {
        byte[] Serialize(T value);
        T Deserialize(byte[] data);
    }

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

假设有以下继承关系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>支持协变性,它容许一个相似下面签名

void 方法(IEnumerable<T> anIEnumberable)

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

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

    public class Program
    {
        public static void Main(string[] args)
        {
            var circles = new List<Circle>
            {
                new Circle(new Point(0, 0), 15),
                new Circle(new Point(10, 5), 20),
            };
            var list = new List<IShape>();

            //泛型的协变:
            //AddRange传入的是特殊的类型List<Circle>,但要求是通常的类型List<IShape>
            //AddRange方法签名:void AddRange(IEnumerable<T> collection)
            //IEnumerable<out T>容许协变(对于LINQ来讲,协变尤为重要,由于不少API都表示为IEnumerable<T>)
            list.AddRange(circles);

            //C# 4.0以前只能这么作
            list.AddRange(circles.Cast<IShape>());
        }
    }

    public sealed class Circle : IShape
    {
        private readonly Point center;
        public Point Center { get { return center; } }

        private readonly double radius;
        public double Radius { get { return radius; } }

        public Circle(Point center, int radius)
        {
            this.center = center;
            this.radius = radius;
        }

        public double Area
        {
            get { return Math.PI * radius * radius; }
        }
    }

    public interface IShape
    {
        double Area { get; }
    }

IComparer<in T>支持逆变性

IComparer支持逆变性。咱们能够简单的实现一个能够比较任何图形面积的方法,传入的输入类型(in是最General的类型IShape。以后,在使用时,咱们得到的结果是较为具体的类型Circle。由于任何图形均可以比较面积,圆形固然也能够。

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

    public class Program
    {
        public static void Main(string[] args)
        {
            var circles = new List<Circle>
            {
                new Circle(new Point(0, 0), 15),
                new Circle(new Point(10, 5), 20),
            };

            //泛型的逆变:
            //AreaComparer能够比较任意图形的面积,但咱们能够传入具体的图形例如圆或正方形
                //Compare方法签名:Compare(IShape x, IShape y)
            //IComparer<in T>支持逆变
            //传入的是圆形Circle,但要求的输入是IShape
            circles.Sort(new AreaComparer());
        }
    }

    class AreaComparer : IComparer<IShape>
    {
        public int Compare(IShape x, IShape y)
        {
            return x.Area.CompareTo(y.Area);
        }
    }

C#中泛型可变性的限制

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

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

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

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

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

这段代码编译器会报错。

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

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

Func<string> stringFunc = () => "";
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + stringFunc;

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

Func<string> stringFunc = () => "";
Func<object> defensiveCopy = new Func<object>(stringFunc);
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + defensiveCopy;

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

 

协变与逆变的相互做用

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

    interface IFoo<in T>
    {

    }

    interface IBar<in T>
    {
        void Test(IFoo<T> foo);
    }

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

    class Bar<T> : IBar<T>
    {
        public void Test(IFoo<T> foo)
        {
            throw new NotImplementedException();
        }
    }

    class Foo<T> : IFoo<T>
    {
        
    }

    class Program
    {
        public static void Main()
        {
            Bar<string> aBar = new Bar<string>();
            Foo<object> foo = new Foo<object>();
            aBar.Test(foo);
        }
    }

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

    interface IBar<in T>
    {
        void Test(IFoo<T> foo);
    }

如今咱们的aBar的类型参数T是string,因此,咱们期待的Test方法的传入类型也应该是IFoo<string>,或者可以变化成IFoo<string>的类型,但传入的倒是一个object。因此,这两个接口的方法的写法是有问题的。

    interface IFoo<out T>
    {

    }

当把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

相关文章
相关标签/搜索