Liao9144 / Blog

我的博客
1 stars 0 forks source link

.NET 高级开发系列之泛型(Generic) #2

Open Liao9144 opened 6 years ago

Liao9144 commented 6 years ago

一、泛型概述

1.什么是泛型?

泛型(generic)是 C# 语言和公共语言运行时 (CLR) 的 2.0 版本中添加的新特性。泛型为 .NET Framework 引入了类型参数 (type parameters)的概念。类型参数使得设计类和方法时,不必确定一个或多个具体的类型,具体类型可延迟到客户代码中声明并初始化。这意味着使用泛型的类型参数 T,写一个类 MyList<T>,客户代码可以这样调用:MyList<int>MyList<string>MyList<MyClass>

2.为什么要使用泛型?

我们先看下面代码

public static void ShowInt(int iParameter)
{
    Console.WriteLine("This is {0},parameter={1},type={2}",
     nameof(CommonMethod), iParameter, iParameter.GetType().Name);
}

public static void ShowString(string sParameter)
{
    Console.WriteLine("This is {0},parameter={1},type={2}",
     nameof(CommonMethod), sParameter, sParameter.GetType().Name);
}

public static void ShowDateTime(DateTime dtParameter)
{
    Console.WriteLine("This is {0},parameter={1},type={2}",
    nameof(CommonMethod), dtParameter, dtParameter.GetType().Name);
}

我们可以看出这三个方法,除了传入的参数不同外,其里面实现的功能都是一样的,我们在编程程序时,经常会遇到功能非常相似的模块,只是它们处理的数据不一样。这个时候问题来了,有没有一种办法,用同一个方法来处理传入不同种类型参数的办法呢? 微软在 1.0 和 1.1 版本的时候可以用 OOP 三大特性之一的继承来解决上述问题(通过继承,子类拥有父类的一切属性和行为;任何父类出现的地方,都可以用子类来代替),我们知道,C# 语言中,所有类型都源自同一个类型,那就是 object。

public static void ShowObject(object oParameter)
{
    Console.WriteLine("This is {0},parameter={1},type={2}",
    nameof(CommonMethod), oParameter, oParameter.GetType().Name);
}

显示结果

int iValue = 123;
string sValue = "abc";
DateTime dtValue = DateTime.Now;

Console.WriteLine("****************常规调用****************");
{
    CommonMethod.ShowInt(iValue);
    CommonMethod.ShowString(sValue);
    CommonMethod.ShowDateTime(dtValue);
}

Console.WriteLine("****************object调用****************");
{
    CommonMethod.ShowObject(iValue);
    CommonMethod.ShowObject(sValue);
    CommonMethod.ShowObject(dtValue);
}

image

我们可以看出,目地是达到了,解决了代码的可读性,但是这样又有个不好的地方了,object 是引用类型,当传个值类型 int 时会有装箱拆箱操作,导致性能损失。 终于,微软在 2.0 的时候发布了泛型,避免了运行时类型转换或装箱操作的代价和风险,接下来我们用泛型方法来实现该功能。

public static void Show<T>(T tParameter)
{
    Console.WriteLine("This is {0},parameter={1},type={2}",
        typeof(GenericMethod).Name, tParameter, tParameter.GetType().Name);
}

显示结果

int iValue = 123;
string sValue = "abc";
DateTime dtValue = DateTime.Now;

Console.WriteLine("****************常规调用****************");
{
    CommonMethod.ShowInt(iValue);
    CommonMethod.ShowString(sValue);
    CommonMethod.ShowDateTime(dtValue);
}

Console.WriteLine("****************object调用****************");
{
    CommonMethod.ShowObject(iValue);
    CommonMethod.ShowObject(sValue);
    CommonMethod.ShowObject(dtValue);
}

Console.WriteLine("****************泛型调用****************");
{
    GenericMethod.Show<int>(iValue);
    GenericMethod.Show(sValue); // 可以省略,自动推算
    GenericMethod.Show<DateTime>(dtValue);
}

image

3.性能对比

