.Net进阶 —— 泛型generic使用和原理 封装泛型缓存

泛型generic使用和原理 封装泛型缓存

一.泛型的由来

泛型基本概念:.NET 泛型是一个很强大的新特性,它为每一种对象生成一份单独的代码(也就是所谓的“实例化”),这一份量身顶做的代码具有很高的效率,是强类型的,不需要运行期多态的支持和负担,有了泛型,就不再需要Object类来参与实现一些通用类或方法了.

泛型的作用: 在CLR(common language runtime)1.0中,当要创建一个灵活的类或方法,但该类或方法在编译期问不知道使用什么类,就必须以System.Object类为基础进行处理,而Object类在编译期间没有类型安全性,又必须进行强制类型转换.另外,给值类型使用Object类会有性能损失,这给程序开发带来诸多不便。故在CLR 2.0(.NET 3.5基于CLR 2.0)中,提供了泛型,创建的目的就是为了不同类型创建相同的方法或类,也包括接口,委托的泛型

通过使用泛型类型,可以根据需要,用特定的类型替换泛型类型,同时保证了类型安全性:如果某个类型不支持泛型类,编译器就会报错,以阻止程度发生运行期错误.正确使用泛型将大大提高代码的灵活性,结合一个优秀的设计模式,可以显著缩短开发时间。

总而言之:泛型,其实就是 CLR+JIT的支持,使用泛型,可以重用代码,由CLR+JIT延迟等编译和执行的时候生成一个个具体的类或方法等等 的副本。比如:public class A类,这样,new的时候,var aStr= new A();var aInt=new A();这样,其实在编译的时候,会生成 A_001(string) 类和A_002 (int)类。

二、泛型的好处

值类型和引用类型的装箱拆箱消耗。值类型分配在线程栈上,引用类型分配在堆上,只把指针放在栈上。如图所示,如果把int类型1装箱,就要把1拷贝到堆中,就会有内存的交换。以前的ArrayList就是类型不安全的,需要频繁的进行装拆箱操作,Add元素的时候全部装箱object,取的时候要拆箱,性能损失比较大。
在这里插入图片描述
泛型的效率等同于硬编码的方式,就是和你很多功能相同的类效率差不多。泛型每个类型只实例化一次,下面泛型缓存会详细解读下。先简单介绍下CLR的运行原理以了解泛型的原理机制。
在这里插入图片描述NET编译器和解释器两阶段,我们先经过编译器编译成IL中间语言(dll、exe),和java的字节码类似,然后经过JIT解释成机器码。这样做的好处就是我们只需要编译成IL后,在各个不同计算机系统上,只要有对应的CLR(JIT)就行,这样就和平台无关。二次编译:为了一次编译,不同平台使用。泛型在第一个编译时会用一个占位符代替,在第二次运行时会编译成具体的类型。所以性能相当于硬编码的方式,每种类型最终都有自己的机器码。

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

泛型与object的对比

比如我们需要打印出,传入参数的类型名称,那么使用常规代码方式,有常用2种方式去实现:
第一种,调用对应的showInt(),showString(),showTime()…方法,如下:
常规代码使用

/// <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);
ShowString(sValue);
ShowDateTime(dtValue);

第二种,使用showObject(object oParameter);
使用object

/// <summary> 
///为什么用object 作为参数类型,调用的时候,可以把任何类型都传进来
///
///C#: 任何父类出现的地方  都可以用子类代替;
///Object类型是一切类型的父类
///
///Object 出现的都可以让任何类型传进来
/// 
/// 但是:有2个问题
///        性能问题:会出现装箱和拆箱;
///        类型安全问题。 
/// </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);
}

//看看调用:
ShowInt(iValue);
ShowString(sValue);
ShowDateTime(dtValue);

泛型类使用

/// <summary>
/// 泛型方法:需要在方法名的后面带一个<>,需要定义T, T是什么呢? 不知道, 
/// T:类型参数,只是一个占位符,类型在声明其实不确定,在调用的时候,确定类型。
///  
/// 延迟声明,推迟一切可以推迟的,事情能晚点做就晚点做。 
/// 在使用的时候,需要指定明确的类型
/// 泛型方法性能和普通方法差不多,可以用一个方法满足多个类型的需求 
/// 集成了前面2种的各自的长处。既可以安全调用,性能又好,并且可以用一个方法代替N个方法
/// </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(CommonMethod), tParameter.GetType().Name, tParameter);
}

