泛型

泛型编程相关内容,涉及基本语法、泛型可变性(协变逆变和不变)、泛型约束(上界和下界)、高阶泛型

泛型

泛型(Generic)是非常重要的语言功能,广泛应用于各种项目、工具、框架。
本文主要关注泛型的应用,由浅入深介绍和讨论能力和限制,给出相关示例来演示应用。

演示代码语言/平台采用最新LTS或主流支持版本,详细情况如下:

.NET版本支持策略参考官方文档: .NET Support Policy; C#F#随.NET一同发布/更新

Scala与JDK兼容情况可参考官方介绍: JDK Compatibility
Scala 2.x 维护计划在Scala 2 maintenance plansScala 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,不能与structnew()约束组合使用。
注:非托管类型包括内置值类型及布尔类型、枚举类型、指针类型、成员都是非托管类型的元组(tuple)和结构(struct)类型。但该约束依然不允许指针和可为NULL的非托管类型。
where T : new() T必须有公开的无参构造器
当与其他约束组合使用时,new()约束必须放在最后。
new()约束不能与structunmanaged约束组合使用。
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必须未声明structclass约束
该约束仅在显式实现接口方法或重写方法时可用,用于声明期望实现/重写的是T未被约束的方法。
where T : allows ref struct 该反约束允许Tref 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是其他类型UT?表示标注过的类型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约束为unmanagedsizeof操作符不可用。

委托约束

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类型,因此该类型实例必须遵守如下规则:

  • 不能被装箱
  • 必须遵守引用安全规则(ref safety rules)
  • 不能在禁止使用ref struct的地方使用该类型,例如static字段
  • 实例可以使用scoped修饰符进行标记

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#

官方文档:\

  1. Covariance and contravariance in generics
  2. Covariance and Contravariance (C#) - Overview
  3. Variance in Generic Interfaces (C#)
  4. Creating Variant Generic Interfaces (C#)
  5. Using Variance in Interfaces for Generic Collections (C#)
  6. Variance in Delegates (C#)
  7. Using Variance in Delegates (C#)
  8. 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. 数组协变(强烈不推荐使用)
1
2
3
4
5
6
// 构造字符串数组实例,可以赋值给对象数组
object[] array = new string[] { "a", "b", "c" };
// 一般的读取不会有类型安全问题
var value = array[0];
// 赋值语句可以通过编译,但会在运行时抛出异常
array[0] = 1;

类型不安全。
该功能是编译器隐式支持的功能,但在运行时检查安全性
协变的数组变量不存在严格的写保护,极有可能出现不经意的赋值操作,生产环境中应当避免使用

  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. 非泛型委托的协变和逆变
 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

本文内容经过较长时间整理、