一篇带你玩转泛型,泛型详解,什么是泛型,泛型缓存,协变与逆变,泛型方法,为什么使用泛型,泛型接口,ASP.Net泛型,泛型Generic

泛型(Generic)

1.什么是泛型?

  • 泛型是C#2.0推出的新语法,不是语法糖,而是2.0由框架升级提供的功能。
    我们在编写代码时,经常会遇到功能非常相似的模块,只是他们处理的数据不一样(参数类),有没有一种方法用同一个方法来处理传入不同类型的参数呢?泛型就可以解决这个问题。
List<string> 就是泛型,带有"<>"括号的都是泛型,List<string>是一个集合,也有可能是一组 <int> 泛型集合。泛型的用处是用一个东西来满足不同类型的需求。
  • 泛型命名空间
using System.Collections.Generic;

2.为什么使用泛型?

  • 下面我们声明了两个方法,一个传入int值并打印,一个传入string值并打印,并进行了调用。
 		/// <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);
        }

	 // 调用
      int iValue = 123;
      string sValue = "456";
      CommonMethod.ShowInt(iValue);
      CommonMethod.ShowString(sValue);
  • 通过以上代码我们可以知道:因指定参数类型,只能传递某个类型,但是方法里实现的功能都是一样的,我们知道在C#语言里,Object是一切变量的基类,那我们是不是可以使用Object呢?
  		/// <summary>
        /// 打印个object值
        /// 1 任何父类出现的地方,都可以用子类来代替
        /// 2 公理:object是一切类型的父类
        /// </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";
     	CommonMethod.ShowObject(sValue);
     	CommonMethod.ShowObject(iValue);
  • 但我们使用Object有两个问题:

  • 1.装箱拆箱,性能损耗 传入一个int值(栈),object又在堆里面,如果把int传递进来,就会把值从栈里面copy到堆里面使用的时候,又需要用对象值,又会copy到栈(拆箱)。

  • 2.会有类型安全问题,因为传递的对象是没有限制的。

  • 注:.netframework 1.o版本原来使用这种方法,后续随着版本(2.0)的提升,推出了泛型这种更加安全的方式替换了这种Object。

3.泛型语法与思想

3.1 泛型方法与思想
 		/// <summary>
        /// 泛型方法:方法名称后面加上尖括号,里面是类型参数
        /// 类型参数实际上就是一个类型T声明,方法就可以用这个类型T了                
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="tParameter"></param>
     public static void Show<T>(T tParameter)//, T t = default(T
        {
            Console.WriteLine("This is {0},parameter={1},type={2}",
               typeof(CommonMethod), tParameter.GetType().Name, tParameter);
        }                                                          
方法调用
        int iValue = 123;
        string sValue = "456";
        CommonMethod.Show<int>(iValue);//调用泛型,需要指定类型参数
        CommonMethod.Show(iValue);//如果可以从参数类型推断,可以省略类型参数---语法糖(编译器提供的功能)
        CommonMethod.Show<string>(sValue);
        CommonMethod.Show<int>(sValue);//类型错了
  • 通过上述代码我们思考下,为什么泛型可以解决上面的问题呢?
    泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候才指定参数类型。 延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列、EF的延迟加载等等。

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

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

 Console.WriteLine(typeof(List<>));
 Console.WriteLine(typeof(Dictionary<,>));
  • 结果:

在这里插入图片描述

  • 从上面的截图中可以看出:泛型在编译之后会生成占位符。

  • 注意:占位符需要在英文输入法状态下才能输入,只需要按一次波浪线(数字1左边的键位)的键位即可,不需要按Shift键。

3.2 泛型类声明与使用
  • 声明泛型类
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     /// <summary>
10     /// 泛型类
11     /// </summary>
12     /// <typeparam name="T"></typeparam>
13     public class GenericClass<T>
14     {
15         public T TProp{get;set;};
16     }
17 }
  • Main()方法中调用:
// T是int类型
GenericClass<int> genericInt = new GenericClass<int>();
genericInt.TProp = 123;
// T是string类型
GenericClass<string> genericString = new GenericClass<string>();
genericString.TProp = "123";
3.4 泛型接口
    /// <summary>
    /// 泛型接口
    /// </summary>
    public interface IGenericInterface<T>
    {
        //泛型类型的返回值
        T GetT(T t);
    }

    /// <summary>
    /// 泛型类,实现泛型接口
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class ClassModel<T> : IGenericInterface<T>
    {
        public T GetT(T t)
        {
            throw new NotImplementedException();
        }
    }

