c#泛型的理解
1.什么是泛型:
泛型就是不确定类型的,被调用时才指定类型的数据类型。
2.如何声明、使用泛型、泛型的好处:
如图所示:我们写了四个不相同类型的方法并且调用,但是我们每个不一样的类型都要重新多写一个方法就会很麻烦,根据 在c#中"object是所有类的基类"这句话 和里氏替换原则:子类可以替代父类并且出现在父类能出现的地方,我们可以得出下图案例:
如图所示:这不是就是相当解决我们的上面的问题了吗。但是这样写也会存在一些问题。
1.性能问题(装箱和拆箱),其实就是类型转换对性能的损耗
2.类型不安全问题
在这里我写了三个方法分别是普通方法,obj方法,泛型方法,为了公平起见我里面没有调用任何东西,分别循环一亿次来查看时间。
如上图所示:普通方法用的时间是347毫秒,obj方法用的是1071毫秒,泛型方法用的是305毫秒,obj方法的性能要比泛型方法差很多。
为什么呢?这就涉及到了装箱和拆箱,即obj方法在调用的时候要转换类型,(byte,short,int,long,float,double,decimal,char,bool 和 struct 统称为值类型。string,类等皆为引用类型。),装箱的操作会损耗性能。而泛型方法是在调用的时候才确定类型转换成普通方法,普通方法性能好是必然的。
Model类:
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 i) //{ } 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($"打乒乓球..."); } }
GenericConstraint类:
public static void ShowObject(object oParameter) { //Console.WriteLine("This is {0},parameter={1},type={2}", // typeof(CommonMethod), oParameter.GetType().Name, oParameter); //Console.WriteLine($"People.Id={oParameter.Id}"); People people = (People)oParameter; Console.WriteLine($"People.Id={people.Id}"); Console.WriteLine($"People.Name={people.Name}"); }
Program:
try{ { People people = new People() { Id = 123, Name = "You" }; Chinese chinese = new Chinese() { Id = 234, Name = "7z" }; Hubei hubei = new Hubei() { Id = 345, Name = "晴天橙子" }; Japanese japanese = new Japanese() { Id = 456, Name = "苍老师" }; GenericConstraint.ShowObject(people); GenericConstraint.ShowObject(chinese); GenericConstraint.ShowObject(hubei); GenericConstraint.ShowObject(japanese); } } catch (Exception ex) { throw; }
从这里可以看出到了参数为 japanese的时候会报错。为什么呢?因为 Chinese继承于people,hubei继承于Chinese但是japanese继承于ISports ,在GenericConstraint类中是进行一个转换的,转成people,不然会报错,因为obj类型没有id和name,这里就不直接写出来了。这就是类型冲突,不匹配,不安全。
3.泛型的原理:
如上图所示:写入c#泛型语法→右键重新生成→就会生成这两个文件,也有可能是其中一个→运行时的二次编译,也就是这个时候确定泛型类型进而生成普通方法→转换成计算机能够识别的二进制编码。
那么T在声明时候经过编译是什么样子的呢?
如上图所示:T在声明的时候回形成占位符~ List传参位数是1就是~1 dictionart是~2,最终生成普通方法。
4.泛型的延迟声明:
从上图可以看出泛型是在被调用的时候才确定参数类型的;延迟声明:在开发中,延迟一切能够延迟的处理,能晚点做,就尽量晚点做。
5.泛型类、方法、接口、委托
泛型方法:一个方法满足了不同类型的需求
泛型类:一个类满足不同类的需求
泛型接口:一个接口满足不同接口的需求
泛型委托:一个委托满足不同委托的需求
6.泛型约束,泛型缓存
举个例子:
最终得出结果:
由图可以看出在第一次循环的时候都会进入构造函数,后续则不会,这是因为每次泛型根据被调用的参数类型都会拷贝一份、生成新的副本、类,所以每次都会进入构造函数里面,后续则因为已经拷贝则不在进入构造函数,这些拷贝的副本、类叫做泛型缓存,泛型缓存的本质就是泛型类。
在第二点的时候我们说到了obj类型不安全的问题,下面用泛型约束来解决这个问题:
GenericConstraint类:
public static void Show<T>(T tParameter)//基类约束 where T : People { { ////People people = (People)oParameter; Console.WriteLine($"People.Id={tParameter.Id}");//调用字段
Console.WriteLine($"People.Name={tParameter.Name}");
tParameter.Hi();//调用方法
} }
public static void IShow<T>(T tParameter) where T : ISports //接口约束
{
tParameter.Pingpang();//调用接口里面的方法
}
public static void NShow<T>(T tParameter) where T : new()//无参数构造函数约束
{
//在Model中的Hubei类加入 public Hubei(int i){ } 一个有参构造函数
}
//在编写之前要把上面的Hubei类的有参构造函数注释或者删掉
/// <summary>
/// 值类型约束
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tParameter"></param>
public static void ShowStruct<T>(T tParameter) where T : struct
{
}
/// <summary>
///引用类型约束
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tParameter"></param>
public static T ShowClass<T>(T tParameter) where T : class
{
return default(T); //任何类型都可以,default(T) 根据实际的调用类来生成默认值;
}
//也可以有多重约束如:
public static void ShowInfo<T>(T tParameter)
where T : People,ISports //只要不冲突即可。
{
}
Program:
//基类约束
GenericConstraint.Show<People>(people); GenericConstraint.Show(chinese); GenericConstraint.Show(hubei); //GenericConstraint.Show(japanese);//这里会出错,因为在GenericConstraint.show中使用了基类约束People,所以会导致类型不一致,禁止传入错误参数。
//接口约束
//GenericConstraint.IShow<People>(people);//这里会出错,因为people与ISports类型冲突,没有继承关系。
GenericConstraint.IShow(chinese);
GenericConstraint.IShow(hubei);
GenericConstraint.IShow(japanese);
//无参构造函数约束
GenericConstraint.NShow<People>(people); GenericConstraint.NShow(chinese); //GenericConstraint.NShow(hubei);//这里会报错,因为他有一个有参构造函数,类在声明的时候即使不去写也默认有一个无参构造函数,这就是另外几个类能传入的原因。 GenericConstraint.NShow(japanese);
//值类型约束
//下面都会出错,因为只能传入值类型,(byte,short,int,long,float,double,decimal,char,bool 和 struct 统称为值类型。string,类等皆为引用类型。)。
GenericConstraint.NShow<People>(people); GenericConstraint.NShow(chinese); GenericConstraint.NShow(hubei); GenericConstraint.NShow(japanese);
//引用类型约束
//下面皆正常,因为(byte,short,int,long,float,double,decimal,char,bool 和 struct 统称为值类型。string,类等皆为引用类型。)。
GenericConstraint.NShow<People>(people); GenericConstraint.NShow(chinese); GenericConstraint.NShow(hubei); GenericConstraint.NShow(japanese);
约束总结:(根据上面的代码来理解)
1.基类约束:约束这个T 就是People; 就约束只能传递People或者People的子类
2.接口约束:约束这个T 是ISports接口的实现类
可以调用接口中的方法---权利;
调用的时候--只能传递实现过这个接口的类进入--义务
3.无参数构造函数约束:传入的参数必须有拥有一个无参数构造函数
4.值类型约束:只能传入值类型的参数
5.引用类型约束:只能传入引用类型的参数
7.协变逆变
ContravariantCovariance类:
1 /// <summary> 2 /// out 协变covariant 修饰返回值 3 /// in 逆变contravariant 修饰传入参数 4 /// </summary> 5 public class ContravariantCovariance 6 { 7 public static void Show() 8 { 9 { 10 Animal animal1 = null; 11 animal1 = new Animal(); 12 13 ///任何父类出现的地方都可以用子类来代替 14 Animal animal2 = null; 15 animal2 = new Cat(); 16 17 Cat cat1 = null; 18 cat1 = new Cat(); 19 20 //Cat cat2 = null; 21 //cat2 = new Animal(); //Animal不一定等于cat,不严密,存在风险,会出错 22 23 //Cat cat2 = null; 24 //cat2 = (Cat)(new Animal()); // 类型不安全,暂时不会出错,但是类型不安全,容易出错,不严密 25 } 26 27 { 28 29 List<Animal> animalList1 = null; 30 animalList1 = new List<Animal>(); 31 32 //泛型类在我们指定了类型以后,其实是一个新的类,/List<Animal>与List<Cat>()并没有继承关系,所以无法转换类型,会出错 33 //List<Animal> animalList2 = null; 34 //animalList2 = new List<Cat>(); //一组Cat 一定是一组Animal,口语上的理解和代码冲突了; 35 36 //为什么不能这样写? 是因为是两个不同的类,也没有继承关系; 在C# 语法中,只能用父类声明子类的实例 37 //泛型存在不和谐的地方; 38 //在这类场景下,编译器也有不完美的地方---编译器错了; 39 40 } 41 42 //就引入了协变和逆变 43 { 44 //协变 就可以让右边用子类 45 IEnumerable<Animal> animalList1 = new List<Animal>(); 46 IEnumerable<Animal> animalList2 = new List<Cat>(); //这个可以 ,相当于解决了刚刚编译器不完美的点 47 48 //这样写,才符合常理; 49 //有没有其他别的问题? 50 //会存在风险; 51 52 53 54 //协变: Out 只能做返回值 ,不能做参数,其实是一种为了避开风险而存在的一种约束 55 ICustomerListOut<Animal> customerList1 = new CustomerListOut<Animal>(); 56 ICustomerListOut<Animal> customerList2 = new CustomerListOut<Cat>(); //协变 57 //customerList2.Show(new Animal()); //这个还能玩吗? 当然不行; 58 //customerList2.Show(new Cat()); 59 60 61 } 62 {//逆变 In 只能做参数 ,不能做返回值,其实是一种为了避开风险而存在的一种约束 63 64 //逆变:就可以让右边用父类; 65 ICustomerListIn<Cat> customerList2 = new CustomerListIn<Cat>(); 66 //ICustomerListIn<Cat> customerList1 = new CustomerListIn<Animal>(); 67 68 //customerList1.Get();//调用的是接口的方法 69 70 //customerList1.Get(); //返回的一定是一个Cat 或者是Cat 的子类; 71 //因为通过接口在调用方法的时候只能返回一个Cat, 72 } 73 //协变逆变的存在,就是为了满足常规场景添加一个避开风险的约束; 74 { 75 IMyList<Cat, Animal> myList1 = new MyList<Cat, Animal>(); 76 IMyList<Cat, Animal> myList2 = new MyList<Cat, Cat>();//协变 77 78 IMyList<Cat, Animal> myList3 = new MyList<Animal, Animal>();//逆变 79 IMyList<Cat, Animal> myList4 = new MyList<Animal, Cat>();//协变+逆变 80 } 81 82 //协变和逆变 只有在泛型接口和泛型委托中存在; 83 84 //协变 协变: Out 只能做返回值 ,不能做参数,可以让右边使用子类 85 //逆变 逆变: in 只能做参数 ,不能做返回值,可以让右边使用父类 86 //可以毫无顾忌的 去声明; 87 } 88 //协变是为了约束,不让输入父类。逆变是为了约束,不让返回父类!是这样理解吧!为了安全 89 90 //架构师的层面来谈这个问题: 可能会出现问题地方,让你在使用的时候就直接编译不过;而不是口头约束; 91 } 92 93 /// <summary> 94 /// 动物 95 /// </summary> 96 public class Animal 97 { 98 public int Id { get; set; } 99 } 100 101 /// <summary> 102 /// Cat 猫 103 /// </summary> 104 public class Cat : Animal 105 { 106 public string Name { get; set; } 107 } 108 109 /// <summary> 110 /// T 就只能做参数 不能做返回值 111 /// </summary> 112 /// <typeparam name="T"></typeparam> 113 public interface ICustomerListIn<in T> 114 { 115 //T Get(); 116 117 void Show(T t); 118 } 119 120 public class CustomerListIn<T> : ICustomerListIn<T> 121 { 122 //public T Get() 123 //{ 124 // return default(T); 125 //} 126 127 public void Show(T t) 128 { 129 130 } 131 } 132 133 /// <summary> 134 /// out 协变 只能是返回结果 135 /// 泛型T 就只能做返回值; 不能做参数; 136 /// </summary> 137 /// <typeparam name="T"></typeparam> 138 public interface ICustomerListOut<out T> 139 { 140 T Get(); 141 142 //void Show(T t); 143 } 144 145 public class CustomerListOut<T> : ICustomerListOut<T> 146 { 147 public T Get() 148 { 149 return default(T); 150 } 151 152 //public void Show(T t) 153 //{ 154 155 //} 156 } 157 158 public interface IMyList<in inT, out outT> 159 { 160 void Show(inT t); 161 outT Get(); 162 outT Do(inT t); 163 164 ////out 只能是返回值 in只能是参数 165 //void Show1(outT t);//左边声明是父类---右边实例化是子类 new MyList<Cat, Cat>()---outT是cat--方法调用时以左边为准,传递的是animal的子类-狗 166 //inT Get1(); 167 } 168 169 /// <summary> 170 /// out 协变 只能是返回结果 171 /// in 逆变 只能是参数 172 /// 173 /// </summary> 174 /// <typeparam name="T1"></typeparam> 175 /// <typeparam name="T2"></typeparam> 176 177 public class MyList<T1, T2> : IMyList<T1, T2> 178 { 179 public void Show(T1 t) 180 { 181 Console.WriteLine(t.GetType().Name); 182 } 183 184 public T2 Get() 185 { 186 Console.WriteLine(typeof(T2).Name); 187 return default(T2); 188 } 189 190 public T2 Do(T1 t) 191 { 192 Console.WriteLine(t.GetType().Name); 193 Console.WriteLine(typeof(T2).Name); 194 return default(T2); 195 } 196 }
协变逆变的理解基于一句话: 子类可以代替父类并且出现在父类能够出现的地方,但是父类并不能够代替子类。简而言之就是类型冲突 子类与父类。