因为下面这张图片,逼自己再看一遍泛型,并做下记录,加深印象。翻看.net core源码,发现泛型已经使用的到处都是,所以掌握泛型是成为最基础的知识了。

前奏

泛型

  • 引入时间:.net framework 2.0
  • 引入目的:为了解决装箱、拆箱带来的性能损失。
  • 安全效果:
    1. 装箱拆箱有一个安全转换问题,所以又解决了类型安全问题。
    2. 在父子类或者接口继承的时候,泛型很好的起到了安全约束的作用,如第一张图片。
  • 阳光普照效果: 节约代码量,提高重复利用(object也可以实现,但是有性能损失)。

泛型的协变和逆变

  • 引入时间:.net framework4.0
  • 引入目的:为了解决泛型父子类型的转换问题。
  • 使用规则:
    1. 只能放在接口或者委托的泛型参数前面。
    2. out 协变covariant,用来修饰返回值,儿子可以赋值给老父亲。
    3. in:逆变contravariant,用来修饰传入参数,老父亲可以赋值给儿子。

正文

泛型方法

  定义:泛型方法是通过类型参数声明的方法。在下面的代码中,Method方法名称后面必须紧跟类型占位符T,如果没有,编译器会将方法参数类型T默认识别为正常类型,而不是占位符,所以会报错的。正确样例如下:

 public class Generic
     {
         /// <summary>
         /// 泛型方法
         /// </summary>
         /// <typeparam name="T"></typeparam>
         /// <param name="tParameter"></param>
         public static void Method<T>(T tParameter)
        {
             Console.WriteLine("This is {0},parameter={1},type={2}",
                 typeof(Generic), tParameter.GetType().Name, tParameter.ToString());
         }
    }

泛型类

  定义:泛型类封装不特定于特定数据类型的操作。 泛型类最常见用法是用于链接列表、哈希表、堆栈、队列和树等集合。 无论存储数据的类型如何,添加项和从集合删除项等操作的执行方式基本相同。
  通常,创建泛型类是从现有具体类开始,然后每次逐个将类型更改为类型参数,直到泛化和可用性达到最佳平衡。样例如下:

 public class Generic<T>
     {
        public T t;
    }

泛型接口

跟泛型类定义一样,只要将class改为interface。样例如下:

 public interface Generic<T>
     {
       T GetT(T t);
    }

泛型委托

跟方法一样,同样是约束方法参数类型。样例如下:

  public delegate void SayHi<T>(T t);

default 关键字

在泛型类和泛型方法中会出现的一个问题是,如何把缺省值赋给参数化类型,此时无法预先知道以下两点:

  1. T将是值类型还是引用类型
  2. 如果T是值类型,那么T将是数值还是结构

对于一个参数化类型T的变量t,仅当T是引用类型时,t = null语句才是合法的; t = 0只对数值的有效,而对结构则不行。这个问题的解决办法是用default关键字,它对引用类型返回空,对值类型的数值型返回零。而对于结构,它将返回结构每个成员,并根据成员是值类型还是引用类型,返回零或空。如下所示:

public class MyList<T>
{
    //...
        public T GetNext()
        {
            T temp = default(T);
            if (current != null)
            {
                temp = current.Data;
                current = current.Next;
            }
            return temp;
        }
}

使用注意点(重点)

注意点1

泛型在声明的时候可以不指定具体的类型,但是在使用的时候必须指定具体类型;
如果子类也是泛型的,那么继承的时候可以不指定具体类型

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

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

    }
}

注意点2

类实现泛型接口也是这种情况

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

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

泛型约束

  为什么要泛型约束,其主要问题还是解决安全问题,规范开发人员写代码的规范性,避免一些在运行时期才能检查到的错误。比如下面的代码,在编译器是不会报错的,但是在运行期会出现转换异常。

public class 动物
{

}
 public class 狗 :动物
{
}


public class 猫
{

}
 

 public class Generic
    {
        public void method(object cat)
        {
            动物 animal= (动物)cat;
        }
    }

  所谓的泛型约束,实际上就是约束的类型T。使T必须遵循一定的规则。比如T必须继承自某个类,或者T必须实现某个接口等等。那么怎么给泛型指定约束?其实也很简单,只需要where关键字,加上约束的条件。