4.泛型的性能

  • 看一下的一个例子,比较普通方法、Object参数类型的方法、泛型方法的性能。

  • 添加一个Monitor类,让三种方法执行同样的操作,比较用时长短:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    /// <summary>
    /// 性能对比
    /// </summary>
    public class Monitor
    {
        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 < 100_000_000; i++)
                    {
                        ShowInt(iValue);
                    }
                    watch.Stop();
                    commonSecond = watch.ElapsedMilliseconds;
                }
                {
                    Stopwatch watch = new Stopwatch();
                    watch.Start();
                    for (int i = 0; i < 100_000_000; i++)
                    {
                        ShowObject(iValue);
                    }
                    watch.Stop();
                    objectSecond = watch.ElapsedMilliseconds;
                }
                {
                    Stopwatch watch = new Stopwatch();
                    watch.Start();
                    for (int i = 0; i < 100_000_000; i++)
                    {
                        Show<int>(iValue);
                    }
                    watch.Stop();
                    genericSecond = watch.ElapsedMilliseconds;
                }
                Console.WriteLine("commonSecond={0},objectSecond={1},genericSecond={2}"
                    , commonSecond, objectSecond, genericSecond);
            }
        }

        #region PrivateMethod
        private static void ShowInt(int iParameter)
        {
            //do nothing
        }
        private static void ShowObject(object oParameter)
        {
            //do nothing
        }
        private static void Show<T>(T tParameter)
        {
            //do nothing
        }
        #endregion

    }
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    /// <summary>
    /// 性能对比
    /// </summary>
    public class Monitor
    {
        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 < 100_000_000; i++)
                    {
                        ShowInt(iValue);
                    }
                    watch.Stop();
                    commonSecond = watch.ElapsedMilliseconds;
                }
                {
                    Stopwatch watch = new Stopwatch();
                    watch.Start();
                    for (int i = 0; i < 100_000_000; i++)
                    {
                        ShowObject(iValue);
                    }
                    watch.Stop();
                    objectSecond = watch.ElapsedMilliseconds;
                }
                {
                    Stopwatch watch = new Stopwatch();
                    watch.Start();
                    for (int i = 0; i < 100_000_000; i++)
                    {
                        Show<int>(iValue);
                    }
                    watch.Stop();
                    genericSecond = watch.ElapsedMilliseconds;
                }
                Console.WriteLine("commonSecond={0},objectSecond={1},genericSecond={2}"
                    , commonSecond, objectSecond, genericSecond);
            }
        }

        #region PrivateMethod
        private static void ShowInt(int iParameter)
        {
            //do nothing
        }
        private static void ShowObject(object oParameter)
        {
            //do nothing
        }
        private static void Show<T>(T tParameter)
        {
            //do nothing
        }
        #endregion

    }
}
  • Main()方法调用:
Monitor.Show();
  • 执行一亿次的结果,毫秒,结果:
    在这里插入图片描述

  • 从结果中可以看出:泛型方法的性能最高,其次是普通方法,object方法的性能最低。

5.泛型约束

5.1 约束汇总
约束说明
where T : struct类型参数必须是值类型
where T : class类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。
where T : new()类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。
where T:<基类名>类型参数必须是指定的基类或派生自指定的基类。
where T:<接口名称>类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。
5.2 代码示例
  • struct_值类型
class ClassModel<T> where T:struct
{
...代码省略部分
}
  • class_引用类型
class ClassModel<T> where T:struct
{
...代码省略部分
}
  • 其他类型命名方法与上面代码类似.

  • 注意:

    基类约束时,基类不能是密封类,即不能是sealed类。sealed类表示该类不能被继承,在这里用作约束就无任何意义,因为sealed类没有子类。

6.泛型缓存与字典缓存

6.1 字典缓存
  • 在实际的项目中,我们需要将用户的一些常用信息或常用的SQL语句进行缓存,这样大大减少了重复操作,下面是一个字典缓存例子:

      /// <summary>
        /// 字典缓存
        /// </summary>
        public class DictionaryCache
        {
            private static Dictionary<Type, string> _TypeTimeDictionary = null;
            static DictionaryCache()
            {
                Console.WriteLine("This is DictionaryCache 静态构造函数");
                _TypeTimeDictionary = new Dictionary<Type, string>();
            }
            public static string GetCache<T>()
            {
                Type type = typeof(Type);
                if (!_TypeTimeDictionary.ContainsKey(type))
                {
                    _TypeTimeDictionary[type] = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff"));
                }
                return _TypeTimeDictionary[type];
            }
        }
    
  • 在Main中调用

   			System.Console.WriteLine(DictionaryCache.GetCache<int>());
            System.Console.WriteLine(DictionaryCache.GetCache<string>());
            System.Console.Read();
  • 结果
    在这里插入图片描述

