欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

第一课《.net之--泛型》

程序员文章站 2022-04-28 12:20:15
今天我来学习泛型,泛型是编程入门学习的基础类型,从.net诞生2.0开始就出现了泛型,今天我们开始学习泛型的语法和使用。 什么是泛型? 泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性。泛型为.NET框架引入了类型参数(type parameters)的概念。类型参数使得 ......

 

  今天我来学习泛型,泛型是编程入门学习的基础类型,从.net诞生2.0开始就出现了泛型,今天我们开始学习泛型的语法和使用。

  什么是泛型?

  泛型(generic)是c#语言2.0和通用语言运行时(clr)的一个新特性。泛型为.net框架引入了类型参数(type parameters)的概念。类型参数使得设计类和方法时,不必确定一个或多个具体参数,其的具体参数可延迟到客户代码中声明、实现。这意味着使用泛型的类型参数t,写一个类mylist<t>,客户代码可以这样调用:mylist<int>, mylist<string>或 mylist<myclass>。这避免了运行时类型转换或装箱操作的代价和风险。

  上面是官方腔调,我说人话:泛型就是为了满足不同类型,相同代码的重用!

 

  为什么要有泛型?

  下面我们举例子来讲解为什么会要泛型,以下我列举了三个例子来讲解:

   我们列举了showint,showstring,showdatatime三个方法,如果我们在程序中每封装一个方法有不同参数,就要像下面这样写一个函数的话,代码会变得很累赘,在调用的时候必须传递吻合的参数,所以不得不写出了3个方法,那有没有好的方法来解决这样的问题呢?答案是当然有,微软是很聪明的,倾听小满哥慢慢来讲。

        /// <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);
        }
     
     定义一些测试变量

      int ivalue = 123;
      string svalue = "456";
      datetime dtvalue = datetime.now;
      object ovalue = "789";

      普通方式调用演示
      showint(ivalue);
      showint(svalue);//这样不行类型必须吻合
      showstring(svalue);
      showdatetime(dtvalue);

  在.net 1.0的时候微软出现了object这个概念,下面有一个方法showobject,你们就会发现不管参数是int srtring datetime 我们都可以调用showobject来操作实现,那为什么会这样呢?

  1:object类型是一切类型的父类。

  2:任何父类出现的地方,都可以用子类来代替。

