C# 之泛型

C# 之泛型

一、何为泛型

  C#泛型的说明抄自MSDN: 泛型是 2.0 版 C# 语言和公共语言运行库 (CLR) 中的一个新功能。泛型将类型参数的概念引入 .NET Framework,类型参数使得设计如下类和方法成为可能:这些类和方法将一个或多个类型的指定推迟到客户端代码声明并实例化该类或方法的时候。例如,通过使用泛型类型参数 T,您可以编写其他客户端代码能够使用的单个类,而不致引入运行时强制转换或装箱操作的成本或风险。

  如以下代码:

using System;

namespace Demo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            GenericTest<String> t = new GenericTest<String>();
            t.obj = "Hello";
            // t.obj = 3; Error
            Console.WriteLine(t.obj.GetType().Name);
        }
    }

    public class GenericTest<T>
    {
        public T obj ;
    }
}

  t的类型为GenericTest<String>,那么在GenericTest<T>类中,所有的T都将会被String替代。因此,我们不能将3赋值给t.obj。

二、为什么要用泛型

  使用泛型主要是从类型的安全性和效率上考虑的。在不使用泛型的时候:

using System.Collections;
using System;

namespace Demo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            ArrayList arr = new ArrayList();
            arr.Add("547");
            arr.Add(3);
            foreach (var i in arr)
            {
                int.Parse((string)i);
            }
        }
    }
}

  C#中有一个ArrayList类,它没有使用泛型,其中保存的是Object类型的成员,由于所有类都继承于Object,因此它可以容纳所有对象。在上段代码中主要有2个问题:1、没有类型的安全检查,例如我们本来想在其中放入String对象,但是可能因为手误或者其他原因混进了别的类型,导致代码出错。2、在操作对象的时候,进行反复地拆包、装包工作,导致效率的降低。因此,可以用泛型来替代:
using System;
using System.Collections.Generic;

namespace Demo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            List<String> arr = new List<String>();
            arr.Add("547");
            //arr.Add(3); Error
            foreach (var i in arr)
            {
                int.Parse((string)i);
            }
        }
    }
}

  上一段代码中,arr再也无法添加数字3了,因为它不是String。

三、泛型是一种单独的类型吗

  是。

  如下一段C#代码:

using System;
using System.Collections.Generic;

namespace Demo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            List<String> genericStr = new List<String>();
            List<Int32> genericInt = new List<Int32>();
            Console.WriteLine(genericStr.GetType().FullName);
            Console.Write(genericStr.GetType() == genericInt.GetType());
        }
    }
}
  最后控制台输出的是false,说明List<String>和List<Int32>确确实实是两个不同的类型。因为如此,C#中的泛型用起来才十分方便。

  在Java中,泛型是一个比较难以理解的机制。如Java中的List<String>、List<Integer>,它们都会被看做是List类,List类被称为原生类型(Raw type),因此无论是List<String>还是List<Integer>的实例,它们的类型总是相同的。另外,Java有一个擦除机制,即List<T>的T只存在于编译器,在运行时,T类型的对象实质上是Object类型,因此你无法获得运行时它的实际类型,如无法用T a = new T()、obj instanceof T这样的语句来进行判断。

四、泛型的约束

  有时候,我们希望泛型T是继承于某类、某接口,或者拥有默认构造方法等。

using System;
namespace Demo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            B<A> test = new B<A>();
            B<Int32> test2 = new B<Int32>();    //编译错误,因为Int32不继承于A
        }
    }

    public class A
    {
        public void Test() { }
    }

    public class B<T> where T : A
    {
        public T obj;
    }
}

  B类的定义后面加了 where T : A,表明 T 必须要继承A。一下是 where 所有的用法:

where T : struct -> T一定要为值类型

where T : class -> T一定要为引用类型

where T : A -> T一定要继承A (A可以为类名或接口名)

where T : new() -> T一定要有无参构造方法

where T1 : T2 -> T1一定要继承于T2 (裸类型约束)

  值得一提的是 where T : new(),只有定义了这样的约束,才可以执行 T a = new T()这样的语句(而这样的语句是绝对不可能在Java中运行的)。

  泛型约束的意义究竟在哪里呢?请看下面的例子:

using System;
namespace Demo
{
    public class A
    {
        public void Test() { }
    }

    public class B<T> where T : A
    {
        public T obj;
        public void Test()
        {
            obj.Test();
        }
    }
}

  只有说明了T是继承于A的,才能够调用obj.Test。因为,继承于A的T里面一定有Test方法,将where子句去掉,程序将无法通过编译。

五、协变与逆变(抗变)

  首先请看以下代码:

using System.Collections.Generic;
namespace Demo
{
    class Food { };
    class Bread : Food { }
    class Program
    {
        public static void Main()
        {
            IEnumerable<Bread> bread = new List<Bread>();
            IEnumerable<Food> food = bread;
        }
    }
}

  我们发现,bread可以将值赋予food,我们从Food和Bread的定义可知,Bread是Food的子类。像这样将接口类型赋予下层级别(子类)(Bread -> Food),成为 协变,也就是基类类型的接口充当一个更大的容器(能够装食物的容器肯定可以装面包)。

  而以下例子又有所不同:

using System;
namespace Demo
{
    class Program
    {
        public static void Main()
        {
            Action<object> actObject = (o) => { };
            Action<string> actString = actObject; 
        }
    }
}

  我们发现,actString被赋予了actObject,这种是从 Object -> String 的转变,将一个抽象的对象具体化,和协变相反,我们称之为 逆变。它是对象具体化的一个过程。

  目前,只有接口、委托才可以定义泛型参数类型。在参数前加out表示协变,加in表示逆变。如IEnumerable<out T>、Action<in T>。

六、泛型方法

  之前我们提到了泛型类、泛型接口,以及刚刚提及了泛型委托,其实方法也可以使用泛型。

using System;
namespace Demo
{
    class Program
    {
        public static void Main()
        {
            int i = GetItem<int>(5);
        }

        public static T GetItem<T>(T item)
        {
            return item;
        }
    }
}

  GetItem为一泛型方法,我们显示强调我们要使用GetItem<int>,意味着GetItem的第一个参数一定要为int。但是,我们也可以用如下方法调用GetItem方法:

string i = GetItem("s");

  我们并没有说明GetItem中泛型T的类型,但是我们很快意识到我们也没有必要说明。编译器发现GetItem后的第一个参数为一个string,于是就把T当做String来处理,这样,我们就不用在意这个方法是泛型方法,而可以把它当成普通方法来看待。

  以上两种做法各有好处。第一种方法可以显示强调正在使用泛型方法,并且编译器会检查参数类型是否正确,但是花费的笔墨太多,第二种方法的优缺点和第一种方法相反。

七、总结

  泛型是一个有用的机制。学习Java的同学最开始很容易被Java的泛型搞得一头雾水,但是C#中泛型类就是一个单独的类,List<int>和List<bool>就是两种不同的类型。泛型多用于容器(List<T>)和委托(Action<T>, Func<T1, TResult>)。泛型的作用可能会接近于“多态”,以至于不知道到底该用泛型还是用基类,如ArrayList和List<T>。这个需要在实际情况中结合类型安全性和效率来考虑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值