zoukankan      html  css  js  c++  java
  • C#基本功之泛型

    一、没有泛型之前

    在没有泛型之前,我们是怎么处理不同类型的相同操作的:

    示例1
     //下面是一个处理string类型的集合类型
     public class MyStringList
        {
            string[] _list;
            public void Add(string x)
            {
                //将x添加到_list中,省略实现
            }
            public string this[int index]
            {
                get { return _list[index]; }
            }
        }
     //调用
     MyStringList myStringList = new MyStringList();
     myStringList.Add("abc");
     var str = myStringList[0];
    
    示例2
        //如果我们需要处理int类型就需要复制粘贴然后把string类型替换为int类型:
        public class MyIntList
        {
            int [] _list;
            public void Add(int x)
            {
                //将x添加到_list中,省略实现
            }
            public int this[int index]
            {
                get { return _list[index]; }
            }
        }
       //调用
        MyIntList myIntList = new MyIntList();
        myIntList.Add(100);
        var num = myIntList[0];
    

    可以看得出我们的代码大部分是重复的,而作为有追求的程序员是不允许发生这样的事情的。
    于是乎,我们做了如下改变:

    示例3
     public class MyObjList
        {
            object[] _list;
            public void Add(object x)
            {
                //将x添加到_list中,省略实现
            }
            public object this[int index]
            {
                get { return _list[index]; }
            }
        }
     //调用
     MyObjList myObjList = new MyObjList();
    myObjList.Add(100);
     var num = (int)myObjList[0];
    

    从上面这三段代码中,我们可以看出一些问题:

    1. int和string集合类型的代码大量重复(维护难度大)。
    2. object集合类型发生了装箱和拆箱(损耗性能)。
    3. object集合类型是存在安全隐患的(类型不安全)。

    问题1,虽然代码重复但是没有装箱、拆箱而且类型是安全的
    问题2,发生了装箱和拆箱,是损耗性能影响执行效率的。
    问题3,如果add的类型不是int类型,在编译器是不会检查出来的(编译通过),运行期就会报错,MyObjList类似于我们熟知的ArrayList

    运行期报错

    现在,我们必须解决如下问题
    1、避免代码重复
    2、避免装箱和拆箱
    3、保证类型安全

    范型为我们提供了完美的解决方案

    二、什么是泛型

    如果你理解类是对象的模板(类是具有相同属性和行为的对象的抽象),那么泛型就很好理解了。
    泛型:generic paradigm(通用的范式),generic这个单词也很好的说明了模板这个概念:通用的,标准的。
    泛型是类型的模板
    不同的是:作为模板的类是通过实例化产生不同的对象,而泛型是通过不同的类型实参产生不同的类型
    泛型的基本概念介绍完,我们来看看泛型到底是怎么帮我们解决问题的

    如何解决代码重复:提取代码相同的部分,封装变换的部分——封装变化,而示例1和示例2中变换的部分就是int和string类型本身,如何将类型抽象呢

    示例4
        //将示例3改装下
        public class MyList<T>
        {
            T [] _list;
            public void Add(T x)
            {
                //将x添加到_list中,省略实现
            }
            public T this[int index]
            {
                get { return _list[index]; }
            }
        }
    

    类型参数 T
    类型参数可以理解为泛型的"形参"("形参"一般用来形容方法的),有“形参”就会有实参。如我们声明的List,string就是实参;List ,int就是实参,而List和List是两种不同的类型。

    不同的类型
    通过类型参数解决了代码重复的问题

    如何解决装箱、拆箱以及类型安全的问题:

     //示例5
           List<int> list = new List<int>();
           list.Add(100);//强类型无需装箱
           //list.Add("ABC"); 编译期安全检查报错
           int num = list[0];//无需拆箱   
    

    编译期安全检查报错

    声明泛型类型时,因为确定了类型实参,所以操作泛型类型不需要装箱、拆箱,而且泛型将大量安全检查从运行时转移到了编译时进行,保证了类型安全。
    注:C#为我们提供了5种泛型:类、结构、接口、委托和方法。

    在示例4中,自定义泛型集合只是添加和获取类型参数的实例,除此之外,没有对类型参数实例的成员做任何操作。C#的所有类型都继承自Object类型,也就是说,我们目前只能操作Object中的成员(Equals,GetType,ToString等)。但是,我自定义的泛型很多时候是需要操作类型更多的成员

    新需求,打印员工的信息

    示例6
        public class Person
        {
            public string Name { get; set; }
            public int Age{ get; set; }
        }
        public class Employee : Person {  }
        public class PrintEmployeeInfo<T>
        {
            public void Print(T t)
            {
                Console.WriteLine(t.Name);//报错
            }
        }
    

    示例6:T未包含“Name”的定义

    如果我们可以将类型参数T限定为Person类型,那么在泛型内部就可以操作Person类型的成员了。

    三、泛型的约束

    表格来至微软官方文档

    约束 描述
    where T:结构 类型参数必须是值类型。 可以指定除 Nullable 以外的任何值类型。
    where T:类 类型参数必须是引用类型;这同样适用于所有类、接口、委托或数组类型。
    where T:new() 类型参数必须具有公共无参数构造函数。 与其他约束一起使用时,new() 约束必须最后指定。
    where T:<基类名称> 类型参数必须是指定的基类或派生自指定的基类。
    where T:<接口名称> 类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。
    where T:U 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。
    示例7
     public class PrintEmployeeInfo<T> where T:Person
        {
            public void Print(T t)
            {
                Console.WriteLine(t.Name);
            }
        }
    

    四、协变和逆变很简单

    有一定工作经验的开发人员一定遇到过下面这样的情况:

    示例8
     List<Employee> list = new List<Employee>();
     list.Add(new Employee() { Age = 20, Name = "小明" });
     IEnumerable<Person> perList;
     perList = list;
     foreach (var item in perList)
     {
         Console.WriteLine("名字:" + item.Name + ",年龄:" + item.Age);
     }
    

    不是说,不同类型实参构造的泛型也是不同的吗,为啥可以将List对象赋值给IEnumerable呢?
    再看下面的示例

    示例9
      public static void PrintEmployee(Person item)
      {
          Console.WriteLine("名字:" + item.Name + ",年龄:" + item.Age);
      }
    
      Action<Employee> empAction = PrintEmployee;
      empAction(new Employee() { Age = 20, Name = "小明" });
    
      Action<Person> perAction = PrintEmployee;
      perAction(new Employee() { Age = 20, Name = "小明" });
    

    执行结果正常输出

    正常输出
    为什么可以将参数类型为Person的方法分别赋值给Action和Action呢?
    示例8说明了泛型的协变性,示例9说明了泛型的逆变性(听起来很唬人)
    其实协变和逆变只要弄清楚两个概念一切就非常清晰了

    1. 类型参数分为输入参数(in)、输出参数(out)和不变参数(没有关键字)
    2. 设计原则:里氏替换原则——派生类(子类)对象能够替换其基类(超类)对象被使用

    IEnumerable的定义
    public interface IEnumerable<out T> : IEnumerable
    示例8中IEnumerable输出参数类型需要的Person类型,而List类型参数给的是Employee(Employee继承了Person)——里氏替换原则
    委托Action的定义
    public delegate void Action<in T>(T obj);
    示例9中方法PrintEmployee需要的参数类型是Person,而Action输入类型参数是Employee(Employee继承了Person)——里氏替换原则
    如果将PrintEmployee的参数类型变为Employee,示例9中其他代码不变,会怎样?

    编译错误
    清楚的错误信息

    方法PrintEmployee需要的参数类型是Employee,而Action的输入参数是Person,显然Person不一定是Employee

    注:in和out关键字只适用于接口和委托类型

  • 相关阅读:
    初级算法梳理 -【任务1 线性回归算法梳理】
    【转】netstat 查看端口占用情况
    【转】Linux多命令顺序执行连接符(; || && |)
    【摘】程序员保持竞争力方法
    【整理】python中re的match、search、findall、finditer区别
    【转】怎样理解阻塞非阻塞与同步异步的区别?
    [笔记]Docker解决了什么问题?
    【笔记】第六章、Linux 的文件权限与目录配置
    [整理]Python程序员面试前需要看的博客(持续整理)
    【整理】知乎回答:为什么计算机语言中的变量名都不能够以数字为开头呢?
  • 原文地址:https://www.cnblogs.com/codinggao/p/7742714.html
Copyright © 2011-2022 走看看