泛型
泛型(Generic)是非常重要的语言功能,广泛应用于各种项目、工具、框架。
本文主要关注泛型的应用,由浅入深介绍和讨论能力和限制,给出相关示例来演示应用。
演示代码语言/平台采用最新LTS或主流支持版本,详细情况如下:
.NET版本支持策略参考官方文档: .NET Support Policy; C#和F#随.NET一同发布/更新
Scala与JDK兼容情况可参考官方介绍: JDK Compatibility
Scala 2.x 维护计划在Scala 2 maintenance plans及Scala development guarantees中有介绍,简言之2.13将持续维护,2.12将在sbt 1广泛应用期间持续维护
Scala 2.13相对与2.12的变化可参考: Migrating a Project to Scala 2.13’s Collections, Scala 2.13.0 is now available!, Releases / Scala 2.13.0
Scala 3相对于2的变化可参考: Scala 3 Migration Guide
Python的版本支持情况可参考官方介绍: Status of Python versions
基本语法
本节简要介绍泛型在各语言中的基本使用。
C#
官方文档: \
泛型接口、泛型类和泛型方法
如下代码演示了泛型接口、泛型类、泛型方法(实例方法及静态方法)的定义和调用。\
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
// ==========[定义泛型]==========
// 定义泛型接口
interface IWritable<T>
{
void Write(T value);
}
interface IReadable<T>
{
T Read();
// 接口自带默认实现
string ReadString() => Read().ToString();
}
// 定义泛型类(并实现泛型接口)
class Response<T> : IReadable<T>, IWritable<T>
{
public int Code { get; set; }
public string Message { get; set; }
// Data属性是个泛型属性
public T Data { get; set; }
// 构造器方法
public Response() { }
public Response(int code, string message, T data) : this()
{
this.Code = code;
this.Message = message;
this.Data = data;
}
// 解构方法
public void Deconstruct(out int code, out string message, out T data)
{
code = Code;
message = Message;
data = Data;
}
// 实现泛型接口中定义的方法
public T Read() => Data;
public void Write(T value) => Data = value;
// 定义泛型实例方法
public bool TryRefreshData<U>(U data)
{
if (data is T t)
{
this.Data = t;
return true;
}
return false;
}
// 尽量避免在泛型类中定义静态方法,调用相对比较麻烦
// 定义泛型静态方法
public static Response<U> Success<U>(U data)
=> new(0, "success", data);
// 定义静态方法
public static Response<string> Failed(string message)
=> new(1, message, "failed");
}
// 定义工具类
// C#中,泛型静态方法最好放在单独的工具类中,可以直接以
class ResponseHelper
{
// 定义泛型静态方法
public static Response<T> Success<T>(T data)
=> new(0, "success", data);
public static Response<string> Failed(string message)
=> new(1, message, "failed");
}
// 定义(静态)扩展类
static class ResponseExtension
{
// 定义泛型扩展方法
public static Response<T> Copy<T>(this Response<T> response)
=> new(response.Code, response.Message, response.Data);
// 定义泛型扩展方法
public static bool IsSuccess<T>(this Response<T> response)
=> response.Code == 0;
}
// ==========[使用泛型]==========
// 初始化对象(调用构造器)
var response = new Response<string>(0, "success", "data");
// 初始化对象(属性初始化)
var response = new Response<string>
{
Code = 0,
Message = "success",
Data = "data"
};
// 初始化对象(new表达式)
Response<string> response = new(0, "success", "data");
// 解构对象
var (code, _, data) = response;
// 泛型方法调用时,编译器可以从参数中推断类型,因此一般无需声明类型
// 调用泛型实例方法
response.TryRefreshData(123);
// 调用泛型实例方法(声明类型)
response.TryRefreshData<short>(123);
// 调用静态泛型方法
// 调用泛型类中的静态方法时,必须要声明泛型类的具体类型(泛型静态方法的类型一般无需声明)
var response = Response<string>.Success<short>(123);
// 调用普通工具类中的静态方法(泛型静态方法的类型一般无需声明)
var response = ResponseHelper.Success<short>(123);
// 调用泛型扩展方法(与实例方法调用语法一致)
var success = response.IsSuccess();
var copy = response.Copy();
|
泛型接口、泛型类、泛型方法算是面向对象编程中对泛型最基本的应用。
此外泛型抽象类并未因泛型而新增特殊之处,其本身的功能及限制只是因为它是“抽象类”而非“泛型”,因此代码中并未演示。
泛型委托和泛型事件
委托和事件C#独有的语言功能,两者皆支持泛型,此处演示相关应用。
相关详细概念请参考官方文档。
1
2
3
4
5
6
|
// 定义泛型委托
delegate T BiOp<T>(T a, T b);
// 使用泛型委托
BiOp<int> add = (a, b) => a + b;
var c = add(1, 2);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
class Singleton<T>
{
// 定义事件
public event EventHandler<T> ValueChanged;
private T _value;
public T Value
{
get => _value;
set
{
if (!EqualityComparer<T>.Default.Equals(Value, value))
{
_value = value;
OnValueChanged(value);
}
}
}
protected virtual void OnValueChanged(T value)
{
// 触发事件
ValueChanged?.Invoke(this, value);
}
}
// 初始化对象
var singleton = new Singleton<int>();
// 订阅事件(多个事件处理程序)
singleton.ValueChanged += (_, i) => Console.WriteLine($"int = {i}");
// 赋值&触发事件
singleton.Value = 1;
// 重复赋值&不触发事件
singleton.Value = 1;
|
事件本质上是特殊类型的委托。对两者的深入探讨请参考官网文档。、
泛型数组
泛型数组本身没什么特殊,此处需要特别注明的是一维数组自动实现IList<T>接口,这有助于实现一个泛型方法对数组或其他集合进行遍历。
但要注意自动实现的IList<T>接口仅支持数据读取,不能用于从数组中增删元素(会抛出异常)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 直接以集合初始化语法定义并初始化数组和List
int[] array = [1, 2, 3, 4, 5];
List<int> list = [1, 2, 3, 4, 5];
// 以一致的方式访问(读取)数组和List
ProcessItems(array);
ProcessItems(list);
static void ProcessItems<T>(IList<T> list)
{
// 数组对象调用IsReadOnly返回True,List对象会返回False
Console.WriteLine("IsReadOnly returns {0} for this collection.", list.IsReadOnly);
// 对数组对象调用Insert/RemoveAt方法会导致抛出异常
//list.RemoveAt(4);
foreach (T item in list)
{
Console.Write(item?.ToString() + " ");
}
Console.WriteLine();
}
|
F#
TBD
Java
泛型约束
泛型约束声明了类型参数预期的能力,声明约束后可以安全地调用支持的操作。
C#
C#具备种类丰富的泛型约束,下方列表中对各种类型进行简要介绍。
| 泛型约束 |
简介 |
where T : struct |
T必须是不可为NULL的值类型(Value Type)(可以是record struct类型)。 由于所有值类型必定存在可用的无参构造器,因此这也隐含声明new()约束。
struct约束不能与new()及unmanaged约束组合使用。 |
where T : class |
T必须是引用类型(Reference Type)。
T可以是类、记录类(record class)、接口、委托、数组。 在允许为NULL上下文(nullable context)中,T必须是不可为NULL的引用类型。 |
where T : class? |
T必须是引用类型(Reference Type),可以是允许为NULL的引用类型,也可以是不允许为NULL的引用类型。
T可以是类、记录类(record class)、接口、委托、数组。 |
where T : notnull |
T必须是不可为NULL的类型。
T可以是不可为NULL的引用类型、不可为NULL的值类型。 |
where T : unmanaged |
T必须是不可为NULL的非托管类型(Unmanaged Type)。
unmanaged约束隐含声明struct,不能与struct及new()约束组合使用。 注:非托管类型包括内置值类型及布尔类型、枚举类型、指针类型、成员都是非托管类型的元组(tuple)和结构(struct)类型。但该约束依然不允许指针和可为NULL的非托管类型。 |
where T : new() |
T必须有公开的无参构造器。 当与其他约束组合使用时,new()约束必须放在最后。
new()约束不能与struct及unmanaged约束组合使用。 |
where T : <class name> |
T必须是给定的类或继承自给定的类。 在允许为NULL上下文(nullable context)中,T必须是不可为NULL的引用类型。 |
where T : <class name>? |
T必须是给定的类或继承自给定的类。 在允许为NULL上下文(nullable context)中,T可以是允许为NULL的引用类型,也可以是不允许为NULL的引用类型。 |
where T : <interface name> |
T必须是给定的接口或实现了给定的接口。 可以声明多个接口约束;约束中给定的接口也可以是泛型接口。 在允许为NULL上下文(nullable context)中,T必须是实现给定接口的不允许为NULL的类型。 |
where T : <interface name>? |
T必须是给定的接口或实现了给定的接口。 可以声明多个接口约束;约束中给定的接口也可以是泛型接口。 在允许为NULL上下文(nullable context)中,T必须是实现给定接口的可为NULL引用类型、不可为NULL的引用类型、值类型。T不可以是允许为NULL的值类型。 |
where T : U |
T必须是类型参数U给定的类型或继承自给定类型。 在允许为NULL上下文(nullable context)中,若U是不允许为NULL的引用类型,则T也必须是不可为NULL的引用类型;若U是允许为NULL的引用类型,则不限制T是否可为NULL。 |
where T : default |
T必须未声明struct或class约束。 该约束仅在显式实现接口方法或重写方法时可用,用于声明期望实现/重写的是T未被约束的方法。 |
where T : allows ref struct |
该反约束允许T是ref struct类型。
T可能是ref struct实例,泛型类型和方法必须遵循引用安全规则(ref safety rules)。 C# 13.0+可用 |
某些约束是互斥的,某些约束必须遵循特定的顺序。
struct/class/class?/notnull/unmanaged约束至多允许一个,且必须是第一个约束;
- 基类约束(
where T : Base/where T : Base?)至多允许一个,使用where T : Base?支持可为NULL的基类,基类约束不能与struct/class/class?/notnull/unmanaged约束组合使用;
- 不允许同时声明单个接口的可NULL和不可NULL形式(
where T : I1, I1? ×; where T : I1, I2? √);
new()约束不能与struct/unmanaged约束组合使用;若存在new()约束,其必须位于约束的最后(反约束可以放在它后面);
default约束只能用于方法重写和显式实现接口方法场景,且不能与struct/class约束组合使用;
- 反约束
allows ref struct不能与class/class?约束组合使用,且必须跟在所有约束的后面。
部分约束有如下注意事项。\
- 使用
class约束时避免使用比较操作符==/!=。这组操作符只比较引用是否相等,不进行值比较。该行为不会因具体类型是否重载比较操作符而变化,因为编译时仅能得知类型是引用类型,必须调用对所有引用类型都合法有效的实现。若要在泛型代码中进行值比较,请使用where T : IComparable<T>或where T : IEquatable<T>约束并为实际类型实现相关接口。
- 若类型参数没有任何约束(例如
SampleClass<T>{}中的T)称为未绑定类型(unbounded type parameter),应当遵循如下规则:
- 不能使用比较操作符
==/!=,因为不能保证实际类型支持该操作符;
- 可以显式转换为任何接口类型,也可以转换成
System.Object类型(或者由System.Object转换成T);
- 可以与
null进行比较(T是值类型时会返回false)
notnull约束用以声明类型参数将约束为不可为NULL的值类型/引用类型,违反notnull约束时,编译器只会生成警告,不会报告错误。notnull约束仅在可空代码上下文中生效,若在忽略可空性的代码上下文使用,违反约束时编译器不会生成任何警告或错误。
以下对部分约束进行详细解释及代码演示。
泛型约束常规应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 单个类型参数的泛型约束
struct Container<T> where T : struct
{
public T Value { get; set; }
}
// 多个类型参数的泛型约束(各自约束互不干涉)
class Container<K, V>
where K : struct
where V : class, new()
{
// ...
}
// 类型参数作为约束
class SampleClass<T, U, V> where T : V { }
class List<T>
{
public void Add<U>(List<U> items) where U : T {/*...*/}
}
|
default约束
参考文档: dotnet/csharplang/proposals/csharp-9.0/unconstrained-type-parameter-annotations
default约束是用于在重写/实现方法时消除nullable泛型重载的歧义。以下以代码演示其功能。
关于?标注
C# 8.0中,?仅能用于显式约束了值类型或引用类型的类型参数;在C# 9.0中,?可以用于任意类型参数,无论是否存在约束。
但要注意,?标注仅能用于设置了#nullable enable的代码上下文中。
若是类型参数T是个引用类型,T?表示该引用类型的可NULL实例。
1
2
|
var s1 = new string[0].FirstOrDefault(); // string? s1
var s2 = new string?[0].FirstOrDefault(); // string? s2
|
若是类型参数T是个值类型,T?表示该值类型的实例。
1
2
|
var i1 = new int[0].FirstOrDefault(); // int i1
var i2 = new int?[0].FirstOrDefault(); // int? i2
|
若是类型参数T是其他标注过的类型U?,T?依然表示标注过的类型U?,而不是U??。
1
2
|
var u1 = new U[0].FirstOrDefault(); // U? u1
var u2 = new U?[0].FirstOrDefault(); // U? u2
|
若是类型参数T是其他类型U,T?表示标注过的类型U?,即使在#nullable disable上下文中。
1
2
|
#nullable disable
var u3 = new U[0].FirstOrDefault(); // U? u3
|
实际上T?仅是个标注,并非一定是在构造新类型。
FirstOrDefault方法的声明是public static TSource? FirstOrDefault<TSource>(this IEnumerable<TSource> source);,返回值部分只是声明可能为NULL,并不是将返回值变成真正的可空类型TSource?。
对于值类型TSource(如int),TSource?和TSource在IL中都是int,方法泛型返回值不能为int标注?(int?实际是Nullable<int>,是个新结构)。
对于返回值,T?等同于[MaybeNull] T,对于参数值,T?等同于[AllowNull] T。当重写方法或实现接口时该等同性非常重要。
以下代码演示了该等同性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public abstract class A
{
// 以特性方式声明抽象方法
// 没有声明`class`/`struct`约束,编译器认为存在歧义,会添加`where T : default`约束
// 实际等同于public abstract T? F1<T>(); (也会默认添加where T : default约束)
[return: MaybeNull] public abstract T F1<T>();
// 实际等同于public abstract void F2<T>(T? t)
public abstract void F2<T>([AllowNull] T t);
}
public class B : A
{
// 重写时必须显式声明where T : default约束
public override T? F1<T>() where T : default { return defalut(T); } // matches A.F1<T>()
public override void F2<T>(T? t) where T : default { } // matches A.F2<T>()
}
|
使用T?或注解形式的可为NULL声明且不存在class/struct约束时,编译器认为存在歧义,会默认添加where T : default约束,表示没有约束class/struct。
方法重写/实现接口时,必须声明where T : default约束,用来表示重写/实现的是不约束class/struct的方法。\
关于歧义
参考如下代码,除泛型约束外,两个方法是一致的形式,但方法签名不包含泛型约束信息。理论上,两个方法不应同时存在。
1
2
3
4
5
|
class C
{
public virtual void F<T>(T? t) where T : struct { }
public virtual void F<T>(T? t) where T : class { }
}
|
实际上,以上代码是合法的,两个方法是不同的签名。
C#8.0引入可为NULL引用类型支持,T?的意义取决于T是值类型或是引用类型。
| 泛型约束 |
T?的含义 |
真实参数类型 |
where T : struct |
可为NULL值类型 |
Nullable<T> |
where T : class |
可为NULL引用类型 |
T(带可为NULL元数据) |
以上代码在编译器视角中的实际形式如下。
1
2
3
4
5
6
7
|
using System.Diagnostics.CodeAnalysis;
class C
{
public virtual void F<T>(Nullable<T> t) where T : struct { }
public virtual void F<T>([AllowNull] T t) where T : class { }
}
|
两个方法的参数在编译器视角中是不同的类型,函数签名不同,因此可以同时存在。
但是对常规写法而言,用户读到的代码是一样的,因此便出现了歧义。
消除歧义
显式class/struct约束的代码,重写/实现时可以通过显式约束消除歧义。
1
2
3
4
5
6
7
8
9
10
11
|
class A1
{
public virtual void F1<T>(T? t) where T : struct { }
public virtual void F1<T>(T? t) where T : class { }
}
class B1 : A1
{
public override void F1<T>(T? t) /*where T : struct*/ { }
public override void F1<T>(T? t) where T : class { }
}
|
注: 显式struct约束的方法参数实际是Nullable<T>,相对于参数为T的方法,这是个约束更强(更具体)的版本,在编译器解析时具备更高的优先级。
因此重写方法时的where T : struct约束可以省去,但非常不建议这样做。
而当存在有约束和无约束版本的方法时,可以使用default约束来消除歧义。
1
2
3
4
5
6
7
8
9
10
11
|
class A2
{
public virtual void F2<T>(T? t) where T : struct { }
public virtual void F2<T>(T? t) { }
}
class B2 : A2
{
public override void F2<T>(T? t) /*where T : struct*/ { }
public override void F2<T>(T? t) where T : default { }
}
|
where T : default声明此处重写的是无class/struct约束的版本。
unmanaged约束
该约束可用于期望以直接操作内存块的方式读写类型实例的代码,如下所示。
1
2
3
4
5
6
7
8
9
|
unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged
{
var size = sizeof(T);
var result = new Byte[size];
Byte* p = (byte*)&argument;
for (var i = 0; i < size; i++)
result[i] = *p++;
return result;
}
|
由于对未知的类型T调用sizeof操作符,因此代码必须声明为不安全上下文(unsafe context);
若没有将类型参数T约束为unmanaged,sizeof操作符不可用。
委托约束
C#语言规范中未提供类似where T : delegate形式的约束,但可以用类型约束来实现类似的功能。
可以将T约束为System.Delegate/System.MulticastDelegate的子类,就可以在满足类型安全的前提下对委托进行操作。
代码演示如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 操作委托的扩展方法
// 合并相同相同类型的委托
public static T? TypeSafeCombine<T>(this T source, T target) where T : Delegate
=> Delegate.Combine(source, target) as T;
// 定义两个相同类型的委托
Action first = () => Console.WriteLine("this");
Action second = () => Console.WriteLine("that");
// 合并&调用
var combined = first.TypeSafeCombine(second); // √
combined!();
// 定义一个不同类型的委托
Func<bool> test = () => true;
// 如下的调用是错误的
var badCombined = first.TypeSafeCombine(test); // ×
|
枚举约束
与委托调用类似,C#语言规范中并未直接提供纯粹的where T : enum约束(unmanaged约束允许传入枚举,但不是只允许枚举),但可以用类型约束来实现类似功能。
可以将T约束为System.Enum的子类,就可以约束为仅允许枚举传入。
代码示例如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 操作枚举的扩展方法
// 将给定枚举的值和名称构造成字典对象
public static Dictionary<int, string> EnumNamedValues<T>() where T : Enum
{
var result = new Dictionary<int, string>();
var values = Enum.GetValues(typeof(T));
foreach (int item in values)
{
result.Add(item, Enum.GetName(typeof(T), item)!);
}
return result;
}
// 定义枚举
enum SomeValue {A, B, C, D}
// 调用扩展方法并初始化字典
var dict = EnumNamedValues<SomeValue>();
|
类型参数继承/实现声明的类/接口(F-Bound)
本质是F-Bound,此处仅简要介绍官方文档对该约束的介绍及演示(样例中涉及到对C#支持接口静态成员的语言功能),请参考下文(F-Bound)对该技巧的详细介绍。
1
2
3
4
5
6
7
|
// T必须实现接口本身
interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
// C#接口支持声明静态成员
static abstract T operator +(T left, T right);
static abstract T operator -(T left, T right);
}
|
allows ref struct约束
该约束是C#13.0新增的语言功能。
allows ref struct实际上是反约束,允许指定的类型参数可以是ref struct类型,因此该类型实例必须遵守如下规则:
F-Bound
F-Bound是一种泛型编程技巧,通常用于面向对象编程中,帮助在继承层次结构中实现类型自参的自引用。简单来说,是指通过继承某个类型的泛型类,子类可以返回类型自己。这个技巧在做流式接口设计、构建DSL或者处理复杂类型时非常有用。
F-Bound通常用于实现以下模式:基类定义了一个泛型方法,该方法返回该类(或者子类)的类型。而子类继承基类时,能够实现这个方法并返回自己的类型。
以下代码演示基本应用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
using System;
// 基类 Shape,使用 F-Bound 技巧
public abstract class Shape<T> where T : Shape<T>
{
// 一个设置颜色的方法,返回当前类型(T)本身
public T SetColor(string color)
{
Console.WriteLine($"Setting color to {color}");
return (T)this; // 返回当前类型本身
}
// 计算形状的面积(可以在子类中实现具体的面积计算)
public abstract double GetArea();
}
// Circle 类,继承自 Shape<T>,并指定自己为 T
public class Circle : Shape<Circle>
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
// 可以链式调用 SetColor 方法
public Circle SetRadius(double radius)
{
Radius = radius;
return this;
}
}
// Rectangle 类,继承自 Shape<T>,并指定自己为 T
public class Rectangle : Shape<Rectangle>
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double GetArea()
{
return Width * Height;
}
// 可以链式调用 SetColor 方法
public Rectangle SetDimensions(double width, double height)
{
Width = width;
Height = height;
return this;
}
}
// 测试 F-Bound
class Program
{
static void Main(string[] args)
{
// 创建一个 Circle 对象,设置颜色和半径
Circle circle = new Circle(5);
circle.SetColor("Red")
.SetRadius(10);
Console.WriteLine($"Circle Area: {circle.GetArea()}");
// 创建一个 Rectangle 对象,设置颜色和尺寸
Rectangle rectangle = new Rectangle(4, 5);
rectangle.SetColor("Blue")
.SetDimensions(6, 7);
Console.WriteLine($"Rectangle Area: {rectangle.GetArea()}");
}
}
|
可变性
可变性(variance)包含协变(covariance)、逆变(contravariance)和不变(invariance)。
语言层面的可变性支持是类型系统支持多态的关键部分,有的是由严格的语法支持,有的语言通过子类型兼容性简介体现。
简单来讲,协变(covariance)是指子类型可以赋值给父类型,常用于返回值(输出)场景;
逆变(contravariance)是指父类型可以赋值给子类型,常用于参数(输入)场景;
不变(invariance)是指完全相同的类型才能赋值,最安全也最严格。
可变性的核心概念(类型转换中派生类与基类之间的方向)在非泛型场景中也存在,但一般是隐式的,或称为“类型兼容性”问题。
有些“类型兼容性”问题(例如数组协变)依赖编译器推断,但在运行时检查,可能抛出异常。
常规场景中,参数逆变,返回值协变,便已是足够完善可靠的规则。
| 语言 |
协变(covariant) |
逆变(contravariant) |
说明 |
| C# |
✅ out |
✅ in |
泛型接口/委托支持,数组协变(但运行时检查) |
| Java |
✅ ? extends |
✅ ? super |
使用通配符在泛型中表达变体,没有关键字(只能在使用处) |
| Scala |
✅ +T |
✅ -T |
在泛型定义中支持变体标注,极其灵活 |
| Python |
✅(Typing 协变) |
✅(Typing 逆变) |
使用 typing.Generic + covariant=True 表达 |
| Kotlin |
✅ out T |
✅ in T |
类似 C#,在泛型类型定义中支持关键字 |
| Swift |
✅ 协变支持 |
✅(使用 protocol) |
支持返回值协变、协议泛型逆变较间接 |
| TypeScript |
✅ 自动推导 |
✅ 自动推导 |
没有变体关键字,但函数/接口参数/返回值类型由结构类型系统自动推导 |
| Rust |
⚠️ 不支持协变/逆变关键字 |
⚠️ 不支持显式变体 |
有复杂的生命周期变体(‘a: ‘b)机制,不针对泛型类型参数 |
| 语言 |
泛型定义处变体 |
使用处变体 |
自动推导 |
安全检查 |
使用推荐度 |
| C# |
✅ in/out |
✅ 委托 |
❌ |
✅ 编译时 |
✅✅✅ |
| Java |
❌ |
✅ ? extends / ? super |
❌ |
✅ 编译时 |
✅✅ |
| Scala |
✅ +T / -T |
✅ |
✅ |
✅ |
✅✅✅✅ |
| Kotlin |
✅ out T / in T |
✅ |
✅ |
✅ |
✅✅✅ |
| Swift |
❌ |
部分支持 |
❌ |
✅ |
✅✅ |
| TypeScript |
❌ |
✅ |
✅✅✅ |
✅(结构化) |
✅✅✅ |
| Python |
✅ (Typing) |
✅ |
❌ |
✅(类型检查器) |
✅✅ |
| Rust |
❌ 泛型不支持 |
❌ |
❌ |
✅(生命周期) |
✅(不同维度) |
上表整理自ChatGPT,其中Kotlin/Swift/TypeScript/Rust的相关内容尚未审慎求证。
C#
官方文档:\
- Covariance and contravariance in generics
- Covariance and Contravariance (C#) - Overview
- Variance in Generic Interfaces (C#)
- Creating Variant Generic Interfaces (C#)
- Using Variance in Interfaces for Generic Collections (C#)
- Variance in Delegates (C#)
- Using Variance in Delegates (C#)
- Using Variance for Func and Action Generic Delegates (C#)
泛型接口/委托中的可变性
泛型类、泛型方法中不能独立使用可变性修饰符in/out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
// 定义泛型接口(实际是标准库中的类型,此处用以演示)
// 比较器接口(逆变)
public interface IEqualityComparer<in T>
{
bool Equals(T? x, T? y);
int GetHashCode([DisallowNull] T obj);
}
// 只读集合接口(协变)
public interface IReadOnlyList<out T> : IEnumerable<T>, IEnumerable, IReadOnlyCollection<T>
{
T this[int index] { get; }
}
// 简单的继承关系
class Base { }
class Derived : Base { }
// 实现基类的比较器
class BaseComparer : IEqualityComparer<Base>
{
public int GetHashCode(Base baseInstance)
{
return baseInstance.GetHashCode();
}
public bool Equals(Base? x, Base? y)
{
return x == y;
}
}
// 基类的比较器可以作为派生类的比较器使用,此处体现了逆变
IEqualityComparer<BaseClass> baseComparer = new BaseComparer();
IEqualityComparer<DerivedClass> childComparer = baseComparer;
// 子类的集合可以作为基类的集合的使用,体现了协变
IReadOnlyList<Derived> childList = [];
IReadOnlyList<Base> baseList = childList;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 泛型委托定义,参数逆变,返回值协变
public delegate TResult Func<in T, out TResult>(T arg);
// 两个委托实例
// 委托1:接受派生类,返回基类
Func<Derived, Base> func1 = (Derived d) =>
{
Console.WriteLine(d);
return new Base();
};
// 委托2:接受基类,返回派生类
Func<Base, Derived> func2 = (Base b) =>
{
Console.WriteLine(b);
return new Derived();
};
// 参数逆变:委托1期望接受派生类,实际可以接受基类
// 返回值协变:委托1期望返回基类,实际可以返回派生类
// 因此以下赋值是有效的(类型兼容),委托2的实例可以作为委托1的实例调用
func1 += func2;
|
非泛型场景中的可变性
- 数组协变(强烈不推荐使用)
1
2
3
4
5
6
|
// 构造字符串数组实例,可以赋值给对象数组
object[] array = new string[] { "a", "b", "c" };
// 一般的读取不会有类型安全问题
var value = array[0];
// 赋值语句可以通过编译,但会在运行时抛出异常
array[0] = 1;
|
类型不安全。
该功能是编译器隐式支持的功能,但在运行时检查安全性。
协变的数组变量不存在严格的写保护,极有可能出现不经意的赋值操作,生产环境中应当避免使用。
- 方法重写返回值类
1
2
3
4
5
6
7
8
9
10
11
|
class A
{
// 基类型中定义虚方法,返回值是基类型A
public virtual A CreateInstance() { return new A(); }
}
class B : A
{
// 派生类中重写方法,返回值类型是派生类型B,返回值协变
public override B CreateInstance() { return new B(); }
}
|
类型安全。
该功能是编译器隐式支持的功能。
虚方法在子类中重写时,参数必须保持一致,但返回值可以是更具体的类型(返回值是协变的),两个方法依然是“类型兼容”的。
- 非泛型委托的协变和逆变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 定义基类型Animal及派生类型Dog
class Animal { }
class Dog : Animal { }
// 定义委托类型
delegate Animal Creator();
delegate void Handler(Dog dog);
// 定义普通方法
static Dog CreateDog() { return new Dog(); }
static void FeedAnimal(Animal animal) { }
// 方法可以构造为委托实例
Creator creator = CreateDog; // 返回值协变
Handler handler = FeedAnimal; // 参数逆变
|
类型安全。
该功能是编译器隐式支持的功能。
委托Creator期望生产Animal实例,实际上得到Dog实例,Dog作为Animal使用是类型安全的,因此Dog生产者作为Animal生产者也是类型安全的。这体现的是协变。
委托Handler期望消费Dog实例,因此调用Handler时给出的必定是Dog(或Dog派生类)实例,现在handler实际上是个消费Animal的实例,Dog作为Animal使用是类型安全的,消费Animal的方法消费Dog也是类型安全的,因此Animal消费者作为Dog消费者也是类型安全的。这体现的是逆变。
委托可以视为类型安全的函数类型,函数类型也可以视为构造类型(由参数类型+返回类型构造),在这个视角看待类型兼容也许有不一样的发现。可参考本站文章CTFP(category theory for programmer/面向程序员的范畴论)中协变和逆变的相关内容。
高阶泛型
Higher Kind
Higher Rank
参考链接
Generic programming
Parametric polymorphism
本文内容经过较长时间整理、