本文主要介绍C#中的泛型。
1. 泛型介绍
我们可以用int a
变量来代表-2147483648—2147483647
之间的任意一个整数,那么我们也可以用T
来代表String
、int
、Object
、自定义类等数据类型,这就是泛型。
对于一些操作,如果需要处理多种数据类型,但处理逻辑是一样的,那么通过使用泛型,可以大大减少代码量。
1.1 泛型示例-集合
在C#中,已经预定义了许多泛型类和泛型接口,最常用的就是System.Collections.Generic
命名空间中的集合类,例如:
List<String> stringList = new List<string>();
stringList.Add("hello");
stringList.Add("你好");
在实例化List
时,需要指定类型参数为String
,这也表示列表中只能存储字符串。如果我们尝试向列表中存储其他类型的数据,则会报错:
1.2 泛型示例-可空类型
在System
命名空间中,存在结构体Nullable<T>
,他的作用是可以将值类型赋值为null。用法如下:
Nullable<int> a = null;
Console.WriteLine("a = " + a);
程序输出如下:
a =
表示整数类型的变量a被赋值为null了,并没有报错。
我们也可以简写:
int? b = null;
int? c = 5;
Console.WriteLine("b = " + b);
Console.WriteLine("c = " + c);
结果为:
b =
c = 5
2. 自定义泛型
除了使用预定义的泛型类和泛型接口,我们也可以自己定义泛型类、泛型方法、泛型接口和泛型委托。
2.1 泛型类
语法为:
class ClassName<T1,T2,[T3,T4...]> [: 类, 接口] {
...
}
例如List<T>
的定义如下:
public class List<T> : ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList
{
...
}
对于一个泛型中的继承和接口实现,需要满足以下任一种情况即可:
- 泛型类继承中,父类的类型参数已被实例化,这种情况下子类不一定必须是泛型类,如下例的
NodeClosed<T>
、Node1
; - 父类的类型参数没有被实例化(或者可以说没有被完全实例化),但来源于子类,也就是说父类和子类都是泛型类,并且二者有相同的类型参数,如
NodeClosed<T>
;
class BaseNode { }
class BaseNodeGeneric<T> { }
// concrete type
class NodeConcrete<T> : BaseNode { }
//closed constructed type
class NodeClosed<T> : BaseNodeGeneric<int> { }
//open constructed type
class NodeOpen<T> : BaseNodeGeneric<T> { }
//No error
class Node1 : BaseNodeGeneric<int> { }
//Generates an error
//class Node2 : BaseNodeGeneric<T> {}
//Generates an error
//class Node3 : T {}
//如果这样写的话,显然会报找不到类型T,S的错误
//public class TestChild : Test< T, S> { }
2.2 泛型方法
自定义泛型方法的语法为:
void methodName<T1,[T2,T3...]>([T1 t1, T2 t2, T3 t3]){
...
}
不论是静态方法还是实例方法,我们都可以定义为泛型方法;
不论是泛型类还是非泛型类,我们都可以在其中定义泛型方法。
对于泛型类,在非泛型方法中也可以使用类型参数,如下:
class SampleClass<T>
{
void Swap(ref T lhs, ref T rhs) { }
}
如果在一个泛型类中定义泛型方法,并且泛型方法和泛型类的类型参数同名,那么泛型方法中的类型参数会掩盖泛型类的类型参数,导致CS0693
警告:
我们可以通过改名的方式解决这类型警告,如将泛型方法的类型参数改名为U:
泛型方法也可以通过类型参数被重载:
void DoWork() { }
void DoWork<T>() { }
void DoWork<T, U>() { }
2.3 泛型接口
泛型接口与泛型类的定义相似,如下:
interface interfaceNmae<T1,[T2,T3...]>{
...
}
在C#的System.Collections.Generic
命名空间中,预定义了一些泛型接口,例如IComparer
和Icomparable
:
public interface IComparer<in T>
{
int Compare([AllowNull] T x, [AllowNull] T y);
}
public interface IComparable<in T>
{
int CompareTo([AllowNull] T other);
}
泛型接口的继承与实现,与泛型类的类似,不再赘述。
2.4 泛型委托
泛型委托的声明语法如下:
delegate T[或者具体的类型参数] MyDelegate<T>(T value);
泛型委托支持在返回值和参数上应用类型参数.
这样如果有两个方法,使用同一个泛型委托就可以了,案例如下:
// 定义泛型委托
public delegate T MyDelegate<T>(T value1, T value2);
// 定义方法
public static int add(int i, int j)
{
return i + j;
}
public static String concate(String str1, String str2)
{
return str1 + str2;
}
// Main方法中
MyDelegate<int> myDelegateInt = Program.add;
MyDelegate<String> myDelegateString = Program.concate;
Console.WriteLine(myDelegateInt(1, 10));
Console.WriteLine(myDelegateString("hello ", "world"));
结果为:
11
hello world
3. 类型参数约束
对于一个类型参数,如果我们不加约束,那么这个类型参数可以为任意的数据类型,显然,毫无约束的类型参数使得程序出错的概率大大增加,所以有了类型参数约束。
我们使用where
来指明类型参数约束。
常见的类型参数约束如下:
where T : struct
:表示类型参数T必须是非空的值类型;where T : class
:表示类型参数T必须是引用类型;where T : <base class name>
:表示类型参数T必须是base class,或者是base class的子类;where T : <interface name>
:表示类型参数T必须是接口interface name,或者实现了该接口;where T : new()
:表示类型参数T必须具有公共无参构造函数,当和其他约束一起时,该约束必须放在最后,并且new()
约束不能和struct
、unmanaged
约束放在一起;where T : U
:表示类型参数T必须是类型参数U,或者T是U的子类;
更多类型参数约束,请参看Constraints on type parameters - C# Programming Guide | Microsoft Docs
我们可以同时对多个类型参数添加约束,每个类型参数分别使用一个单独的where
关键字;也可以对一个类型参数添加多个约束,但要注意不要矛盾冲突,如:
class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{ }
如果一个类型参数没有任何约束,则称之为未绑定的类型参数,未绑定的类型参数有如下规则:
- 不能使用
==
和!=
运算符; - 可以与
System.Object
相互转换,也可以转换为任意接口; - 可以与
null
比较;如果类型参数是值类型,则与null
的比较结果始终为false
;
4. 参考资料
[1] Generic Type Parameters - C# Programming Guide | Microsoft Docs