这是一个用指定值生成数组的函数:
int[] arr(int source, int num)
{
int[] p = new int[num];
for (int i = 0; i < p.Length; i++)
p[i] = source;
return p;
}
和上面的一样,只不过是用来针对double类型的:
double[] arr(double source, int num)
{
double[] p = new double[num];
for (int i = 0; i < p.Length; i++)
p[i] = source;
return p;
}
用于string类型的:
string[] arr(string source, int num)
{
string[] p = new string[num];
for (int i = 0; i < p.Length; i++)
p[i] = source;
return p;
}
除了参数类型以外都一样。我们希望写一个函数,对任何类型都生效。
使用object是肯定不行的。返回一个object数组后还需要类型转换。类型转换是可能出错的。
为了解决这个问题我们需要用到泛型。
泛型也是一种参数。在声明函数时声明一种类型,并假设这种类型存在。
泛型的参数的“形参”称为类型占位符,泛型参数的“实参”称为类型参数。
在函数名与参数列表之间,加一对尖括号,里面声明类型占位符。多个占位符间使用逗号隔开。
T[] arr<T>(T source, int num)
{
T[] p = new T[num];
for (int i = 0; i < p.Length; i++)
p[i] = source;
return p;
}
当声明了类型占位符后,这个类型就假设已经存在了。
它可以在参数列表中使用,可以在返回值使用,可以在方法中使用。
但因为它被假设是任何类型,所以使用起来有很大的限制。只有所有类型都有的功能,他才能用。
调用泛型函数时也需要加一对尖括号,就像正常填参数一样填入类型。
int a = 10;
var p = arr<int>(a, 20);
不过,如果从参数中可以推断出完整的泛型占位符,那么可以省略泛型参数。
特别对于部分以out参数为输出的函数。不使用var而是写完整参数,反而可以少些一些代码。
可以在类型上声明泛型,这样可以使用泛型字段和泛型属性。
方法也可以使用由类声明的泛型占位符。并且不需要额外声明占位符。
泛型类可以和同名但不同泛型占位符数量的类共存
class Tesk<T>
{
T[] arr;
public T this[int i]
{
get => arr[i];
set => arr[i] = value;
}
public T SetArr(params T[] arr)
{
this.arr = arr;
return arr[0];
}
}
class Tesk
{
}
需要注意的是:泛型类的构造器声明时不需要加泛型,调用时必须要加泛型,哪怕能从参数中识别出。
在静态一章说,静态字段是和类绑定的,而类是唯一的,所以静态字段是唯一的。
泛型类不是。每一种不同的类型,都是由运行时现场合成的。
不同的泛型参数间,泛型类的静态字段是不一样的。
Tesk<int>.a = 10;
Tesk<double>.a = 20;
Console.WriteLine(Tesk<int>.a + Tesk<double>.a + Tesk<string>.a);//35
class Tesk<T>
{
public static int a = 5;
}
泛型类可以继承其他类,也可以被继承。
在被继承时,需要有效的类型参数,可以是实际的类型,也可以是另一个泛型类的占位符。
class Derive : Tesk<int> { }
class Derive<T> : Tesk<T> { }
使用泛型时会假设泛型占位符是任何类型。
为了满足所有的可能类型,可用的操作非常少。
为此我们可以为泛型占位符添加约束。虽然会让能兼容的类型变少,但是可以使用更多的操作。
在函数的参数列表后,或泛型类的泛型占位符后使用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()约束。
观察以下代码
object[] o = new string[5];
o[0] = 12;
string类型可以转化为object类型大家知道。
但是string数组不应该能直接转换为object数组。
像这样肯定就会出错。
泛型类不会像数组一样可以整个转换,更为安全。
而协变和逆变可以打开这个限制。
不过,只有泛型接口和泛型委托可以使用协变逆变。
IInterface<string> inter = new MyClass();
interface IInterface<in T>
{
void a() { }
}
class MyClass : IInterface<object> { }
协变使用out关键字修饰,表示输出,修饰的泛型占位符仅能作为返回值类型。
逆变使用in关键字修饰,表示输入,修饰的泛型占位符仅能作为参数类型
协变:和谐的变化,儿子老了会当父亲。泛型填的是子类,可以迎合父类。
逆变:大逆不道的变化,要父亲当儿子。泛型填的是父类,却要迎合子类。