.NET开发技术栈: 泛型

什么是泛型

C#是一门强类型的静态语言,所有变量的数据类型都是在编译时确定的。我们可以在编写类或方法等元素时先不确定类型,而是用类型占位符T(T可以为任意其他名字,比如K, Ts,TP)代替, 再真正调用时再把类型传进来。这样能使我们构建的类, 方法等元素更具通用型,

避免为每个数据类型写一个特定的方法。

泛型的基本使用方法

假设有一个方法,接收一个参数, 然后在控制台打印出来,并返回传入的参数,  代码如下:

        public string PrintMsg(string msg)
        {
            Console.WriteLine(msg);
       return msg; }

 

此处参数的类型为string, 如果我们要讲参数类改为 int , DateTime,  decimal时,我们需要重新定义三个方法

        public int PrintMsg(int msg)
        {
            Console.WriteLine(msg);
            return msg;
        }

        public DateTime PrintMsg(DateTime msg)
        {
            Console.WriteLine(msg);
            return msg;
        }

        public decimal PrintMsg(decimal msg)
        {
            Console.WriteLine(msg);
            return msg;
        }

 

这些方法的内部实现逻辑完全一样, 有所不同的只是参数和返回值的数据类型, 这时候泛型就可以上场了,改为泛型实现如下

        public T PrintMsg<T>(T msg)
        {
            Console.WriteLine(msg);
            return msg;
        }

 

调用时,我们只需把想要的类型传进去就可以了

        public void Test()
        {
            string msg = "泛型如此简单";
            PrintMsg<string>(msg);
        }

 

程序执行时,方法内的T将被确定下来, 此处为string, 就相当于执行了这个代码

        public string PrintMsg(string msg)
        {
            Console.WriteLine(msg);
        return msg;
        }

若传入int, DateTime, decimal ,其他类型,包括自定义类, 也是一样高的效果。

 

.NET中的泛型原理

泛型的类型是在什么时候确定下来的呢, 这里就得提一下.NET的运行机制了,我们编写完的c#代码经过编译, 会生成exe或dll文件, 这些文件中包含的是中间语言IL, 在运行程序时,IL会被JIT编译, 生成cpu真正认识的二进制机器码,大致如下图

泛型中的类型就是在JIT编译的这一步被确定下来的,真正运行时JIT会根据传入的类型参数,生成一个此类型参数的普通类或方法。

因此泛型方法和普通方法在执行效率上几乎是一样的。

也因为这一特点,产生了泛型缓存这一概念。

 

泛型缓存

 

   class Cache<T>
    {
        private static T cache_value;

        public static object Get()
        {
            return cache_value;
        }

        public static void Set(T value)
        {
            cache_value = value;
        }
    }

 

以上就是一个最简单的泛型缓存类,传入不同的类型参数,在运行时会生成与之对应的不同副本,也就是说,每个类型一个缓存类,这样就能很好地缓存不同类型的值。

 

 

泛型约束

 Cat, Dog 两个类继承自Animal类,代码如下

 public abstract class Animal 
    {
        public string name { get; set; }
        public abstract void sleep();
    }

    public class Cat : Animal
    {
        public override void sleep()
        {
            Console.WriteLine($"{name} sleep in day");
        }
    }

    public class Dog : Animal
    {
        public override void sleep()
        {
            Console.WriteLine($"{name} sleep in night");
        }
    }


PetShop是一个泛型类, where T:Animal的意思是: 传入的类型参数必须继承自Animal类

   public class PetShop<T> where T : Animal
    {
        public void ShowSleepTime (T animal)
        {
            animal.sleep();
        }
    }

使用泛型约束后,可以访问到约束类型的方法或字段, 比如上面代码中的animal.sleep() 访问的就是类Animal中的方法。

泛型约束一共有以下六种类

    • where T : struct  结构约束, 类型T必须是值类型
    • where T : class   类约束, 类型T必须是引用类型
    • where T : new()  构造函数约束,类型T必须有一个默认的构造函数
    • where T : IFoo        接口约束, 类型T必须实现接口IFoo
    • where T : Foo         类约束, 类型T必须继承基类Foo
    • where T1 : T2         类型T1必须继承泛型T2

泛型的种类

  • 泛型类
  • 泛型方法
  • 泛型接口
  • 泛型委托
  • 泛型集合

 

逆变协变

 

 Animal cat = new Cat();

一只猫是一只动物,非常正确!

 List<Animal> cats = new List<Cat>();

一组猫是一组动物, 按理说也是非常正确, 但此处编译器会报错: 无法将类型List<Cat> 转换为 List<Animal>。

究其原因,是因为List<Cat> 和 List<Animal> 这两个类并没有继承关系,这么写当然是错误的。

 

我们自己定义个接口, 然后写一个泛型类继承此接口 (这里只是用于演示泛型的协变逆变,未实现通常List所拥有的方法)

 public interface IMyList<T>
    {
        T Get();
        void Show(T t);
    }

    public class MyList<T> : IMyList<T>
    {
        public T Get()
        {
            return default(T);
        }

        public void Show(T t)
        {
            //show object t
        }
    }

现在我们是否可以这么写

 IMyList<Animal> cats = new MyList<Cat>();

MyLsit继承自IMyList, Cat继承自Animal, 看上去似乎已经天衣无缝, 但是编译器还是报错。

解决问题

协变:

IMyList的类型参数加out关键字

 

  public interface IMyList<out T>
    {
        T Get();
      //  void Show(T t);
    }

 

加上out关键字后

 IMyList<Animal> cats = new MyList<Cat>();

这个代码不报错了, 但是这里的 T 只能作返回值, 不能作参数,

逆变:

IMyList的类型参数加in关键字

 

  public interface IMyList<in T>
    {
       // T Get();
          void Show(T t);
    }

 加上in关键字后

 IMyList<Cat> cats = new MyList<Animal>();

这个代码是不会报错的,编译通过。

为什么协变out后类型参数不能作为参数传入, 逆变in后参数类型不能作为返回值返回, 可以结合代码,自行思考其逻辑可行性。

协变逆变小结:

协变和逆变只有泛型接口和泛型委托有 ,用out和in关键字约束类型参数是作为返回值还是参数。

协变用out修饰,类型参数只能作返回值, 可以实现 IMyList<Animal> animal = new MyList<Cat>() 这种用子类替换父类的情况。

逆变用in修饰,类型参数只能作参数, 可以实现 IMyList<Cat> animal = new MyList<Animal>() 这种用父类替换子类的情况。

 

posted @ 2020-08-04 22:43  我是张志淼  阅读(209)  评论(0)    收藏  举报