出现这个基本满足了开发人员的一些需求,在.net1.0和1.1的时候,这个时候还没有泛型就用object代替。

        /// <summary>
        /// 打印个object值
        /// 1 object类型是一切类型的父类
        /// 2 任何父类出现的地方,都可以用子类来代替
        /// .net framework 1.0 1.1
        /// </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);
        } 
     定义一些测试变量

      int ivalue = 123;
      string svalue = "456";
      datetime dtvalue = datetime.now;
      object ovalue = "789";

     object方式调用演示

      showobject(ovalue);
      showobject(ivalue);
      showobject(svalue);
      showobject(dtvalue);


 

  接着吹牛比,劳动人民的智慧还是很屌的,经过之前的经历在.net2.0的时候,微软让主角登场了"泛型",当然同时出现的还有“线程池”这个我们先不讲,回到正轨什么是泛型?你们在开发的时候有没有用过list<t>这个集合?这个就是泛型,深化下直接举个栗子吧,下面我写一个例子show<t>(t tparameter)看下泛型的写法:

  有个毛用?下面这个方法也可以向上面showobject一样,你们会发现不管参数是int srtring datetime 我们也可以调用show<t>(t tparameter)来操作实现,替换了showobject这个方法的实现,具备了满足了不同参数类型的方法实现,更适用性,泛型方法声明的时候,没有指定类型,而是调用的时候指定,具有延迟声明和延迟思想,这个思想对我们在开发框架的时候灰常有用,不得不说老外这点还是很几把厉害(还是我们被*了?也许吧,我相信等中文编程语言出来估计会更屌,中华文化博大精深嘛),现在小伙伴们是不是大概了解泛型的基础作用了?

        /// <summary>
        /// 泛型方法声明的时候,没有指定类型,而是调用的时候指定
        /// 延迟声明:把参数类型的声明推迟到调用
        /// 延迟思想:推迟一切可以推迟的
        /// 
        /// 2.0泛型不是语法糖,而是由框架升级提供的功能
        /// 泛型方法性能上和普通方法差不多的/// </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.tostring());
        }

     调用演示

      show<object>(ovalue);
      show<int>(ivalue);
      show(ivalue);//可以去掉,自动推算
      show<string>(ivalue);//必须匹配
      show<string>(svalue);
      show<datetime>(dtvalue);

    那问题来了,既然都可以实现为什么要用这个呢?我们做事凡事都要带着疑问去看待,有些事别人说好,但真的好不好我们要自己亲自试试才知道,我们最关注的的效率问题,下面是测试代码:

     public static void show()
        {
            console.writeline("****************monitor******************");
            {
                int ivalue = 12345;
                long commonsecond = 0;
                long objectsecond = 0;
                long genericsecond = 0;

                {
                    stopwatch watch = new stopwatch();
                    watch.start();
                    for (int i = 0; i < 100000000; i++)
                    {
                        showint(ivalue);
                    }
                    watch.stop();
                    commonsecond = watch.elapsedmilliseconds;
                }
                {
                    stopwatch watch = new stopwatch();
                    watch.start();
                    for (int i = 0; i < 100000000; i++)
                    {
                        showobject(ivalue);
                    }
                    watch.stop();
                    objectsecond = watch.elapsedmilliseconds;
                }
                {
                    stopwatch watch = new stopwatch();
                    watch.start();
                    for (int i = 0; i < 100000000; i++)
                    {
                        show<int>(ivalue);
                    }
                    watch.stop();
                    genericsecond = watch.elapsedmilliseconds;
                }
                console.writeline("commonsecond={0},objectsecond={1},genericsecond={2}"
                    , commonsecond, objectsecond, genericsecond);
            }
        }

  结果如下:commonsecond=508,objectsecond=916,genericsecond=452   你会发现普通方法508ms,object方法916ms,泛型是452ms,其中object最慢,为什么最慢呢?因为object会经过一个装箱拆箱的过程,所以性能上会损失一些,但是在我看来这样上亿次这点损耗,算不了什么,但是可以证明泛型和普通类型速度是差不多的,这一点可以认可泛型还是性能挺好的,这个可以推荐使用泛型的理由之一。

  但泛型仅仅表现在这个层面吗?远远不止,我们用泛型远远不是为了提升刚刚那点性能,为什么要用泛型?答案来了,我们要用泛型就是为了满足不同类型,相同代码的重用,下面我继续举栗子:

   泛型的一些用法,泛型只有四种用途,泛型类,泛型接口,泛型委托,泛型方法,如下:

    public class genericclass<t>
    {
        public t property { get; set; }
    }

   public interface igenericinterface<t>
     {

     }

   public delegate void donothing<t>();

    调用演示

    list<int> intlist = new list<int>();//原生态list类
    list<string> stringlist = new list<string>();

    genericclass<int> igenericclass = new genericclass<int>();
    igenericclass.property = 1;

    genericclass<string> sgenericclass = new genericclass<string>();
    sgenericclass.property = "1233";

    donothing<int> method = new donothing<int>(() => { });

    还有一种,别被吓到:t,s,xiaomange这些语法,只是占位符别怕,你可以自己定义的,在你调用的时候确定类型就ok了,好了差不多能理解泛型了吧?再说一次泛型就是为了满足不同类型,相同代码的重用。

    public class childclassgeneric<t, s, xiaomange> : genericclass<t>, igenericinterface<s>
    {

    }

  接下来我们来聊一聊拓展的一部分,好像泛型很吊的样子感觉什么都能用泛型类型代替,但是天下哪有那么好的事情,双刃剑的道理都懂,所以出现了泛型的约束这个紧箍咒。

  泛型的约束

  直接来代码:

  很简单的一个例子,接口isports,iwork,people类,japanese类等简单继承了一下,目前准备的一些代码。

  public interface isports
    {
        void pingpang();
    }

    public interface iwork
    {
        void work();
    }

    public class people
    {
        public int id { get; set; }
        public string name { get; set; }

        public void hi()
        { }
    }
