C# 泛型

泛型的使用

演化过程

  1. 没有泛型:多个参数类型的相同方法需要写多次。
public class CommonMethod
{
   /// <summary>
   /// 打印个int值
   /// 声明方法时,指定了参数类型,确定了只能传递某个类型
   /// </summary>
   /// <param name="iParameter"></param>
   public static void ShowInt(int iParameter)
   {
       Console.WriteLine("This is {0},parameter={1},type={2}",
           typeof(CommonMethod).Name,iParameter.GetType().Name,iParameter);
   }

   /// <summary>
   /// 打印个string值
   /// </summary>
   /// <param name="sParameter"></param>
   public static void ShowString(string sParameter)
   {
       Console.WriteLine("This is {0},parameter={1},type={2}",
           typeof(CommonMethod).Name,sParameter.GetType().Name,sParameter);
   }

   /// <summary>
   /// 打印个DateTime值
   /// </summary>
   /// <param name="oParameter"></param>
   public static void ShowDateTime(DateTime dtParameter)
   {
       Console.WriteLine("This is {0},parameter={1},type={2}",
           typeof(CommonMethod).Name,dtParameter.GetType().Name,dtParameter);
   }
}
  1. 使用 Object 代替,解决重复的问题,但是引入装箱拆箱产生的性能问题和类型安全问题
 public class CommonMethod
{
    /// <summary>
    /// 打印个object值
    /// </summary>
    /// <param name="oParameter"></param>
    public static void ShowObject(object oParameter)
    {
        Console.WriteLine("This is {0},parameter={1},type={2}",
            typeof(CommonMethod), oParameter.GetType().Name, oParameter);
    }
}

从上面的结果中我们可以看出,使用Object类型达到了我们的要求,解决了代码的可复用。

object类型的,为什么可以传入int、string等类型呢?

object类型是一切类型的父类。
通过继承,子类拥有父类的一切属性和行为,任何父类出现的地方,都可以用子类来代替。

但是上面object类型的方法又会带来另外一些问题:
装箱和拆箱,会损耗程序的性能。
类型安全问题。

于是微软在C#2.0的时候推出了泛型,可以很好的解决上面的问题。

  1. 使用泛型
 public class GenericMethod
    {
        /// <summary>
        /// <summary>
        /// 泛型方法:方法名称后面加上尖括号,里面是类型参数
        ///           类型参数实际上就是一个类型T声明,方法就可以用这个类型T了
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="tParameter"></param>
        public static void Show<T>(T tParameter)
        {
            Console.WriteLine("This is {0},parameter={1},type={2}",
               typeof(GenericMethod), tParameter.GetType().Name, tParameter);
        }
    }

泛型运行原理

1、为什么泛型可以解决上面的问题呢?
泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候才指定参数类型。 延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列、EF的延迟加载等等。

2、泛型究竟是如何工作的呢?
控制台程序最终会编译成一个exe程序,exe被点击的时候,会经过JIT(即时编译器)的编译,最终生成二进制代码,才能被计算机执行。泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,会做特殊的处理:生成占位符。再次经过JIT编译的时候,会把上面编译生成的占位符替换成具体的数据类型。

泛型是类型安全的

开发一个用于在应用中传递对象的容器。但对象类型并不总是相同。因此,需要开发一个能够存储各种类型对象的容器。

鉴于这种情况,要实现此目标,显然最好的办法是开发一个能够存储和检索 Object 类型本身的容器,然后在将该对象用于各种类型时进行类型转换


public class ObjectContainer {

    private Object obj;

    public Object GetObj() {
        return obj;
    }

    public void SetObj(Object obj) {
        this.obj = obj;
    }
}

虽然这个容器会达到预期效果,但就我们的目的而言,它并不是最合适的解决方案。它不是类型安全的,并且要求在检索封装对象时使用显式类型转换,因此有可能引发异常。

ObjectContainer myObj = new ObjectContainer();

// 存储一个 string
myObj.SetObj("Test");
Console.WriteLine("Value of myObj:" + myObj.GetObj());

// 存储一个 int (自动装箱为一个 Integer 对象)
myObj.setObj(3);
Console.WriteLine("Value of myObj:" + myObj.GetObj());

List objectList = new ArrayList();
objectList.add(myObj);

// 我们必须转换并且必须转换正确的类型以避免 ClassCastException!
string myStr = (string) ((ObjectContainer)objectList.get(0)).GetObj();
Console.WriteLine("myStr: " + myStr);

可以使用泛型开发一个更好的解决方案,在实例化时为所使用的容器分配一个类型,也称泛型类型,这样就可以创建一个对象来存储所分配类型的对象。泛型类型是一种类型参数化的类或接口,这意味着可以通过执行泛型类型调用分配一个类型,将用分配的具体类型替换泛型类型。然后,所分配的类型将用于限制容器内使用的值,这样就无需进行类型转换,还可以在编译时提供更强的类型检查。

public class GenericContainer<T> {
    private T obj;

    public GenericContainer(){
    }

    // 将类型作为参数传递给构造函数
    public GenericContainer(T t){
        obj = t;
    }

    public T getObj() {
        return obj;
    }

    public void setObj(T t) {
        obj = t;
    }
}

最显著的差异是类定义包含 <T>,类字段 obj 不再是 Object 类型,而是泛型类型 T。类定义中的尖括号之间是类型参数部分,介绍类中将要使用的类型参数(或多个参数)。T 是与此类中定义的泛型类型关联的参数。

