C#泛型的理解/为什么要使用泛型/什么是泛型/怎么用泛型

1、为什么要使用泛型

我们在编写程序时,经常遇到两个模块的功能非常相似,只是一个是处理int数据,另一个是处理string数据,或者其他自定义的数据类型,但我们没有办法,只能分别写多个方法处理每个数据类型,因为方法的参数类型不同。有没有一种办法,在方法中传入通用的数据类型,这样不就可以合并代码了吗?泛型的出现就是专门解决这个问题的。

泛型在实例化对象的时候,使用这个对象需要的类型

读完本篇文章,你会对泛型有更深的了解。
我们先简单的看一下下面的需求及其解释,后面我们会详细讲解里面的具体特性。
我们现在要求实现一个栈,这个栈只能处理int数据类型:

public class Stack{
    private int[] a;
    public int pop(){}

    public void push(int aa){}

    public Stack(int i){

        this.a=new int[i];
    }
}

上面代码运行的很好,但是,当我们需要一个栈来保存string类型时,该怎么办呢?很多人都会想到把上面的代码复制一份,把int改成string不就行了。当然,这样做本身是没有任何问题的,但一个优秀的程序是不会这样做的,因为他想到若以后再需要long、Node类型的栈该怎样做呢?还要再复制吗?优秀的程序员会想到用一个通用的数据类型object来实现这个栈:

public class Stack{
    private int[] a;
    public int pop(){}

    public void push(object aa){}

    public Stack(int i){

        this.a=new int[i];
    }
}

这个栈写的不错,他非常灵活,可以接收任何数据类型,可以说是一劳永逸。但全面地讲,也不是没有缺陷的,主要表现在:

当Stack处理值类型时,会出现装箱、折箱操作,这将在托管堆上分配和回收大量的变量,若数据量大,则性能损失非常严重。
在处理引用类型时,虽然没有装箱和折箱操作,但将用到数据类型的强制转换操作,增加处理器的负担。

所以泛型出现了,真正解决了我们的问题。

下面是用泛型来重写上面的栈,用一个通用的数据类型T来作为一个占位符,等待在实例化时用一个实际的类型来代替。让我们来看看泛型的威力:

public class Stack<T>{
    private T[] a;
    public T pop(){}

    public void push(T aa){}

    public Stack(int i){

        this.a=new int[i];
    }
}

类的写法不变,只是引入了通用数据类型T就可以适用于任何数据类型,并且类型安全的。这个类的调用方法:

//实例化只能保存int类型的类
 Stack<int> b=new Stack<int>(100);
    b.Push(10);
    int x=async.pop();

//实例化只能保存string类型的类
Stack<sting> c=new Stack<string>(100);
    c.Push(10);//这一行无法编译通过因为c的类型只接受string
    c.push("8888");
    string y=c.pop();

这个类和object实现的类有截然不同的区别:

  1. 他是类型安全的。实例化了int类型的栈,就不能处理string类型
    的数据,其他数据类型也一样。
    2.无需装箱和折箱。这个类在实例化时,按照所传入的数据类型生成
    本地代码,本地代码数据类型已确定,所以无需装箱和折箱。
  2. 无需类型转换。

2、怎么使用泛型?

泛型的意思就是Generic,它所代表的是通用类型,可以代替任意的数据类型,使得类型参数化,从而达到了只需要一个方法就可以操作多种数据的目的。泛型将方法实现行为与方法操作的数据类型分离,实现了代码重用,下面的代码演示了泛型的作用。
public static void Main(string[] args){
    
    List<int> intList=new List<int>;
    intList.Add(3);

     List<string> stringList=new List<string>;
    string.Add("hello");
    
}

我们如果想实现两个数之间的比较,具体的代码如下。如果不引入泛型的话我们下面的一段比较代码就必须这么实现:

public class Compare{
    public static int CompareInt(int int1,int int2){
        if(int1.CompareTo(int2)>0){
            return int1;
        } else
        return int2;
    }

      public static string CompareInt(string int1,string int2){
        if(int1.CompareTo(int2)>0){
            return int1;
        } else
        return int2;
    }
}

代码虽然完成了需求,但是如果我们想再次增加浮点类型,则我们需要再次修改代码,甚是麻烦。所以,提供了泛型这个特性,使得类型可以被参数化,使用它的时候,如下所示:

public class Compare<T> where T:ICompareble{
    public static T CompareGeneric(T t1,T t2){
        if(t1.CompareTo(t2)>0){
            return t1;
        } else
        {
        return t2;
        }
    }
}

我们上面实现了一个自定义的泛型类,其中T是泛型的类型参数,compareGeneric是实现的泛型方法。代码where是参数类型的约束,它用来使得类型参数可以适用于CompareTo方法。这样,我们在主函数调用的时候,如下:

System.Console.WriteLine(Compare<int>.compareGeneric(3,4));
System.Console.WriteLine(Compare<string>.compareGeneric("abc","a"));

泛型不仅仅可以实现代码的重用,减少了装箱和拆箱的过程。泛型是避免损失的有效方法。下面我们通过代码测试了使用泛型和不适用泛型的执行时间。向泛型数组中加入元素的效率远高于非泛型数组,因为非泛型的Add操作中,参数为object类型,当int传入的时候,会发生装箱操作,从而导致性能的损失,时间变长。

3、全面解析泛型

3.1 类型参数

泛型分为开放泛型和封闭泛型。其中开放类型是指包含类型参数的泛型,但是未绑定的类型;封闭类型是指已经为每一个类型参数都传递了数据类型

4、类型参数约束

在前面我们实现的泛型之中,我们使用了where T:IComparable的代码,其中where语句用来使类型继承于IComparable接口,从而对类型参数进行约束。下面我们来看看泛型常见的约束。

