Eleven老师之泛型讲解(包括泛型的协变和逆变)

1.使用泛型的好处,请看如下代码

//请按照step顺序逐步阅读
class Program
{
    static void Main(string[] args)
    {
        //step1: 如下我们需要根据不同的参数类型调用不同的方法
        MyGeneric.ShowInt(1);
        MyGeneric.ShowString("1");
        MyGeneric.ShowDateTime(DateTime.Now);
        Console.WriteLine("--------------------------------");

        //step2: 思考:我们可以让不同类型的参数调用同一个方法吗?
        //答:可以,如下. 
        MyGeneric.ShowObject(1);
        MyGeneric.ShowObject("1");
        MyGeneric.ShowObject(DateTime.Now);
        //上面的输出如下,value还可以理解,type居然不显示System.Object,这就有意思了
        //value: 1__type: System.Int32
        //value:1__type: System.String
        //value:2021 / 12 / 29 上午 09:41:34__type: System.DateTime
        //思考: 上面那样做真的没什么问题吗? 答:有问题,1.装箱拆箱造成性能损失            
        //(DateTime.Now居然是struct,即是值类型)
        //2.类型安全问题,ShowObject没办法约束传入数据的类型

        //step3: 思考:那我们有什么方法,既可以让不同类型的参数调用同一个方法,
        //又没有装箱拆箱及类型安全问题.
        //答:有,我们可以使用泛型方法,如下:
        MyGeneric.Show<int>(1);
        MyGeneric.Show<string>("1");
        MyGeneric.Show<DateTime>(DateTime.Now);  //<>里面的类型是灰色的,说明其可以省略,因为编译器可以根据实参类型推断出其类型

        Console.ReadKey();
    }
}
public class MyGeneric
{
    public static void ShowInt(int para)
    {
        Console.WriteLine($"value:{para}__type:{para.GetType()}");
    }
    public static void ShowString(string para)
    {
        Console.WriteLine($"value:{para}__type:{para.GetType()}");
    }
    public static void ShowDateTime(DateTime para)
    {
        Console.WriteLine($"value:{para}__type:{para.GetType()}");
    }
    
    public static void ShowObject(object para)//没办法约束传入数据的类型
    {
        Console.WriteLine($"value:{para}__type:{para.GetType()}");
        //(Form)para  如果传入的类型不能转换成Form就会报错
    }
    
    public static void Show<T>(T para)//泛型方法在方法后面有个类型参数
    {
        Console.WriteLine($"value:{para}__type:{para.GetType()}");
    }

}

2.泛型的类型参数问题

class Program
{
    static void Main(string[] args)
    {
        //MyGeneric.Method<int,string,DateTime>(1,"1",10);
        Console.ReadKey();
    }
}
public class MyGeneric
{
    //泛型方法的类型参数可以有多个,在参数列表里可以不使用类型参数,如下,
    //只是在调用该泛型方法时就不能从实参数据类型推断出来T的类型,需要调用的时候声明其类型,如上
    public static void Method<R,S,T>(R r,S s,int n)
    {
        //逻辑代码
    }
}
class Program
{
    static void Main(string[] args)
    {
        MyGeneric<DateTime>.Method<int,string>(DateTime.Now,1,"1");
        Console.ReadKey();
    }
}
//还可以这个样子如下,调用时如上
public class MyGeneric<R>
{
    public static void Method<S,T>(R r,S s,T t)
    {
        //逻辑代码
    }
}

3.深入探讨泛型的工作原理

泛型是在.NetFramework2.0出现的,在这一版本的编译器和CLR/JIT做了很大的很大的升级.此时的编译器和CLR/JIT开始支持泛型.编译器在编译过程中遇到泛型会生成一个占位符,在CLR/JIT编译过程中遇到泛型时(此时类型参数已经确定)会将其编译成确定类型的代码(就等同于原生代码),不同的类型会产生不同的副本,如下图:

在这里插入图片描述

class Program
{
    //Diagnostic: 诊断
    static long commonSecond = 0;
    static long objectSecond = 0;
    static long genericSecond = 0;
    static void Main(string[] args) 
    {
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i < 100000000; i++)
            {
                MyGeneric.ShowInt(1);
            }
            watch.Stop();
            commonSecond = watch.ElapsedMilliseconds;
        }
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i < 100000000; i++)
            {
                MyGeneric.ShowObject(1);//这个涉及装箱和拆箱
            }
            watch.Stop();
            objectSecond = watch.ElapsedMilliseconds;
        }
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i < 100000000; i++)
            {
                MyGeneric.ShowGeneric(1);
            }
            watch.Stop();
            genericSecond = watch.ElapsedMilliseconds;
        }
        Console.WriteLine($" commonSecond:{commonSecond}\n objectSecond:{objectSecond}\n genericSecond:{genericSecond}");
        Console.ReadKey();
    }
}
public class MyGeneric
{
    public static void ShowInt(int n) { }
    public static void ShowObject(object n) { }
    public static void ShowGeneric<T>(T n) { }
}