要使用泛型容器,必须在实例化时使用尖括号表示法指定容器类型。因此,以下代码将实例化一个 int 类型的 GenericContainer,并将其分配给 myInt 字段。
GenericContainer myInt = new GenericContainer();`
如果我们尝试在已经实例化的容器中存储其他类型的对象,代码将无法编译:

myInt.setObj(3);  // OK
myInt.setObj("Int"); // Won't Compile

使用泛型的好处

上面的示例已经演示了使用泛型的一些好处。一个最重要的好处是更强的类型检查,因为避开运行时可能引发的 ClassCastException 可以节省时间。

另一个好处是消除了类型转换,这意味着可以用更少的代码,因为编译器确切知道集合中存储的是何种类型。

泛型的性能

对值类型使用非泛型集合类,在把值类型转换为引用类型,和把引用类型转换为值类型时,需要进行装箱和拆箱操作。

值类型存放在栈上,引用类型存放在堆上。

ArrayList 在使用 add() 的时候,需要装箱,但是在读取ArrayList的值的时候,需要进行拆箱。拆箱和装箱操作很容易,但是性能损失比较大,遍历很多项的时候更是如此。
但是 List 类不使用对象,而是在使用的时候定义类型。在JIT编译器动态生成的类中使用,不在装箱和拆箱操作。

泛型类型参数

在泛型类型或泛型方法的定义中,类型参数是一个占位符(placeholder),通常为一个大写字母,如T。在客户代码声明、实例化该类型的变量时,把T替换为客户代码所指定的数据类型。

代码必须在尖括号内指定一个类型参数,来声明并实例化一个已构造类型(constructed type)。这个特定类的类型参数可以是编译器识别的任何类型。

泛型的默认值:有时候需要给泛型指定为 null,但是不能把 null 赋予泛型类型,因为泛型类型可以被实例化为值类型,而 null 只能用于引用类型,为了解决这个问题,可以使用default关键字,通过关键字,将 null 赋值给引用类型,将 0 赋值给值类型。

使用

泛型类

public class GenericClass<T>
{
    public T _T;
}

class Program
{
    static void Main(string[] args)
    {
        try
        { 
            // T是int类型
            GenericClass<int> genericInt = new GenericClass<int>();
            genericInt._T = 123;
            // T是string类型
            GenericClass<string> genericString = new GenericClass<string>();
            genericString._T = "123";
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        Console.ReadKey();
    }
}

泛型接口

public interface IGenericInterface<T>
{
    //泛型类型的返回值
    T GetT(T t);
}

泛型委托

public delegate void SayHi<T>(T t);//泛型委托

泛型类继承父类,实现接口

如果子类不是泛型的,那么继承的时候必须指定具体类型

/// <summary>
/// 使用泛型的时候必须指定具体类型,这里的具体类型是int
/// </summary>
public class CommonClass : GenericClass<int>
{
}

如果子类也是泛型的,那么继承的时候可以不指定具体类型

/// <summary>
/// 子类也是泛型的,继承的时候可以不指定具体类型
/// </summary>
/// <typeparam name="T"></typeparam>
public class CommonClassChild<T> : GenericClass<T>
{
}

如果子类不是泛型的,那么实现泛型接口的时候必须指定具体类型

/// <summary>
/// 必须指定具体类型
/// </summary>
public class Common : IGenericInterface<string>
{
    public string GetT(string t)
    {
        throw new System.NotImplementedException();
    }
}

如果子类也是泛型的,那么实现泛型接口的时候可以不指定具体类型

/// <summary>
/// 可以不知道具体类型,但是子类也必须是泛型的
/// </summary>
/// <typeparam name="T"></typeparam>
public class CommonChild<T> : IGenericInterface<T>
{
    public T GetT(T t)
    {
        throw new NotImplementedException();
    }
}

类型参数的约束

若要检查表中的一个元素,以确定它是否合法或是否可以与其他元素相比较,那么编译器必须保证:客户代码中可能出现的所有类型参数,都要支持所需调用的操作或方法。这种保证是通过在泛型类的定义中,应用一个或多个约束而得到的。

一个约束类型是一种基类约束,它通知编译器,只有这个类型的对象或从这个类型派生的对象,可被用作类型参数。一旦编译器得到这样的保证,它就允许在泛型类中调用这个类型的方法。上下文关键字where用以实现约束。

约束 描述
where T: struct 类型参数必须为值类型。
where T : class 类型参数必须为引用类型。
where T : new() 类型参数必须有一个公有、无参的构造函数。当于其它约束联合使用时,new()约束必须放在最后。
where T : 类型参数必须是指定的基类型或是派生自指定的基类型。
where T : 类型参数必须是指定的接口或是指定接口的实现。可以指定多个接口约束。接口约束也可以是泛型的。
where T : T2 类型参数 T 必须派生自泛型类型 T2。

只能为默认构造函数定义构造函数约束,不能为其他构造函数定义构造函数约束。

类型参数的约束,增加了可调用的操作和方法的数量。这些操作和方法受约束类型及其派生层次中的类型的支持。因此,设计泛型类或方法时,如果对泛型成员执行任何赋值以外的操作,或者是调用System.Object中所没有的方法,就需要在类型参数上使用约束。

无限制类型参数的一般用法

没有约束的类型参数,如公有类MyClass{...}中的T, 被称为无限制类型参数(unbounded type parameters)。无限制类型参数有以下规则:

  • 不能使用运算符 != 和 == ,因为无法保证具体的类型参数能够支持这些运算符。
  • 它们可以与System.Object相互转换,也可显式地转换成任何接口类型。
  • 可以与null比较。如果一个无限制类型参数与null比较,当此类型参数为值类型时,比较的结果总为false。

无类型约束

当约束是一个泛型类型参数时,它就叫无类型约束(Naked type constraints)。当一个有类型参数成员方法,要把它的参数约束为其所在类的类型参数时,无类型约束很有用。

posted @ 2021-11-16 14:43  x_amos  阅读(70)  评论(0编辑  收藏  举报