public class chinese : people, isports, iwork { public void tradition() { console.writeline("仁义礼智信,温良恭俭让"); } public void sayhi() { console.writeline("吃了么?"); } public void pingpang() { console.writeline("打乒乓球..."); } public void work() { throw new notimplementedexception(); } }
public class hubei : chinese { public hubei(int id) { } public string changjiang { get; set; } public void majiang() { console.writeline("打麻将啦。。"); } }
public class japanese : isports { public int id { get; set; } public string name { get; set; } public void pingpang() { console.writeline("打乒乓球..."); } public void hi() { } }

  再来个调用类constraint

 public class constraint
    {
        /// <summary>
        /// 代码编译没问题,执行的时候才报错
        /// 代码安全问题
        /// </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);

            people people = (people)oparameter;

            console.writeline(people.id);//这里就不行了 代码安全问题,调用不到,但编译不会报错。
            console.writeline(people.name);
        }

        /// <summary>
        /// 基类约束:
        /// 1 带来权力,可以使用基类里面的属性和方法
        /// 2 带来义务,类型参数必须是基类或者其子类
        /// </summary>
        /// <typeparam name="t"></typeparam>
        /// <param name="tparameter"></param>
        public static void show<t>(t tparameter)
            where t : people, isports, new()//都是and 关系
        {
            console.writeline("this is {0},parameter={1},type={2}",
                typeof(genericmethod), tparameter.gettype().name, tparameter.tostring());

            console.writeline(tparameter.id);
            console.writeline(tparameter.name);
            tparameter.hi();
            tparameter.pingpang();
            t t = new t();
        }

        public static void showpeople(people tparameter)
        {
            console.writeline("this is {0},parameter={1},type={2}",
                typeof(genericmethod), tparameter.gettype().name, tparameter.tostring());

            console.writeline(tparameter.id);
            console.writeline(tparameter.name);
            tparameter.hi();
            //tparameter.pingpang();
        }

        public static t donothing<t>(t tparameter)
            //where t : isports//接口约束
            //where t : class//引用类型约束
            //where t : struct//值类型约束
            where t : new()//无参数构造函数约束
        {
            //tparameter.pingpang();
            //return null;
            t t = new t();
            return default(t);//会根据t的类型,去产生一个默认值
        }
    }

  有兴趣的可以测试下,用showobject的方法和泛型show<t>(t tparameter)调用来看差异,如果不加入约束,想调用参数的属性和方法,代码安全问题是调用不了的,会报错,但是加入基类约束之后是可以调用到的,所以泛型约束带来了权利,可以使用基类的属性和方法,但也带来义务,参数只能是基类和子类,又想马儿跑,又想马儿不吃草的事情是没有的,权利和义务是相对的,在享受权利的同时也会带来义务。

  其次,约束可以多重约束,然后即可作为参数约束也可以作为返回值约束,例如default(t)会根据泛型类型返回一个默认值,如果是无参数构造约束就可以类似这样写返回值t t=new t()。

  总之,我觉得泛型约束为了更灵活的满足不同条件的需求而产生的,就是我们在写一些固定的需求,约束叠加来完成我们的功能,同时不让泛型肆无忌惮。

        泛型约束范围如下:

约束

描述

where t: struct

类型参数必须为值类型。

where t : class

类型参数必须为引用类型。

where t : new()

类型参数必须有一个公有、无参的构造函数。当于其它约束联合使用时,new()约束必须放在最后。

where t : <base class name>

类型参数必须是指定的基类型或是派生自指定的基类型。

where t : <interface name>