public static void Show(int iValue)
{
    long commonTime = 0;
    long objectTime = 0;
    long genericTime = 0;

    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 100000000; i++)
        {
            ShowInt(iValue);
        }
        watch.Stop();
        commonTime = watch.ElapsedMilliseconds;
    }
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 100000000; i++)
        {
            ShowObject(iValue);
        }
        watch.Stop();
        objectTime = watch.ElapsedMilliseconds;
    }
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 100000000; i++)
        {
            Show<int>(iValue);
        }
        watch.Stop();
        genericTime = watch.ElapsedMilliseconds;
    }

    Console.WriteLine("commonTime={0}ms,objectTime={1}ms,genericSecond={2}ms"
        , commonTime, objectTime, genericTime);
}

private static void ShowInt(int iParameter) { }
private static void ShowObject(object oParameter) { }
private static void Show<T>(T tParameter) { }

显示结果

Console.WriteLine("****************Monitor****************");
Monitor.Show(iValue);

image

4.总结及扩展

泛型是 2.0 推出的新语法,不是语法糖,而是 2.0 由框架升级提供的功能,需要编译器支持 + JIT 支持(即时编译器,中间语言 IL 转成 JIT(机器码) 生成一个占位符 `n(<T>==>`1、<T,U>==>`2、<T,U,V>==>`3、<T,U,V,...n>==>`n),泛型方法性能 ≈≈ 普通方法 > object 方法(需要装箱拆箱),延迟声明把参数类型的声明推迟到调用,这里可以衍生出 “推迟一切可以推迟的”延迟思想的设计思想,延迟思想有很多地方有使用,比如前端中图片的懒加载、后端 EF 的延迟加载,系统架构的分布式消息队列。

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

// <summary>
/// 一个类来满足不同的具体类型,做相同的事
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericClass<T>
{
    private T _t;
}

/// <summary>
/// 一个接口来满足不同的具体类型的接口,做相同的事
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IGenericInterface<T>
{
    T GetT(T t);  // 泛型类型的返回值
}

public class CommonClass
    //: GenericClass<int>  // 必须指定
    : IGenericInterface<int>  // 必须指定
{
    public int GetT(int t)
    {
        throw new NotImplementedException();
    }
}

public class GenericClassChild<T>
    //: GenericClass<T>
    : GenericClass<int>, IGenericInterface<T>
{
    public T GetT(T t)
    {
        throw new NotImplementedException();
    }
}

public delegate void SayHi<T>(T t);  // 泛型委托

在仓储,ORM,继承泛型类,实现泛型接口,泛型来泛型去用得会比较多。

三、泛型约束

定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的几种类型施加限制。 如果客户端代码尝试使用约束所不允许的类型来实例化类,则会产生编译时错误。 这些限制称为约束。 通过使用 where 上下文关键字指定约束。下表列出了六种类型的约束:

image

使用约束的原因

我们先看下面一段代码

public class People
{
    public int Id { get; set; }
    public string Name { get; set; }

    public void SayHi()
    {
        Console.WriteLine("Hi");
    }
}

public interface ITeacher
{
    void Lecture();
}

public interface IStudent
{
    void Work();
}

public class Teacher : People, ITeacher
{
    public void Lecture()
    {
        Console.WriteLine("讲课");
    }
}

public class Student : People, IStudent
{
    public void Work()
    {
        Console.WriteLine("做作业");
    }
}

现在又有个需求,用泛型方法把 Teacher 和 Student 的 ID 和 Name 都打印出来,我们简单改造 Show 方法

image

报错,编译器不同过,用泛型可以把不同类型参数传进来,但是任何类型都能过来,你知道我是谁?解决这个问题可以加上约束

public static void Show<T>(T tParameter)
        where T : People
{
    //Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
    //Console.WriteLine($"{(People)(tParameter).Id}_{(People)(tParameter).Name}");

    Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
}

这次代码编译通过,这里使用了基类约束能强制保证 T 一定是 People 或者 People 的子类,所以可以使用基类的一切属性方法,这里体现了一句话“没有约束,也就没有自由”。其实上面可以不用泛型也可以完成这个需求

public static void ShowBase(People people)
{
    Console.WriteLine($"{people.Id}_{people.Name}");
}

那为什么还要用泛型呢,因为约束可以叠加,更灵活

public static void Show<T>(T tParameter)
    where T : People, ITeacher, new()
{
    //Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
    //Console.WriteLine($"{(People)(tParameter).Id}_{(People)(tParameter).Name}");

    Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
    tParameter.SayHi();
    tParameter.Lecture();
}

显示结果

Console.WriteLine("****************约束****************");
{
    People people = new People
    {
        Id = 11,
        Name = "张三"
    };
    Teacher teacher = new Teacher
    {
        Id = 12,
        Name = "李老师"
    };
    Student student = new Student
    {
        Id = 13,
        Name = "赵同学"
    };
    //Constraint.Show<People>(people);
    Constraint.Show<Teacher>(teacher);
    //Constraint.Show<Student>(student);
}

image

四、泛型代码中的默认关键字(default)

在泛型类和泛型方法中产生的一个问题是,在预先未知以下情况时,如何将默认值分配给参数化类型 T:

给定参数化类型 T 的一个变量 t,只有当 T 为引用类型时,语句 t = null 才有效;只有当 T 为数值类型而不是结构时,语句 t = 0 才能正常使用。解决方案是使用 default 关键字,此关键字对于引用类型会返回空,对于数值类型会返回零。对于结构,此关键字将返回初始化为零或空的每个结构成员,具体取决于这些结构是值类型还是引用类型。

五、协变&逆变

协变和逆变能够实现数组类型、委托类型和泛型类型参数的隐式引用转换。 协变保留分配兼容性,逆变则与之相反。 下面看个例子

People people = new Teacher();

老师是人,没错

IList<People> peoples = new List<Teacher>();

一群老师是一群人,编译器告诉你错了

image

不合理阿,一群老师当然是一群人拉,所以微软在 4.0 修复了这个问题

IEnumerable<People> peoples = new List<Teacher>();

image

由子类向父类方向转变是协变,协变只能用于返回值类型用, out 关键字 由父类向子类方向转变是逆变,逆变只能用于方法的参数类型用, in 关键字

/// <summary>
/// .net 4.0
/// 只能放在接口或者委托的泛型参数前面
/// out 协变covariant    修饰返回值 
/// in  逆变contravariant  修饰传入参数
public interface IMyList<in inT, out outT>
{
    void Show(inT t);
    outT Get();
    outT Do(inT t);
}

public class MyList<inT, outT> : IMyList<inT, outT>
{
    public outT Do(inT t)
    {
        throw new NotImplementedException();
    }

    public outT Get()
    {
        throw new NotImplementedException();
    }

    public void Show(inT t)
    {
        throw new NotImplementedException();
    }
}

六、泛型缓存

public class GenericCache<T>
{
    private static string _TypeTime = "";

    static GenericCache()
    {
        Console.WriteLine("This is GenericCache 静态构造函数");
        _TypeTime = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff"));
    }

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

我们知道静态构造函数只会初始化一次,在内存中只有一个,但是有了泛型之后可以为每个不同的T,都会生成一份不同的副本 显示结果

Console.WriteLine("****************泛型缓存****************");
{
    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);
    }
}

image

与字典缓存性能对比

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];
    }
}

显示结果

public class CacheMonitor
{
    public static void Show()
    {
        long dictionaryCacheTime = 0;
        long genericCacheTime = 0;

        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i < 100000000; i++)
            {
                DictionaryCache.GetCache<Monitor>();
            }
            watch.Stop();
            dictionaryCacheTime = watch.ElapsedMilliseconds;
        }
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i < 100000000; i++)
            {
                GenericCache<Monitor>.GetCache();
            }
            watch.Stop();
            genericCacheTime = watch.ElapsedMilliseconds;
        }

        Console.WriteLine("dictionaryCacheTime={0}ms,genericCacheTime={1}ms"
            , dictionaryCacheTime, genericCacheTime);
    }
}
    Console.WriteLine("****************Monitor****************");
    {
        Monitor.Show();
    }

image

字典缓存读取结果是要通过哈希查找得到,而泛型缓存是直接拿内存地址得到结果,效率会高很多,但是也有局限性,只能适合不同类型,需要缓存一份数据的场景。