//看看调用:

Show(iValue); //如果类型参数,可以通过参数类型推导出来,那么就可以省略
/* Show<int>(sValue);*/ //因为类型错了 
Show(sValue);
Show<DateTime>(dtValue);
Show<object>(oValue);

为什么要有泛型,而不用object
object类型:

  • 1.任何父类出现的地方都可以用子类代替
  • 2.object是一切类型的父类

object弊端:装箱拆箱性能损耗(栈的出栈和入栈受CLR管理)
int类型赋值之后存放在栈里面的,传递给Object类型时需要放到堆里面;使用的时候又需要从堆里面放到栈里面。

三、泛型各种应用

泛型类、泛型方法、泛型接口、泛型委托

泛型方法 一个方法满足多个类型的需求
泛型类 就是一个类 满足多个类型的需求
泛型接口 就是一个接口 满足多个多个类型的需求
泛型委托 就是一个委托 满足多个多个类型的需求

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

泛型类

//泛型类的定义在常规的类后面加<T>,T为占位符,可以任何字符代替。
//eg:  public class Student<T>{}
//亦可以多个类型。占位符不重复,用逗号隔开
//eg:  public class People<T,A,B,C>{}

//例子a:
public class ClassA<T>
{
    /// <summary>
    /// 获取T的类型全名称
    /// </summary>
    /// <returns></returns>
    public string GetTTypeName()
    {
        return typeof(T).FullName;
    }
}
//使用
//使用泛型来打印实例类型
{
    ClassA<int> aInt = new ClassA<int>();
    Console.WriteLine(aInt.GetTTypeName());//System.Int32

    ClassA<string> aStr = new ClassA<string>();
    Console.WriteLine(aStr.GetTTypeName());//System.String

    Console.ReadLine();
}

//例子b:
public class ClassB<T,A> 
    where T:struct  //约束T是个struct
    where A:class,new () //约束A是个引用类型,无参数构造函数约束
{
    /// <summary>
    /// 获取T的类型全名称
    /// </summary>
    /// <returns></returns>
    public string GetTTypeName()
    {
        return typeof(T).FullName;
    }
}

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

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

泛型接口

//我们有4个类(人类、中国人、广东人、日本人),
//中国人继承人类,广东人继承中国人。日本人没继承人类。
//有2个接口(IWork,ISports)

public interface ISports
{
    void Pingpang();
}

public interface IWork
{
    void Work();
}

/// <summary>
/// 人类
/// 基本属性有:ID,名称,会说Hi
/// </summary>
public class People
{
    public int Id { get; set; }
    public string Name { get; set; }
    public void Hi()
    { }
}

/// <summary>
/// 中国人:继承了人类(拥有人类的基本属性)
/// 也有自己的说Hi方式,有额外的属性,还实现了运动,工作
/// </summary>
public class Chinese : People, ISports, IWork
{
    public void Tradition()
    {
        Console.WriteLine("仁义礼智信,温良恭俭让");
    }
    public void SayHi()
    {
        Console.WriteLine("吃了么?");
    }
    public void Pingpang()
    {
        Console.WriteLine("打乒乓球...");
    }
    public void Work()
    {
        Console.WriteLine("工作中...");
    }
}

/// <summary>
/// 广东人:继承了中国人(拥有中国人的基本属性)
/// 额外还会吃。
/// </summary>
public class GuangDong : Chinese {
    public void Eat()
    {
        Console.WriteLine("正在吃福建人...");
    }
}

/// <summary>
/// 日本人:只实现了运动
/// </summary>
public class Japanese : ISports
{
    public int Id { get; set; }
    public string Name { get; set; }

    public void Pingpang()
    {
        Console.WriteLine("雅美蝶,打乒乓球...");
    }
}


//如下调用
People people = new People()
{
    Id = 1,
    Name = "令狐冲"
};
Chinese chinese = new Chinese()
{
    Id = 2,
    Name = "岳不群"
};
GuangDong guangDong = new GuangDong()
{
    Id = 3,
    Name = "小林子"
};
Japanese japanese = new Japanese()
{
    Id = 4,
    Name = "冲田杏梨"
};