通过以上结果我们可以得知,字典缓存满足不了传入的类型不同,可以输出不同的结果,因为静态构造函数只会执行一次,静态属性常驻内存,而且字典缓存需要去内存堆里查找,执行也非常慢,并且也满足不了不同类型缓存不同内容,这时随着框架的升级就出现了泛型缓存。

6.2 泛型缓存
  • 泛型缓存,众所周知,在C#里,静态构造函数只会执行一次,而在泛型类中,因为T的类型不同,每个的类型T,都会执行一次静态构造函数,所以会产生不同的静态属性和静态构造函数,例:

     // 泛型缓存,使用场景:每个不同的泛型类 T,都会生成一份不同的副本,适合不同类型,
     // 需要缓存一份数据的场景,效率高。代码如下:
     // 声明泛型类
       public class GenericCache<T>
        {
            static GenericCache()
            {
                Console.WriteLine("This is GenericCache 静态构造函数");
                _TypeTime = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff"));
            }
    
            private static string _TypeTime = "";
    
            public static string GetCache()
            {
                return _TypeTime;
            }
    
        }
    
    
    • 在Main中调用
    // 调用方法
          for (int i = 0; i < 5; i++)
          {
                Console.WriteLine(GenericCache<int>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache<long>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache<DateTime>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache<string>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache<GenericCacheTest>.GetCache());
                Thread.Sleep(10);
           }
    
    

    结果如下,我们可以看到泛型缓存每当缓存不同类型的参数,都会执行一次构造函数,满足了缓存不同类型的要求,并且泛型缓存的执行速度也要大大超过字典缓存。

    在这里插入图片描述

    注:构造函数每次参数为不同类型时第一次都会调用,同一类型第二次调用不会执行构造函数,而是直接返回缓存的值。

7.泛型的协变逆变

7.1 为什么是泛型的协变与逆变呢?
  • 协变与逆变是在.NET 4.0的时候出现的并且都是跟泛型相关, 只能放在接口或者委托的泛型参数前面。
7.2 协变_covariant
  • 协变:就是让右边可以用子类,让泛型用起来更方便,关键字 out 修饰,协变后,T 只能作为返回值,不能当参数
  • 在下面代码中:我们定义了一个鸟类,还定义了一个啄木鸟类,我们的啄木鸟继承了鸟这个类,并进行了实例化。
    /// <summary>
    /// 鸟类
    /// </summary>
    public class Bird
    {
        public int Id { get; set; }
    }

 	/// <summary>
    /// 啄木鸟类
    /// </summary>
    public class Woodpecker : Bird
    {
        public string Name { get; set; }
    }
	
	
	Bird bird1 = new Bird();//实例化鸟类
    Bird bird2 = new Woodpecker();//子类实例化 啄木鸟当然是个鸟
  • 那咱们的集合是不是也可以这样说呢:一堆的啄木鸟也是一堆鸟?语义上是没问题的,但是咱们的**C#的语法不通过,原因是List<Bird>是一个类List<Woodpecker>**也是一个类,没有父子关系,如果想要使用,我们只有强制转换一下。
 		  List<Bird> birdList1 = new List<Bird>();   
          //不成立,语法不通过,原因是List<Bird>是一个类--List<Woodpecker>也是一个类,没有父子关系
          List<Bird> birdList3 = new List<Woodpecker>();
		  //如果想使用,只能转换一下
		  List<Bird> birdList4 = new List<Woodpecker>().Select(c => (Bird)c).ToList();
  • 但是这样是不是太麻烦了?上面代码证明了泛型还有不够和谐的地方,咱们泛型不就是为了书写方便,所以在.Net 4.0框架的时候,增加了协变与逆变,如下:

    在这里插入图片描述

      			//IEnumerabl就是协变接口,并且是List的父类
                //out修饰,协变后,T只能作为返回值,不能当参数
                IEnumerable<Bird> birdList1 = new List<Bird>(); //实例化集合鸟类
                IEnumerable<Bird> birdList2 = new List<Woodpecker>();//实例化集合啄木鸟类,父类是鸟类,可以正常使用
7.3 自定义协变
  • 下面代码,定义了一个协变,**ICustomerListOut<out T>**客户列表泛型接口 与 **CustomerListOut<T> : ICustomerListOut<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);
        }

    }

  • 在Main中调用
                ICustomerListOut<Bird> customerList1 = new CustomerListOut<Bird>();
                ICustomerListOut<Bird> customerList2 = new CustomerListOut<Woodpecker>();
                customerList2.Get();

