zoukankan      html  css  js  c++  java
  • .NET进阶篇-语言章-1-Generic泛型深入

    内容目录

    一、概述二、泛型的好处三、泛型使用1、泛型方法2、泛型类、泛型接口四、泛型的功能1、泛型中的默认值2、约束3、协变逆变5、泛型委托4、泛型缓存五、总结

    一、概述

    泛型我们一定都用过,最常见的List<T>集合。.NET2.0开始支持泛型,创建的目的就是为了不同类型创建相同的方法或类,也包括接口,委托的泛型。比如常见的ORM映射,一个方法通过传入不同的类,返回不同的类实例,再调用时才确定参数类型。

    我们知道想要一个类相同名称的方法,如果仅参数类型不同,那么要重载。重载会有很多冗余的代码。在.NET1.0时代也可以不用重载,那就是参数类型直接用Object类型,那么任何类型都能传进去了,但是会有装箱拆箱操作,影响性能。

    public static void Show(string sValue)
    {
        Console.WriteLine(sValue);
    }

    public static void Show(int iValue)
    {
        Console.WriteLine(iValue);
    }

    public static void Show(object oValue)
    {
        Console.WriteLine(oValue);
    }

    二、泛型的好处

    值类型和引用类型的装箱拆箱消耗。值类型分配在线程栈上,引用类型分配在堆上,只把指针放在栈上。如图所示,如果把int类型1装箱,就要把1拷贝到堆中,就会有内存的交换。以前的ArrayList就是类型不安全的,需要频繁的进行装拆箱操作,Add元素的时候全部装箱object,取的时候要拆箱,性能损失比较大。

    泛型的效率等同于硬编码的方式,就是和你很多功能相同的类效率差不多。泛型每个类型只实例化一次,下面泛型缓存会详细解读下。先简单介绍下CLR的运行原理(详细在CLR章节)以了解泛型的原理机制。

    .NET编译器和解释器两阶段,我们先经过编译器编译成IL中间语言(dll、exe),和java的字节码类似,然后经过JIT解释成机器码。这样做的好处就是我们只需要编译成IL后,在各个不同计算机系统上,只要有对应的CLR(JIT)就行,这样就和平台无关。二次编译:为了一次编译,不同平台使用。泛型在第一个编译时会用一个占位符代替,在第二次运行时会编译成具体的类型。所以性能相当于硬编码的方式,每种类型最终都有自己的机器码。

    List<T>是在使用时定义类型,JIT编译器解析时动态的生成,如定义List<int>,在JIT运行时就声称List<int>类型,然后操作就不会出现装箱拆箱,而且只能添加指定的类型,这就类型安全

    三、泛型使用

    1、泛型方法

    常见的泛型方法就是在方法后面带上<T>(T param),“T”可以随便定义,只要不是关键保留字就行,默认约定俗成都用T,此处就代表你定义了一个T类,然后后面参数就可以用这个T类型。(如果把鼠标光标放在参数类型T上,然后F12转到定义就会定位到前面这个T。)这样就可以用一个方法,满足不同的参数类型,去做相同的事情。把参数的类型申明推迟到调用时,延迟声明。后面框架中也会有很多这种延迟思想,延迟以达到更好的扩展。

    public static void Show<T>(T tValue)
    {
        Console.WriteLine(tValue);
    }
    CommonMethod.Show<int>(123);

    2、泛型类、泛型接口

    创建方法类似,语法一样<T>。用的最多的List<T>就是很典型的泛型类,用来满足不同的具体类型,完成相同的事情

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

    public interface IGenericInterface<T>
    {
        GetT();
    }

    四、泛型的功能

    1、泛型中的默认值

    既然用了泛型,那么在内部想要初始化怎么办呢?因为泛型进来的类型不一定是值类型或引用类型,所以初始化就不能简单直接赋null。这个时候需要用到default关键字,用于将泛型类型初始化为null或其他值类型默认值(0,0001/1/1 0:00:00日期等);

    2、约束

    泛型导致任何类型都可以进来,那么如何去使用这个类型T,编写的时候我们是不知道T是什么,也不知道它能干什么。一个方法就是可以用反射,任何一个类型通过发射都能获取内部的结构属性方法调用。泛型约束提供更简便的方法。在声明泛型时在参数后面追加where关键字。约束可以同时指定多个,像这样where:T People,IWork,new()。同时约束传进来的类型People或其子类,并且继承了IWork接口,有无参数构造函数。

    public static void Show<T>(T tValue) where T : People
    {
        Console.WriteLine(tValue.Name);
    }

    3、协变逆变

    协变逆变就是对参数和返回值的类型进行转换。协变用一个派生更大的类去代替某个类型(小代替大),其实就是设计原则的里氏替换原则,比如狗继承自动物,那么任何用动物作为参数类型的地方,调用时都可以用狗代替。逆变就是反过来。

    //协变
    public void ShowName(Animal animal)
    {
    }

    ShowName(dog);

    泛型接口的协变逆变。如果泛型类型用了out关键字标注,泛型接口就是协变的。这也意味着返回类型只能是T。如果用了in关键字标注,就是逆变,只能把泛型类型T用作方法的输入。这块很绕,实际使用非常少。

    //一堆狗肯定是一堆动物啊,为啥就不能这么做呢?下面这句编译不通过
    //前后两个类型是没有父子关系的
    List<Animal> animalLst = new List<Dog>();
    //下面这句就可以呢?
    IEnumerable<Animal> animalLst2 = new List<Dog>();

    //因为在接口中添加了out关键字
    public interface IEnumerable<out T> : IEnumerable
    {
        //
        // 摘要:
        //     Returns an enumerator that iterates through the collection.
        //
        // 返回结果:
        //     An enumerator that can be used to iterate through the collection.
        IEnumerator<T> GetEnumerator();
    }

    ICustomListIn<Dog> customLstIn = new CustomListIn<Animal>();

    public interface ICustomListIn<in T>
    {
        void Show(T t);
    }

    public class CustomListIn<T> : ICustomListIn<T>
    {
        public void Show(T t)
        {
            Console.WriteLine(typeof(T).FullName);
        }
    }

    interface ISetData<in T>  //使用逆变
    {
        void SetData(T data);
    }

    interface IGetData<out T>   //使用协变
    {
        T GetData();
    }

    class MyTest<T> : ISetData<T>, IGetData<T>//继承两个泛型接口
    {
        private T data;
        public void SetData(T data)
        {
            this.data = data;   //赋值
        }
        public T GetData()
        {
            return this.data;   //取数据
        }
    }

    MyTest<object> my = new MyTest<object>();
    ISetData<string> set = my;
    set.SetData("nihao");

    其实协变逆变就是语法糖,为了让不是继承关系的类型也可以互相赋值编译通过。运行时实际右边是什么类型就是什么类型。(欺骗编译器,自己应该会很少写协变逆变的接口或委托)

    5、泛型委托

    以Action为例,Action是.NET Framework内置的泛型委托,可以使用Action委托以参数形式传递方法,而不用显示声明自定义的委托。其实我们撸代码过程中不太需要自己定义委托,内置的Action和Func就够用,也便于统一。Action无返回值委托,可以有16个参数,可以传入不同的类型。在委托事件一章会详细介绍。

    4、泛型缓存

    泛型类的静态成员只能在类的一个实例中共享。运行时泛型类的实例已经指定了具体类型,每一个不同的泛型类实例共享静态成员,利用这个特点就可以做缓存。每一个不同的T缓存一个版本数据。如例子所示,当第一次指定不同的T时,会重新构造,再次有相同的类型时,就不会进入静态构造函数了。相当于为缓存了多个版本的静态成员。比如在各个数据库实体类需要有一些增删改查的SQL时,就可以利用用泛型特性,每一个数据库实体类都会缓存一份自己的增删改查SQL。

    public class GenericCache<T>
    {

        static GenericCache()
        
    {
            Console.WriteLine("进入静态构造函数");
            _TypeTime = $"{typeof(T).FullName}_{DateTime.Now.ToString()}";
        }

        private static string _TypeTime = "";

        public static string GetCache()
        
    {
            return _TypeTime;
        }
    }

    Console.WriteLine("************************");
    Console.WriteLine(GenericCache<int>.GetCache());
    Thread.Sleep(1000);
    Console.WriteLine(GenericCache<string>.GetCache());
    Thread.Sleep(1000);
    Console.WriteLine("认真比较打印出的静态成员值");
    Console.WriteLine(GenericCache<int>.GetCache());
    Thread.Sleep(1000);
    Console.WriteLine(GenericCache<string>.GetCache());
    Console.WriteLine("************************");

    五、总结

    通过泛型类可以创建独立于类型的类,泛型方法创建出独立于类型的方法。接口、结构、委托也可以用泛型的方式创建。建议如果我们需要设计和类型无关的对象时,可以使用泛型,把锅甩给调用方,由上端决定实例化具体什么类型。

    如果手机在手边,也可以关注下vx:xishaobb,互动或获取更多消息。当然这里也一直更新de。

  • 相关阅读:
    第十三章 部署Java应用程序
    分布式系列五: RMI通信
    分布式系列四: HTTP及HTTPS协议
    分布式系列三: 对象序列化
    程序中的 “负数取模” 问题
    【转】Linux C函数库参考
    【转】 Linux中记录终端输出到txt文本文件
    【转】 #define用法详解
    error: ‘to_string’ was not declared in this scope
    exit() 与 return() 的区别
  • 原文地址:https://www.cnblogs.com/xibei/p/11634619.html
Copyright © 2011-2022 走看看