经过上面代码运行可以知道:ShowObject方法涉及装箱拆箱有性能损失.按照原理来说ShowInt方法耗时要比ShowGeneric少,但是测试结果却不是这样,好奇怪!

        

4.泛型缓存

class Program
{
    static void Main(string[] args)
    {
        ///JIT会根据不同的T产生不同副本,相当于产生不同的类
        string data;
        for (int i=0;i<3;i++)
        {
            data = MyGenericCache<int>.GetData();
            data = MyGenericCache<string>.GetData();
            data = MyGenericCache<DateTime>.GetData();
        }
        Console.ReadKey();
    }
}
/// <summary>
/// 泛型缓存
/// 1.当用到类MyGenericCache<T1>时,该类就会被加载,先初始化静态字段cacheData并保存至静态
/// 存储区,然后执行静态构造函数MyGenericCache,如果静态方法GetData没被用到是不会被
/// 加载到静态存储区,被用到后才会(延迟加载). 当再次用到类MyGenericCache<T1>时,静态字段
/// cacheData/构造函数MyGenericCache就不会在被执行,因为静态成员只会被加载一次
/// </summary>
public class MyGenericCache<T>
{
    private static string cacheData = null;
    static MyGenericCache()
    {
        cacheData = typeof(T).FullName + "__" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff");
    }
    public static string GetData()
    {
        return cacheData;
    }
}
/// <summary>
/// 普通缓存--静态字典缓存
/// </summary>
public class CustomCache
{
    private static Dictionary<string, string> customCacheDictionary = new Dictionary<string, string>();
    //一种类型对应一个值
    public string Get<T>()
    {
        string key = typeof(T).FullName;
        if (customCacheDictionary.ContainsKey(key))
        {
            return customCacheDictionary[key];
        }
        else
        {
            string value = typeof(T).FullName + "__" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff");
            customCacheDictionary[key] = value;
            return value;
        }
    }
}

泛型缓存:只能针对类型进行缓存,可以是类型组合,但是性能高,除了内存损耗几乎无性能损耗.

字典缓存:hash寻址,用的空间也比较大.

5.泛型五大约束:基类约束、接口约束、new()约束、引用类型约束、值类型约束

        a.基类约束:类型T必须是基类C1或C1的派生类

                public class GenericTest<T> where T : C1 {}

        b.接口约束:类型T必须实现了I1接口的类

                public class GenericTest<T> where T : I1 {}

        c.new() 约束:类型T必须有公有无参的构造函数;new()约束仅允许使用无参的构造函数在类GenericTest<T>内部构建对象,即使类型T有其他的有参构造函数也不能使用;与其他约束一起使用时new()必须放在最后;不可以同时使用new()约束和值类型约束,因为值类型和new()都隐式的提供了一个无参公共构造函数。

                public class GenericTest<T> where T : new() {}        

        d.引用类型约束:类型T必须是引用类型的

                public class GenericTest<T> where T : class

        e.值类型约束:类型T必须是值类型的

                 public class GenericTest<T> where T : struct

        以上各种泛型约束可以进行组合,形成组合约束. 组合约束要按一定顺序才能符合要求,new()约束要放在最后一个,接口约束放在倒数第二.

        如果只有一个接口约束或基类约束,可以不用写约束,如下:

                public class GenericTest<C1 T> 

                public class GenericTest<I1 T> 

6.泛型协变和逆变(看官方文档对协变和逆变的解释请点我)

只能放在接口和委托的类型参数的前面,协变保留分配兼容性,逆变则与之相反。

协变和逆变的作用:协变和逆变能够实现数组类型委托类型泛型类型参数的隐式转换。

        out 协变 covariant 修饰返回类型参数

        in 逆变 contravariant 修饰传入类型参数

//协变
IEnumerable<string> strings = new List<string>();
//协变   public interface IEnumerable<out T> : IEnumerable
//如下,数组的协变能够使派生程度更大的类型的数组隐式转换为派生程度更小的类型的数组
IEnumerable<object> objects = strings;
//但是,此操作不是类型安全的操作,如下面示例代码
object[] array = new String[10];
array[0] = 10;  //此处会报错,因为array[0]已经先分配给string了,无法再接受整型。
array[0] = "10"; //这样就不会报错了

对于方法组的协变和逆变允许将方法签名和委托类型相匹配.这样,不仅可以将匹配签名的方法分配给相应的委托,还可以给委托分配与委托指定的返回类型相比返回类型派生程度更大的方法(协变)或给委托分配与委托指定的输入类型相比输入类型派生程度更小的方法(逆变),如下:

public class CovarianceAndContravariance
{
    static object GetObject() { return null; }
    static void SetObject(object obj) { }
    static string GetString() { return ""; }
    static void SetString(string str) { }
    static void Test()
    {
        Func<object> del = GetString;//协变
        Action<string> del2 = SetObject;//逆变
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值