注:通过上述代码可以看到,在泛型接口的T前面有一个out关键字修饰,而且T只能是返回值类型,不能作为参数类型,这就是协变。使用了协变以后,左边声明的是基类,右边可以声明基类或者基类的子类,这就是协变,让泛型更加方便使用。

7.4 逆变_contravariant
  • 逆变:就是让右边可以用父类,让泛型用起来更方便,**out修饰,协变后,T**只能作为返回值,不能当参数。

在这里插入图片描述

  • 在Main方法中调用委托
 Action<Woodpecker> act = new Action<Bird>((Bird i) => { }); // 子类 = 父类
7.5 自定义逆变
  • 下面代码,定义了一个逆变,**ICustomerListIn<in T>**客户列表接口 与 **CustomerListIn<T> : ICustomerListIn<T>**客户列表类
 	/// <summary>
    /// 逆变
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ICustomerListIn<in T>
    {
        //T Get();//不能做返回值,报错

        void Show(T t);
    }

    public class CustomerListIn<T> : ICustomerListIn<T>
    {

        public void Show(T t)
        {
        }
    }

  • 在Main中调用
                //逆变:就是让右边可以用父类,让泛型用起来更方便
                //in修饰,逆变后,T只能作为当参数  不能做返回值,
                ICustomerListIn<Woodpecker> customerList1 = new CustomerListIn<Bird>();
                customerList1.Show(new Woodpecker());

注:通过上述代码可以看到,这时左边作为子类,右边可以用基类,这就是逆变,让泛型更加方便使用。

7.6 自定义协变+逆变
  • 协变和逆变也可以同时使用,例:
 /// <summary>
    /// 泛型接口
    /// </summary>
    /// <typeparam name="inT">in 逆变,只能当参数</typeparam>
    /// <typeparam name="outT">out 协变,只能当返回值</typeparam>
    public interface IMyList<in inT, out outT>
    {
        /// <summary>
        /// 声明了Show方法,参数为逆变inT
        /// </summary>
        /// <param name="t"></param>
        void Show(inT t);
        /// <summary>
        /// 声明Get方法,无参数,返回值为逆变outT
        /// </summary>
        /// <returns></returns>
        outT Get();
        /// <summary>
        /// 声明了Do方法,参数为逆变inT,返回值为协变outT
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        outT Do(inT t);

        out 只能是返回值   in只能是参数
        //void Show1(outT t);
        //inT Get1();

    }

    /// <summary>
    /// 泛型类,实现了IMyList<T1, T2>泛型接口
    /// </summary>
    /// <typeparam name="T1"></typeparam>
    /// <typeparam name="T2"></typeparam>
    public class MyList<T1, T2> : IMyList<T1, T2>
    {
        
        public void Show(T1 t)
        {
            Console.WriteLine(t.GetType().Name);
        }

        public T2 Get()
        {
            Console.WriteLine(typeof(T2).Name);
            return default(T2);
        }

        public T2 Do(T1 t)
        {
            Console.WriteLine(t.GetType().Name);
            Console.WriteLine(typeof(T2).Name);
            return default(T2);
        }
    }
  • 在Main方法中调用
     		    // (in逆变)子类与父类(out协变)  =  (in逆变)子类与父类(out逆变)
                IMyList<Woodpecker, Bird> myList1 = new MyList<Woodpecker, Bird>(); //正常使用
                // (in逆变)子类与父类(out协变)  =  (in逆变)子类与子类(out逆变)
                IMyList<Woodpecker, Bird> myList2 = new MyList<Woodpecker, Woodpecker>();//协变
                // (in逆变)子类与父类(out协变)  =  (in逆变)父类与父类(out逆变)
                IMyList<Woodpecker, Bird> myList3 = new MyList<Bird, Bird>();//逆变
                // (in逆变)子类与父类(out协变)  =  (in逆变)父类与子类(out逆变)
                IMyList<Woodpecker, Bird> myList4 = new MyList<Bird, Woodpecker>();//协变+逆变
 				//协变out:左边是父类,右边是子类,只能当参数,无法当返回值
                //逆变in :左边是子类,右边是父类,只能当返回值,无法当参数

注:协变和逆变在C#中通常用于事件处理(其中事件处理程序可以处理派生类的事件)和LINQ(其中协变和逆变允许更灵活的类型转换)。在接口中使用它们可以增强代码的灵活性和重用性。然而,需要注意的是,不是所有的泛型接口或方法都可以使用协变和逆变,它们的使用受到一定的限制,并且通常只在特定的上下文(如委托和接口)中才有意义。

  • 39
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值