4.1 基类约束

基类约束有两个功能。首先,它允许在泛型类中使用自由约束指定的基类所定义的成员。通过提高基类约束,编译器将知道所有的类型实参都拥有由指定的基类所定义的成员。基类约束的第二个功能是,确保只使用支持指定基类的类型实参。这意味着对于任意给定的基类约束,类型实参必须是基类本身或者是派生于该基类的类。基类约束使用如下形式的where子句:

where T:base-class-name

其中,T是类型形参的名称,base-class-name是基类的名称。只能指定一个基类。

4.2 接口约束

接口约束是指定某个类型实参必须实现的接口。它的两个主要功能与基类约束一样,允许开发人员在泛型类中使用接口的成员;确保只能使用实现了特定接口的类型实参。也就是说对任何给定的接口约束,类型实参必须是接口本身或者实现了该接口的类。接口约束使用的where子句具有以下形式:
where T:interface-name
其中,T是类型形参的名称,interface-name是接口的名称。可

以使用逗号分隔开指定的多个接口。若某个约束同时包含基类和接口,则需先指定基类列表,再指定接口列表。

4.3 new()构造函数约束

new()构造函数约束允许开发人员实例化一个泛型类型的对象。new()约束要求类型实参必须提供一个无参数的公有构造函数。使用new()约束时,可以通过调用该无参数的构造函数来创建对象。new()构造函数约束的形式为:

where T:new()

使用new()约束时应当注意3点:
1.new()约束可以与其他约束一起使用,但必须位于约束列表的末端
2.new()约束仅允许开发人员使用无参数的构造函数构造一个对象,
即使同时存在其他的构造函数也是如此。即不允许给类型形参的构造
函数传递实参。
3.不可以同时使用new()约束和值类型约束。因为值类型都隐式的提
供了一个无参公共构造函数。就如同定义接口时指定访问类型为
public一样,编译器会报错,因为接口一定是public的。

4.4 引用类型约束

引用类型约束将一个类型形参限定为引用类型。引用类型一般是用户定义的类型,包含类、接口、委托、字符串和数组类型。引用类型约束使用class关键字,它的通用形式为

where T:class

在这个where子句中,class关键字是指定T必须是引用类型。因此,尝试对T使用值类型,将会导致编译错误。

4.5 值类型约束

值类型约束将一个类型形参限定为值类型。值类型派生于System.ValueType类型。基元和结构都是值类型。值类型约束使用struct关键字,它的通用形式为:

where T:struct
在该形式中,struct关键字指定T必须是值类型。因此,尝试对T使用引用类型,将导致编译错误。

4.6 组合约束

同一个类型形参可以使用多个约束。这种情况下,需要使用一个由逗号分隔的约束列表。在该列表中,第一个约束必须是引用类型约束或者值类型约束,或者是基类约束。指定引用类型约束或值类型约束的同时也指定基类约束是非法的。接下来必须是所有的接口约束,最后是new()约束。下面是一个合法的声明:

class Test<T>where T:MyClass,linterface,new(){}

在上述声明中,用于替换T的类型实参必须继承MyClass类,实现Iinterface接口,并且拥有一个无参数的构造函数。
在使用两个或者更多的类型形参时,可以使用多条where子句分别为它们指定约束。

5 、泛型委托

与方法一样,委托也可以是泛型的。因为泛型内在的类型安全性,无法将不兼容的方法赋给委托。声明泛型委托的形式为:

delegate ret-type delegate-name<type-parameter-list>(arg-list);

类型形参的声明紧跟在委托的名称之后。泛型委托的优点在于,它允许开发人员以类型安全的方式定义一种通用形式,该形式可用于匹配任意兼容的方法。

6 、泛型接口

除了定义泛型类和泛型方法外,还可以定义泛型接口。泛型接口的定义与泛型类基本相同。泛型接口的声明形式为:

interface linterface<T>{}

使用泛型时,要注意一下限制:

1、extern修饰符不能用于泛型方法。

2、属性、运算符、索引器和事件不能泛型化。这些项仍可以用作泛型中,并且可以使用类的泛型类型形参。

3、指针类型不能用作类型实参。

4、如果泛型类包含一个static字段,那么每一个构造类型都会有该字段的独立副本。这意味着同一个构造函数类型的所有实例都会共享同一个static字段。然而,不同的构造类型使用不同的字符副本。因此,static字段并不是由所有的构造类型共享。

7、泛型类中的方法重载

方法的重载在.Net Framework中被大量应用在泛型类中,由于通用类型T在类编写时并不确定,所以在重载时有些注意事项,这些事项我们通过以下的例子说明:

public class Node<T,V>{
    public T add(T a,V b)
    {
        return a;
    }
    
    public T add(V a,T b)
    {
        return b;
    }
    
    public int add(int a,int b){

        return a+b;
    }
}

上面的类很明显,如果T和V都传入int的话,三个add方法将具有同样的签名,但这个类仍然能通过编译,是否会引起调用混淆将在这个类实例化和调用add方法时判断。请看下面调用代码:

Node<int,int>node=new Node<int, int>();
object x=node.add(2,11);

这个Node的实例化引起了三个add具有同样的签名,但却能调用成功,因为他优先匹配了第三个add。但如果删除了第三个add,上面的调用代码则无法编译通过,提示方法产生的混淆,因为运行时无法在第一个add和第二个add之间选择。

Node<string,int>node=new Node<string, int>();
object x=node.add(2,"11");

这两行调用代码可正确编译,因为传入的string和int,使三个add具有不同的签名,当然能找到唯一匹配的add方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值