序号 约束 说明
1 T:struct 类型参数必须是值类型
2 T:class 类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。
3 T:new() 类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。
4 T:基类名 类型参数必须是指定的基类或派生自指定的基类。
5 T:接口名称 类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。
6 T:基类名,接口名称,new() 泛型约束也可以同时约束多个,但是new()必须放在最后

协变和逆变

在OO的世界里,可以安全地把子类的引用赋给父类引用。但是在T的世界里,就不一定了。有的能变,有的不能变,先了解以下几点:

  • 以前的泛型系统(或者说没有in/out关键字时),是不能“变”的,无论是“逆”还是“顺(协)”。
  • 当前仅支持接口和委托的逆变与协变 ,不支持类和方法。但数组也有协变性。
  • 值类型不参与逆变与协变。

如果不能理解以上几句话,就先看下面的知识点

协变

  所谓协变,就是为了解决子类泛型接口或委托能回到父类泛型接口或委托上来。(Foo = Foo )

//泛型委托:
public delegate T MyFuncA<T>();//不支持逆变与协变
public delegate T MyFuncB<out T>();//支持协变
 
MyFuncA<object> funcAObject = null;
MyFuncA<string> funcAString = null;
MyFuncB<object> funcBObject = null;
MyFuncB<string> funcBString = null;
MyFuncB<int> funcBInt = null;
 
funcAObject = funcAString;//编译失败,MyFuncA不支持逆变与协变
funcBObject = funcBString;//变了,协变
funcBObject = funcBInt;//编译失败,值类型不参与协变或逆变
 
//泛型接口
public interface IFlyA<T> { }//不支持逆变与协变
public interface IFlyB<out T> { }//支持协变
 
IFlyA<object> flyAObject = null;
IFlyA<string> flyAString = null;
IFlyB<object> flyBObject = null;
IFlyB<string> flyBString = null;
IFlyB<int> flyBInt = null;
 
flyAObject = flyAString;//编译失败,IFlyA不支持逆变与协变
flyBObject = flyBString;//变了,协变
flyBObject = flyBInt;//编译失败,值类型不参与协变或逆变
 
//数组:
string[] strings = new string[] { "string" };
object[] objects = strings;

逆变

  所谓协变,就是为了解决父类泛型接口或委托能回到子类泛型接口或委托上来。(Foo = Foo)

public delegate void MyActionA<T>(T param);//不支持逆变与协变
public delegate void MyActionB<in T>(T param);//支持逆变
 
public interface IPlayA<T> { }//不支持逆变与协变
public interface IPlayB<in T> { }//支持逆变
 
MyActionA<object> actionAObject = null;
MyActionA<string> actionAString = null;
MyActionB<object> actionBObject = null;
MyActionB<string> actionBString = null;
actionAString = actionAObject;//MyActionA不支持逆变与协变,编译失败
actionBString = actionBObject;//变了,逆变
 
IPlayA<object> playAObject = null;
IPlayA<string> playAString = null;
IPlayB<object> playBObject = null;
IPlayB<string> playBString = null;
playAString = playAObject;//IPlayA不支持逆变与协变,编译失败
playBString = playBObject;//变了,逆变

注意

in/out是什么意思呢?为什么加了它们就有了“变”的能力,是不是我们定义泛型委托或者接口都应该添加它们呢?
原来,在泛型参数上添加了in关键字作为泛型修饰符的话,那么那个泛型参数就只能用作方法的输入参数,或者只写属性的参数,不能作为方法返回值等,总之就是只能是“入”,不能出。out关键字反之。

泛型的本质

为什么泛型可以解决的各种类型问题呢?

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

泛型究竟是如何工作的呢?

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

1 Console.WriteLine(typeof(List<>));
2 Console.WriteLine(typeof(Dictionary<,>));

从上面的截图中可以看出:泛型在编译之后会生成占位符。
其次,通过测试发现泛型方法的性能与普通方法相等,object方法的性能最低。

总结

  泛型已经发展了很多年,现在流行DDD开发方式,使用泛型解决各种问题也越来越多,以上总结只是个人翻看别人博客,然后学习留下记录。如果要查看更多信息,可以浏览更多链接。如有帮助,点个赞。
  撸完泛型,接下来还要重新回顾撸两个东西

  1. 撸·委托
  2. 撸·反射

引用

  1. 逆变与协变详解
  2. c#泛型详解

版权声明:本文为zhan520g原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/zhan520g/p/10397117.html