引言:
本人从事.Net开发已很多年了,最近有家公司约我面试高级开发。怀着看看的心情就答应了。见面时对方直接给我一试题,好多年没换工作了,平时工作遇到问题都baidu解决的多。很少去记忆代码语句或概念。给我一份A4纸的面试题与一支笔,都不知怎么下手了! 唉,是我太弱鸡还是这样的面试太死板了呢? 了解一下还真很多公司是这样的。
泛型的使用比较多,相信大家都会使用。什么是泛型。原理是什么。有什么好处,要注意什么等等问题时你怎么回答? 下面来整理一下吧。
1.什么是泛型
泛型就是不确定类型的,被调用时才指定类型的数据类型。
泛型(Generic) 允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或方法。
您可以通过数据类型的替代参数编写类或方法的规范。当编译器遇到类的构造函数或方法的函数调用时,它会生成代码来处理指定的数据类型。
泛型的特性
使用泛型是一种增强程序功能的技术,具体表现在以下几个方面:
- 它有助于您最大限度地重用代码、保护类型的安全以及提高性能。
- 您可以创建泛型集合类。.NET 框架类库在 System.Collections.Generic 命名空间中包含了一些新的泛型集合类。您可以使用这些泛型集合类来替代 System.Collections 中的集合类。
- 您可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
- 您可以对泛型类进行约束以访问特定数据类型的方法。
- 关于泛型数据类型中使用的类型的信息可在运行时通过使用反射获取。
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.泛型约束,泛型缓存
举个例子:
最终得出结果:
由图可以看出在第一次循环的时候都会进入构造函数,后续则不会,这是因为每次泛型根据被调用的参数类型都会拷贝一份、生成新的副本、类,所以每次都会进入构造函数里面,后续则因为已经拷贝则不在进入构造函数,这些拷贝的副本、类叫做泛型缓存,泛型缓存的本质就是泛型类。
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类:
/// <summary>
/// out 协变covariant 修饰返回值
/// in 逆变contravariant 修饰传入参数
/// </summary>
public class ContravariantCovariance
{
public static void Show()
{
{
Animal animal1 = null;
animal1 = new Animal();
///任何父类出现的地方都可以用子类来代替
Animal animal2 = null;
animal2 = new Cat();
Cat cat1 = null;
cat1 = new Cat();
//Cat cat2 = null;
//cat2 = new Animal(); //Animal不一定等于cat,不严密,存在风险,会出错
//Cat cat2 = null;
//cat2 = (Cat)(new Animal()); // 类型不安全,暂时不会出错,但是类型不安全,容易出错,不严密
}
{
List<Animal> animalList1 = null;
animalList1 = new List<Animal>();
//泛型类在我们指定了类型以后,其实是一个新的类,/List<Animal>与List<Cat>()并没有继承关系,所以无法转换类型,会出错
//List<Animal> animalList2 = null;
//animalList2 = new List<Cat>(); //一组Cat 一定是一组Animal,口语上的理解和代码冲突了;
//为什么不能这样写? 是因为是两个不同的类,也没有继承关系; 在C# 语法中,只能用父类声明子类的实例
//泛型存在不和谐的地方;
//在这类场景下,编译器也有不完美的地方---编译器错了;
}
//就引入了协变和逆变
{
//协变 就可以让右边用子类
IEnumerable<Animal> animalList1 = new List<Animal>();
IEnumerable<Animal> animalList2 = new List<Cat>(); //这个可以 ,相当于解决了刚刚编译器不完美的点
//这样写,才符合常理;
//有没有其他别的问题?
//会存在风险;
//协变: Out 只能做返回值 ,不能做参数,其实是一种为了避开风险而存在的一种约束
ICustomerListOut<Animal> customerList1 = new CustomerListOut<Animal>();
ICustomerListOut<Animal> customerList2 = new CustomerListOut<Cat>(); //协变
//customerList2.Show(new Animal()); //这个还能玩吗? 当然不行;
//customerList2.Show(new Cat());
}
{//逆变 In 只能做参数 ,不能做返回值,其实是一种为了避开风险而存在的一种约束
//逆变:就可以让右边用父类;
ICustomerListIn<Cat> customerList2 = new CustomerListIn<Cat>();
//ICustomerListIn<Cat> customerList1 = new CustomerListIn<Animal>();
//customerList1.Get();//调用的是接口的方法
//customerList1.Get(); //返回的一定是一个Cat 或者是Cat 的子类;
//因为通过接口在调用方法的时候只能返回一个Cat,
}
//协变逆变的存在,就是为了满足常规场景添加一个避开风险的约束;
{
IMyList<Cat, Animal> myList1 = new MyList<Cat, Animal>();
IMyList<Cat, Animal> myList2 = new MyList<Cat, Cat>();//协变
IMyList<Cat, Animal> myList3 = new MyList<Animal, Animal>();//逆变
IMyList<Cat, Animal> myList4 = new MyList<Animal, Cat>();//协变+逆变
}
//协变和逆变 只有在泛型接口和泛型委托中存在;
//协变 协变: Out 只能做返回值 ,不能做参数,可以让右边使用子类
//逆变 逆变: in 只能做参数 ,不能做返回值,可以让右边使用父类
//可以毫无顾忌的 去声明;
}
//协变是为了约束,不让输入父类。逆变是为了约束,不让返回父类!是这样理解吧!为了安全
//架构师的层面来谈这个问题: 可能会出现问题地方,让你在使用的时候就直接编译不过;而不是口头约束;
}
/// <summary>
/// 动物
/// </summary>
public class Animal
{
public int Id { get; set; }
}
/// <summary>
/// Cat 猫
/// </summary>
public class Cat : Animal
{
public string Name { get; set; }
}
/// <summary>
/// T 就只能做参数 不能做返回值
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ICustomerListIn<in T>
{
//T Get();
void Show(T t);
}
public class CustomerListIn<T> : ICustomerListIn<T>
{
//public T Get()
//{
// return default(T);
//}
public void Show(T t)
{
}
}
/// <summary>
/// out 协变 只能是返回结果
/// 泛型T 就只能做返回值; 不能做参数;
/// </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);
}
//public void Show(T t)
//{
//}
}
public interface IMyList<in inT, out outT>
{
void Show(inT t);
outT Get();
outT Do(inT t);
out 只能是返回值 in只能是参数
//void Show1(outT t);//左边声明是父类---右边实例化是子类 new MyList<Cat, Cat>()---outT是cat--方法调用时以左边为准,传递的是animal的子类-狗
//inT Get1();
}
/// <summary>
/// out 协变 只能是返回结果
/// in 逆变 只能是参数
///
/// </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);
}
}
协变逆变的理解基于一句话: 子类可以代替父类并且出现在父类能够出现的地方,但是父类并不能够代替子类。简而言之就是类型冲突 子类与父类。