//现在有这4个对象
//我想要他们,统统运动起来,如下:
//常规:
// people 没有运动的方法
chinese.Pingpang();
guangDong.Pingpang();
japanese.Pingpang();

//如果,我想他们统统运动起来,使用把对象传入进去的方法,
//那么又回到上面我说,替这3个类加3个方法
/*eg:
 * public static void ChineseShowSports(Chinese chinese){ chinese.Pingpang(); }
 * public static void GuangDongShowSports(GuangDong guangDong){guangDong.Pingpang(); }
 * public static void JapaneseShowSports(Japanese japanese){japanese.Pingpang(); }
 */
 //调用如下
ChineseShowSports(chinese);
GuangDongShowSports(guangDong);
JapaneseShowSports(japanese);


//这样也是可以的,那么下面的泛型就可以把他们泛型成一个方法
/*eg:
 * public static void ShowSports<T>(T t) where T: People,ISports
 * {
 *     t.Pingpang();
 * }
 */
//调用如下
ShowSports(chinese);
ShowSports(guangDong);
ShowSports(japanese);

泛型委托:使用一个委托时,都需要先声明这个委托类,规定参数和返回值类型,然后才能实例化、调用。为了简化这个过程, .NET 框架为我们封装了三个泛型委托类,因此大部分情况下我们不必再声明委托,可以拿来直接实例化使用,方便了我们的日常Coding。

.这三种泛型委托包括:Func委托、Action委托和Predicate委托。

普通委托的定义:

public delegate int 委托名(int a, int b);

泛型委托的定义:

public delegate T 委托名<T>(T a, T b);
  • Action委托代表返回值为空 void 的委托,它也有一些列重载,最多拥有16个输入参数。用法与Func相同。
  • Func委托代表着拥有返回值的泛型委托。Func有一系列的重载,形式如 Func<T1,T2, … TResult>,其中TResult代表委托的返回值类型,其余均是参数类型。只有一个T时,即Func,代表该委托是无参数的。.NET封装了最多16个输入参数的Funct<>委托。
  • Predicate委托,这个一般用的较少,它封装返回值为bool类型的委托,可被Func代替。

四、泛型的功能

泛型中的默认值

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

约束

泛型约束 是 在泛型类、方法、等对其调用的约束,起到保护作用和使用(履行义务+享受权利),使用方法是 后面跟 where T : Class,new()等。多重约束使用逗号隔开。
泛型导致任何类型都可以进来,那么如何去使用这个类型T,编写的时候我们是不知道T是什么,也不知道它能干什么。一个方法就是可以用反射,任何一个类型通过发射都能获取内部的结构属性方法调用。泛型约束提供更简便的方法。在声明泛型时在参数后面追加where关键字。约束可以同时指定多个,像这样where:T People,IWork,new()。同时约束传进来的类型People或其子类,并且继承了IWork接口,有无参数构造函数。
在这里插入图片描述

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

//多泛型如T,G,F,使用多个where。如:
public static void ShowSports<T,G,F>(T t)

where T :People,ISports

where G:ISports

where F:Class

{
  //这里是业务,可以使用 T,G,F
}

说明:
where T:People// 约束T是People类型
where T:ISports// 约束T是实现ISports类型
where T :Class// 约束T是引用类型
where T :new()// 无参数构造的函数约束
where T :struct// 约束T是结构类型

协变逆变

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

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

ShowName(dog);

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

//所谓协变逆变都是跟泛型相关
//只能放在接口或者委托的泛型参数前面
// out 协变 修饰返回值。就是让右边可以用子类
// out修饰之后只能做返回值,不能做参数
// in 逆变 修饰传入参数

//一堆狗肯定是一堆动物啊,为啥就不能这么做呢?下面这句编译不通过
//前后两个类型是没有父子关系的
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");

其实协变逆变就是语法糖,为了让不是继承关系的类型也可以互相赋值编译通过。运行时实际右边是什么类型就是什么类型。

泛型缓存

泛型类的静态成员只能在类的一个实例中共享。运行时泛型类的实例已经指定了具体类型,每一个不同的泛型类实例共享静态成员,利用这个特点就可以做缓存。每一个不同的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("************************");

在这里插入图片描述

五、总结

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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cool2Feel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值