类型参数必须是指定的接口或是指定接口的实现。可以指定多个接口约束。接口约束也可以是泛型的。

  好了泛型的约束的先说到这,继续套底子,了解到的都倒出来。

 

  逆变和协变

  逆变和协变不知道有没有小伙伴熟悉的,我开始是不知道这个的,第一次看到也是一脸懵逼,到现在也有点迷糊,能不能讲清楚看造化了,哈哈

   继续举栗子:

//协变
public interface ienumerable<out t> : ienumerable
//逆变
public delegate void action<in t>(t obj);

  这段代码是不是很熟悉?里面有个out t 还有 in t,这里的out 不是我们熟悉的参数返回out,ref的作用,是协变专用的,逆变和协变指出现在接口或者委托泛型前面,

  in只能作为参数传入,out只能作为参数传出。

  下面来个代码  

  

    public class bird
    {
        public int id { get; set; }
    }
    public class sparrow : bird
    {
        public string name { get; set; }
    }
调用实例 ienumerable<int> intlist = new list<int>(); action<int> iaction = null; bird bird1 = new bird(); bird bird2 = new sparrow();//左边是父类 右边是子类 sparrow sparrow1 = new sparrow(); //sparrow sparrow2 = new bird();//不是所有的鸟,都是麻雀 list<bird> birdlist1 = new list<bird>();//一群鸟 是一群鸟 //list<bird> birdlist2 = new list<sparrow>();//一群麻雀难道不是一群鸟 ? 不是的,没有父子关系 list<bird> birdlist3 = new list<sparrow>().select(c => (bird)c).tolist();//这里使用别扭了,明明知道但就是不能这样写

  以上代码发现问题了吗?很明显出现了一些不和谐的地方,我们换个方式如下: 

      
      
   /// <summary>
    /// 逆变:只能修饰传入参数
    /// </summary>
    /// <typeparam name="t"></typeparam>
    public interface icustomerlistin<in t>
    {
        //t get();

        void show(t t);
    }

    public class customerlistin<t> : icustomerlistin<t>
    {
        //public t get()
        //{
        //    return default(t);
        //}

        public void show(t t)
        {
        }
    }

    /// <summary>
    /// out 协变 只能是返回结果
    /// </summary>
    /// <typeparam name="t"></typeparam>
    public interface icustomerlistout<out t>
    {
        t get();

        //void show(t t);//不能做参数
    }

    public class customerlistout<t> : icustomerlistout<t>
    {
        public t get()
        {
            return default(t);
        }

        //public void show(t t)
        //{

        //}
    }

 



       {//协变:接口泛型参数加了个out,就是为了解决刚才的不和谐 ienumerable<bird> birdlist1 = new list<bird>(); ienumerable<bird> birdlist2 = new list<sparrow>(); //func<bird> func = new func<sparrow>(() => null); icustomerlistout<bird> customerlist1 = new customerlistout<bird>(); icustomerlistout<bird> customerlist2 = new customerlistout<sparrow>(); } {//逆变 icustomerlistin<sparrow> customerlist2 = new customerlistin<sparrow>(); icustomerlistin<sparrow> customerlist1 = new customerlistin<bird>(); //customerlist1.show() icustomerlistin<bird> birdlist1 = new customerlistin<bird>(); birdlist1.show(new sparrow()); birdlist1.show(new bird()); action<sparrow> act = new action<bird>((bird i) => { }); }

  这样可以了,协变ienumerable加入协变out 左边是个父类,右边可以是子类,逆变in 左边是个字类,右边也可以是父类,下面这段就更晕了,稍微看下吧。

       {
                imylist<sparrow, bird> mylist1 = new mylist<sparrow, bird>();
                imylist<sparrow, bird> mylist2 = new mylist<sparrow, sparrow>();//协变
                imylist<sparrow, bird> mylist3 = new mylist<bird, bird>();//逆变
                imylist<sparrow, bird> mylist4 = new mylist<bird, sparrow>();//协变+逆变
            }

  总结下原理:泛型在jit编译时指定具体类型,同一个泛型类,不同的类型参数,其实会变成不用的类型。

  我走的很慢,但从不后退!