c#入门-泛型

泛型

这是一个用指定值生成数组的函数:

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;
} 

当声明了类型占位符后,这个类型就假设已经存在了。
它可以在参数列表中使用,可以在返回值使用,可以在方法中使用。
但因为它被假设是任何类型,所以使用起来有很大的限制。只有所有类型都有的功能,他才能用。
c#入门-泛型_第1张图片

调用泛型函数

调用泛型函数时也需要加一对尖括号,就像正常填参数一样填入类型。

int a = 10;
var p = arr<int>(a, 20);

不过,如果从参数中可以推断出完整的泛型占位符,那么可以省略泛型参数。
特别对于部分以out参数为输出的函数。不使用var而是写完整参数,反而可以少些一些代码。

例如,尝试将字符串转为枚举的方法:
c#入门-泛型_第2张图片

泛型类

可以在类型上声明泛型,这样可以使用泛型字段和泛型属性。
方法也可以使用由类声明的泛型占位符。并且不需要额外声明占位符。
泛型类可以和同名但不同泛型占位符数量的类共存

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

非托管类型是指不包含任何引用类型字段的结构。
托管类型即指引用类型,这些类型会放在堆内存里,并由运行时管理。

实现接口

实现接口只需要直接写接口名
但是正如继承时的语法一样,接口约束必须写在类约束或结构约束之后。

默认占位符以隐式实现方式实现接口所有方法
也就是说可以直接从参数中调用接口的方法。
在多个接口存在同名方法时需要转化为接口再调用。
c#入门-泛型_第3张图片
如果接口存在静态抽象方法,则可以通过类型占位符访问静态方法。

void Boo<T>(T t) where T : IInterface
{
	T.aa(); 
}
interface IInterface
{
	public static abstract void aa();
}

其他

new()约束:虽然看起来只是说要有一个无参构造器。但实际上还要求这个构造器是公开的
也就是要求目标类型具有公共无参构造器。具有此约束的类型可以在函数中调用这个构造器。
new约束必须放在约束列表的最后一个。
c#入门-泛型_第4张图片
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关键字修饰,表示输入,修饰的泛型占位符仅能作为参数类型

协变:和谐的变化,儿子老了会当父亲。泛型填的是子类,可以迎合父类。
逆变:大逆不道的变化,要父亲当儿子。泛型填的是父类,却要迎合子类。

你可能感兴趣的:(#,进阶部分,c#)