泛型方法
假设我们要编写一个方法,它可以获取任意类型数组中的最大值,
并返回该值。我们可能会这样写:
static int GetMax(int[] array)
{
Array.Sort(array);
return array[array.Length - 1];
}
这个方法可以实现我们的需求,但是它只能处理整数类型。
如果我们想获取字符串数组或浮点数数组中的最大值,就需要重新定义一个新的方法,如下:
static string GetMax(string[] array)
{
Array.Sort(array);
return array[array.Length - 1];
}
static double GetMax(double[] array)
{
Array.Sort(array);
return array[array.Length - 1];
}
这样做显然很冗余,而且每增加一种数据类型,就需要增加一个新的方法。
这样不仅增加了代码量,也增加了出错和维护的难度。
声明和使用
就像我们把变量作为参数一样,泛型是类型版本的参数。
泛型的“形参”称为类型占位符,泛型的“实参”称为类型参数。
声明一个泛型的形参声明方法是在函数名和函数参数之间写一对尖括号。
尖括号中你可以任意起名字,多个标识符使用逗号隔开。
然后在这个函数中,以及参数和返回值中,这个类型会被认为是有效的类型。
static T GetMax<T>(T[] array)
{
Array.Sort(array);
return array[array.Length - 1];
}
这个方法使用了一个类型参数 T 来代替具体类型。
这个方法可以获取任意类型数组中的最大值,
要使用一个泛型方法,我们需要在方法名后面使用尖括号指定类型实参。
int maxInt = GetMax<int>(new int[] { 1, 2, 3 }); // 获取整数数组中的最大值
string maxString = GetMax<string>(new string[] { "a", "b", "c" }); // 获取字符串数组中的最大值
这些调用都指定了类型实参为 int 或 string,并且传递了相应类型的数组作为参数。
这样,编译器就可以根据类型实参来检查类型安全,并且生成相应的执行代码。
类型推断
如果泛型方法在使用时,仅靠参数就能推断出所有的泛型参数,可以省略类型实参。
类似var
,或default
,或null
,等值是无法推断出类型的。
_ = Enum.TryParse("Red", out Color color); // 省略了类型实参
_ = Enum.TryParse<Color>("Red", out var color2);//无法从var推断出类型
enum Color { Red, Green, Blue }
泛型类
泛型类是一种在类型声明时使用泛型类型参数的类,这样可以创建泛型字段和泛型属性。
泛型类的泛型参数数量也视为类型签名之一。签名不同的类可以共存。
class NonGenericList<T>
{
T[] items;
public T this[int index]
{
get => items[index];
set => items[index] = value;
}
public T SetItems(params T[] items)
{
this.items = items;
return items[0];
}
}
class NonGenericList
{
int[] items;
public int this[int index]
{
get => items[index];
set => items[index] = value;
}
public int SetItems(params int[] items)
{
this.items = items;
return items[0];
}
}
泛型类的构造器在声明时不需要(不能)加泛型类型参数。
但在调用时必须要加泛型类型参数,即使可以从参数中推断出来。
静态字段
不同泛型参数的泛型类之间是不同的。他们各自拥有自己的静态成员。
StaticValue<int>.Value = 10;
StaticValue<double>.Value = 20;
Console.WriteLine(StaticValue<int>.Value + StaticValue<double>.Value + StaticValue<string>.Value);//35
static class StaticValue<T>
{
public static int Value = 5;
}
继承
泛型类可以继承其他类,也可以被继承。
在被继承时,需要有效的类型参数,可以是实际的类型,也可以是另一个泛型类的占位符。
class Base<T>
{
}
class Derive : Base<int>
{
}
class Derive<T> : Base<T>
{
}
泛型约束
泛型约束是指在定义泛型类型或方法时,对泛型类型参数进行一些限制,
使其具有更多的功能和操作。
如果不使用泛型约束,泛型类型参数会被假设为任何类型(object),
这样会导致只能使用所有类型都共有的功能(如ToString()方法),非常受限。
为了解决这个问题,我们可以为泛型类型参数添加一个或多个约束。
虽然这样会减少能兼容的类型范围,但是可以增加可用的操作。
如何声明约束
在函数的参数列表后,或泛型类的泛型类型参数后使用where
关键字+:
指定约束。
一个类型参数的多个约束之间使用逗号隔开。
为多个类型参数指定约束需要使用多个where,没有分隔符。
class Foo<T> where T : class
{
public void Boo<E, F>() where E : class, new() where F : class
{
}
}
常用的约束
派生自类
如果要求泛型类型参数代表的类型派生自某一类型,
只需要直接写类型名。如果只要求是一个引用类型,那么使用class。
显然,类型约束不能是密封类或静态类。类型名可以是另一个泛型类型参数
具有类型约束后,可以调用此类型有权限访问的实例方法和属性。
是结构
是一种值类型,使用struct。
不能是可为空的值类型使用notnull。
必须是非托管类型使用unmanaged。
非托管类型是指不包含任何引用类型字段的值类型,
且它的字段的字段,一直递归下去,都不包含。
实现接口
实现接口只需要直接写接口名。
但是正如继承时的语法一样,接口约束必须写在类约束或结构约束之后。
默认情况下,可以通过泛型类型参数隐式地调用接口的所有方法。
也就是说可以直接从参数中调用接口的方法。
在多个接口存在同名方法时需要转化为接口再调用。
如果接口存在静态抽象方法,则可以通过泛型类型参数访问静态方法。
void Boo<T>(T t) where T : IInterface
{
T.aa();
}
interface IInterface
{
public static abstract void aa();
}
其他
new()约束:这个约束要求目标类型具有公共无参构造器。具有此约束的类型可以在函数中调用这个构造器。
new()约束必须放在约束列表的最后一个。
default约束:这个约束名字没起好,改为base约束更容易理解。他的实际作用是在重写或显式实现接口时
表示基方法或基类的约束,但class和struct约束除外。
互斥的约束
不能同时存在的约束,要么是因为不兼容,要么是因为有包含关系。
不能兼容的约束:类约束和结构约束。
有包含的约束:unmanaged约束一定是一种struct约束,struct约束一定是一种new()约束。
这是我对你的表述的优化版本,希望对你有帮助。如果你还有其他需要,请告诉我。😊
协变逆变
泛型赋值
泛型列表是一种可以存储任意类型数据的集合。
你可以把它看作是一个可以动态扩展大小的数组。
例如,下面是两个泛型列表,一个存储字符串类型(string),一个存储对象类型(object)。
List<string> strList = new List<string>() { "123", "456", "789" };
List<object> objList = new List<object>(3);
现在我们想把第一个列表中的元素复制到第二个列表中。
我们可以用一个循环来遍历第一个列表,并把每个元素赋值给第二个列表对应的位置。
for (int i = 0; i < strList.Count&&i<objList.Count; i++)
{
objList[i]= strList[i];
}
这样做是没有问题的,因为string
类型是object
的子类,所以可以把字符串类型赋值给对象类型。
但是如果我们不访问元素,直接把整个列表赋值给另一个列表呢?像这样:
List<string> strList = new List<string>() { "123", "456", "789" };
List<object> objList = strList;
这样是不行的。但是数组却可以这样做:
string[] strArr = new string[] { "123", "456", "789" };
object[] objArr = strArr;
objArr[0] = 12;
这里我们把一个字符串数组赋值给了一个对象数组,
然后试图把一个整数赋值给对象数组的第一个元素。
这在编译时是可以通过的,但实际运行起来,会出现类型转换失败的异常。
这是因为object
数组实际上还是只能储存string
类型的元素。不能接收int
值。
协变和逆变
泛型具有严格的限制,像上面那种使用方式是不允许的。
而协变和逆变就是用来解开这种限制的。
但是协变和逆变也有限制,不能对类使用,只能对接口或委托使用。
因为他们不会储存字段,只有方法。
协变
协变:和谐的变化,子类随着时间流逝会当父类。泛型填的是子类,可以迎合父类。
使用out关键字修饰泛型占位符,表示输出。修饰的泛型占位符仅能作为返回类型。
interface IOut<out T>
{
T GetValue();
}
class COut : IOut<string>
{
public string GetValue() => "hello";
}
IOut<string> out1 = new COut();
IOut<object> out2 = out1;
因为这个接口中只有输出的泛型。
在输出的时候把string作为object看待是没有问题的。
逆变
逆变:大逆不道的变化,要父类当子类。泛型填的是父类,却要迎合子类。
使用in关键字修饰泛型占位符,表示输入。修饰的泛型占位符仅能作为参数类型。
interface IIn<in T> {
void SetValue(T value);
}
class CIn : IIn<object>
{
public void SetValue(object value) { }
}
IIn<object>in1=new CIn();
IIn<string> in2 = in1;
因为这个接口中只有参数的泛型,
在作为参数的时候,把string作为object看待是没有问题的。
而泛型类中的字段,是既能作为输入,又能作为输出的存在。
所以协变和逆变对泛型类不可用。
协变和逆变不能把泛型参数装箱拆箱
所有的值类型不能参与协变和逆变。
class CT<T> : IOut<T>, IIn<T>
{
T IOut<T>.GetValue() => throw new NotImplementedException();
void IIn<T>.SetValue(T value) => throw new NotImplementedException();
}
CT<Stream> ct = new CT<Stream>();
IIn<FileStream> Iin = ct;
IOut<object> Iout = ct;
Iout = new CT<int>();
IIn<int> Iin2 = new CT<object>();
这是因为值类型需要装箱和拆箱才能转换成对象类型或其他值类型,
而协变和逆变不会进行装箱和拆箱操作。