第3章:泛型、委托和事件(建议32-45)

覆盖泛型优先、静态成员、约束与default,FCL委托、lambda/闭包、delegate与标准事件模型,以及接口和委托的协变、逆变与不变性。

学习目标

  • 能分析object、多态与generic API保留type relationship的差异,并设计最小constraint、per-T static和default policy
  • 能解释lambda到delegate的binding,判断closure、multicast invocation与event subscription的state和lifetime风险
  • 能推导invariant、covariant和contravariant conversion是否类型安全,并正确标注producer/consumer type parameter

机制总览

第3章:泛型、委托和事件(建议32-45):机制路径

  1. 1

    为什么泛型、委托和变体属于同一个类型契约

    泛型把“输入类型与输出类型有什么关系”交给compiler证明;delegate把“以后要调用哪段typed behavior”变成value;event再限制谁能发布这段behavior;variance则回答一个generic/delegate type能否沿继承关系安全转换。它们共同解决的不是…

  2. 2

    Generic Contract与Default(建议32…

    当同一算法适用于多种type且需要保留input/output relationship时,generic优于 object +cast:caller获得compile-time checking,value type通常避免boxing,implementation也无需type switch。优…

  3. 3

    Delegate、Closure与Event Owners…

    常见callable shape优先复用 Action 、 Func 、 Predicate 、 Comparison 与 EventHandler ,降低无意义delegate type数量并获得既有variance。需要domain name、 ref/out 参数、特殊calling conv…

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

第3章:泛型、委托和事件(建议32-45):机制与证据

切换《第3章:泛型、委托和事件(建议32-45)》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 为什么泛型、委托和变体属于同一个类型契约

泛型把“输入类型与输出类型有什么关系”交给compiler证明;delegate把“以后要调用哪段typed behavior”变成value;event再限制谁能发布这段behavior;variance则回答一个generic/delegate type能否沿继承关系安全转换。它们共同解决的不是…

可核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么泛型、委托和变体属于同一个类型契约」的收益与反例。

学完《第3章:泛型、委托和事件(建议32-45)》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

第3章:泛型、委托和事件(建议32-45):失效与核验

为什么泛型、委托和变体属于同一个类型契约

典型失效

若把「为什么泛型、委托和变体属于同一个类型契约」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。

核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么泛型、委托和变体属于同一个类型契约」的收益与反例。

Generic Contract与Default(建议32…

典型失效

若把「Generic Contract与Default(建议32…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。

核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Generic Contract与Default(建议32…」的收益与反例。

Delegate、Closure与Event Owners…

典型失效

若把「Delegate、Closure与Event Owners…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。

核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Delegate、Closure与Event Owners…」的收益与反例。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么泛型、委托和变体属于同一个类型契约

泛型把“输入类型与输出类型有什么关系”交给compiler证明;delegate把“以后要调用哪段typed behavior”变成value;event再限制谁能发布这段behavior;variance则回答一个generic/delegate type能否沿继承关系安全转换。它们共同解决的不是语法简写,而是type information在时间和边界上能保留多久。

先预测:Cache<User>Cache<Order>是否共享同一个static field;for循环创建的三个lambda稍后会打印三个值还是同一个值;IList<Dog>为什么不能赋给IList<Animal>,但IEnumerable<Dog>可以。答案都来自实际读写能力,而不是关键词记忆。

Generic Contract与Default(建议32-35)

建议32:总是优先考虑泛型

当同一算法适用于多种type且需要保留input/output relationship时,generic优于object+cast:caller获得compile-time checking,value type通常避免boxing,implementation也无需type switch。优先不等于所有variation都用type parameter;行为本质不同、只共享少量代码时,interface/polymorphism或composition更清楚。

public static T PickBest<T>(IEnumerable<T> source, IComparer<T> comparer)
{
    using IEnumerator<T> iterator = source.GetEnumerator();
    if (!iterator.MoveNext()) throw new InvalidOperationException("Sequence is empty.");
 
    T best = iterator.Current;
    while (iterator.MoveNext())
        if (comparer.Compare(iterator.Current, best) > 0)
            best = iterator.Current;
    return best;
}

generic不是性能自动开关。code specialization、JIT/AOT size、reflection metadata和复杂constraint都有成本;先看它是否让非法调用无法表达。

建议33:避免在泛型类型中声明静态成员

generic type中的static field按closed type分别存在:Metrics<User>.CountMetrics<Order>.Count不是同一计数器。这可能正是per-type cache的意图,也可能悄悄把global state分裂并放大memory/initialization。原书的“避免”应改写为“只有明确需要按T分区时才使用”。

public static class SerializerCache<T>
{
    public static readonly JsonTypeInfo<T> Metadata = BuildMetadata<T>();
}
 
// Correct only because metadata is intentionally different for every T.

若需要process-wide registry,把state放在non-generic owner,并以Type或explicit key索引。测试至少创建两个closed types,验证共享/隔离符合设计。

建议34:为泛型参数设定约束

constraint声明implementation实际需要的capability,让body可直接调用成员并把错误提前到call site。应选择最小constraint:需要stable key可用notnull,需要比较可依赖IComparable<T>或传入comparer,构造对象前再考虑new()。不为“看起来严谨”限制成某个concrete base class。

public static Dictionary<TKey, TValue> IndexBy<TKey, TValue>(
    IEnumerable<TValue> values,
    Func<TValue, TKey> keySelector)
    where TKey : notnull
{
    return values.ToDictionary(keySelector);
}

constraint不能表达所有domain invariant,例如“金额非负”;这些仍由constructor/validation承担。classstructunmanaged、interface与new()也会影响nullable分析和可用operation,要为每一项写出理由。

建议35:使用default为泛型类型变量指定初始值

default(T)/default是未知T的zero/null initialization,适合buffer slot、Try pattern的失败output或显式empty state。它不保证是合法domain value:reference是null,numeric是0,struct fields全为default,可能绕过constructor invariant。

public static bool TryFind<T>(IEnumerable<T> source, Predicate<T> match, out T value)
{
    foreach (T item in source)
        if (match(item))
        {
            value = item;
            return true;
        }
 
    value = default!; // Valid only because false forbids consuming value.
    return false;
}

nullable context里的default!只抑制analysis,不改变runtime value。API最好用bool/result/discriminated state让caller无法把failure default误当成功数据。

分步1 / 3

切换object、generic、static-per-T、constraint与default

Delegate、Closure与Event Ownership(建议36-41)

建议36:使用FCL中的委托声明

常见callable shape优先复用ActionFuncPredicateComparisonEventHandler<TEventArgs>,降低无意义delegate type数量并获得既有variance。需要domain name、ref/out参数、特殊calling convention或清晰public API语义时,再声明named delegate。

public static TResult Retry<TResult>(
    Func<CancellationToken, TResult> operation,
    Predicate<Exception> shouldRetry,
    CancellationToken cancellationToken)
{
    // Retry policy omitted; the signatures preserve result and cancellation types.
    return operation(cancellationToken);
}

不要因signature相同就认定semantics相同。Func<string,bool>可能表示validation、authorization或filter,public boundary若容易混淆,应封装policy interface或named type。

建议37:使用Lambda表达式代替方法和匿名方法

短、局部、无副作用的behavior用lambda能让policy贴近调用点;anonymous method通常没有额外价值。逻辑复杂、递归、复用、需要独立测试或stack trace name时,named method更合适。lambda是否分配取决于capture与compiler/runtime优化,不从syntax猜测。

Order[] recent = orders
    .Where(order => order.IsPaid)
    .OrderByDescending(order => order.PaidAt)
    .Take(limit)
    .ToArray();

建议38:小心闭包中的陷阱

closure捕获的是variable storage,不保证复制当前value。for loop中多个lambda引用同一个index,稍后执行时可能都看到最终值;捕获this也可能让短命callback长期保留整个owner/object graph。

var actions = new List<Action>();
for (int index = 0; index < 3; index++)
{
    int snapshot = index;
    actions.Add(() => Console.WriteLine(snapshot));
}

现代C# foreach iteration variable按iteration具有独立capture语义,但不要把它泛化到for或外部mutable variable。检查delegate被保存多久、谁修改captured state、是否跨线程以及是否需要unsubscribe。

建议39:了解委托的实质

delegate是包含method与可选target的typed callable object;multicast delegate还维护ordered invocation list。delegate value近似immutable,+=/-=产生新组合并重新赋值。instance method持有target,static method没有instance target;这解释了为什么subscription会延长subscriber lifetime。

Action handlers = First;
handlers += Second;
 
foreach (Action handler in handlers.GetInvocationList())
{
    try { handler(); }
    catch (Exception exception) { Report(handler, exception); }
}

是否逐个隔离异常属于publisher policy,不能随意吞掉。delegate equality/removal也依赖method-target pair,使用新lambda退订通常匹配不到原来的instance。

建议40:使用event关键字为委托施加保护

public delegate field允许外部覆盖整个invocation list或直接raise;event只允许外部+=/-=, invocation权保留给declaring type。custom add/remove accessor可转接或同步,但复杂accessor要保持subscription identity和exception semantics。

public event EventHandler<OrderPaidEventArgs>? OrderPaid;
 
protected virtual void OnOrderPaid(OrderPaidEventArgs args)
{
    EventHandler<OrderPaidEventArgs>? handler = OrderPaid;
    handler?.Invoke(this, args);
}

local copy给出一次稳定delegate snapshot,但不把整个publisher state变成transactional。强publisher-短subscriber组合可能造成memory leak;可用explicit unsubscribe、subscription token、weak-event pattern或统一lifetime owner。

建议41:实现标准的事件模型

.NET标准event通常使用EventHandlerEventHandler<TEventArgs>,第一个参数是sender,第二个是EventArgs payload;publisher提供protected virtual OnXxx让derived type扩展raise behavior。payload应描述已经发生的事实,通常immutable;可取消operation则显式设计cancel contract,避免模糊双向mutation。

public sealed class OrderPaidEventArgs : EventArgs
{
    public OrderPaidEventArgs(OrderId orderId, decimal amount)
        => (OrderId, Amount) = (orderId, amount);
 
    public OrderId OrderId { get; }
    public decimal Amount { get; }
}

同步event handler在publisher thread执行;UI/thread-affinity、slow I/O与handler exception都可能反向影响publisher。需要durability、retries或process boundary时,event不是message broker,应换outbox/queue等明确机制。

分步1 / 3

切换FCL delegate、lambda、closure、multicast与event

Invariance、Covariance与Contravariance(建议42-45)

建议42:使用泛型参数兼容泛型接口的不可变性

mutable generic interface通常invariant:即使Dog继承Animal,IList<Dog>也不能当IList<Animal>,否则caller可插入Cat破坏原list。不要用cast绕过;让method自身成为generic并约束元素,或只请求实际需要的read-only producer contract。

public static void PrintNames<TAnimal>(IEnumerable<TAnimal> animals)
    where TAnimal : Animal
{
    foreach (TAnimal animal in animals)
        Console.WriteLine(animal.Name);
}

generic method保留具体T,同时允许body按base constraint使用共同能力。若需要写入,parameter应表达正确consumer direction,而不是把producer collection错误扩宽。

建议43:让接口中的泛型参数支持协变

type parameter只出现在output position时,可标为outIProducer<out T>能让producer-of-Dog作为producer-of-Animal,因为caller只会读出Animal。property getter和return value是output;method input、setter或ref会破坏covariance。

public interface IProducer<out T>
{
    T Next();
}
 
IProducer<Dog> dogs = GetDogProducer();
IProducer<Animal> animals = dogs;

建议44:理解委托中的协变

delegate method binding支持compatible return covariance:需要Func<Animal>的地方,可绑定返回Dog的方法,因为每个Dog都是Animal。FCL Func<out TResult>也利用generic covariance。参数方向相反,不能用“继承方向一样”作为记忆法,要检查caller提供和接收什么。

static Dog CreateDog() => new Dog("Ada");
Func<Animal> createAnimal = CreateDog;
Animal animal = createAnimal();

建议45:为泛型类型参数指定逆变

type parameter只被consume时可标为inIComparer<Animal>能用于排序Dog,因为它承诺接受任意Animal,自然也能接受Dog;这就是contravariance。parameter不能作为return输出,也不能出现在不安全的invariant position。

public interface IHandler<in T>
{
    void Handle(T message);
}
 
IHandler<Animal> animalHandler = new AuditAnimalHandler();
IHandler<Dog> dogHandler = animalHandler;
dogHandler.Handle(new Dog("Lin"));

variance只适用于reference type conversion,并受language/runtime规则限制。它改善assignment compatibility,不改变对象本身,也不应掩盖API同时produce/consume T的事实;双向接口保持invariant通常正确。

分步1 / 3

切换invariance、interface covariance/contravariance与delegate variance

本章回顾:保留Type Information,也管理Lifetime

  1. generic的价值是保留type relationship;constraints只声明body真正需要的capability。
  2. per-T static按closed type隔离,default只是language initialization,不自动是domain value。
  3. delegate保存method/target,closure还保存captured storage;因此callable同时是type contract和lifetime object。
  4. event限制raise ownership,标准模型还必须定义payload、thread、exception和unsubscribe policy。
  5. covariance允许安全producer widening,contravariance允许consumer narrowing,双向mutable contract保持invariant。
  6. 14条建议的共同验收问题是:compiler证明了什么,runtime持有什么,谁能调用或修改什么。

练习

问题 1:一个generic cache既要按T保存serializer metadata,又要统计全局hit count,static成员应放在哪里?

问题 2:事件订阅为何可能泄漏subscriber,怎样验证修复?

问题 3:为什么IEnumerable<Dog>可转为IEnumerable<Animal>IComparer<Animal>可转为IComparer<Dog>,而IList<Dog>不能转为IList<Animal>

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

generic type identity
closure lifetime
invocation list
event ownership
variance position

原版目录概念补充核对

以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。

建议33:避免在泛型类型中声明静态成员:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议33:避免在泛型类型中声明静态成员涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。

建议34:为泛型参数设定约束:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议34:为泛型参数设定约束涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。

建议35:使用default为泛型类型变量指定初始值:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议35:使用default为泛型类型变量指定初始值涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。

建议36:使用FCL中的委托声明:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议36:使用FCL中的委托声明是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议37:使用Lambda表达式代替方法和匿名方法:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议37:使用Lambda表达式代替方法和匿名方法是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议38:小心闭包中的陷阱:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议38:小心闭包中的陷阱是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议39:了解委托的实质:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议39:了解委托的实质是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议40:使用event关键字为委托施加保护:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议40:使用event关键字为委托施加保护是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议41:实现标准的事件模型:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议41:实现标准的事件模型是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议42:使用泛型参数兼容泛型接口的不可变性:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议42:使用泛型参数兼容泛型接口的不可变性涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。

建议43:让接口中的泛型参数支持协变:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议43:让接口中的泛型参数支持协变涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。

建议44:理解委托中的协变:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议44:理解委托中的协变是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议45:为泛型类型参数指定逆变:机制、边界与证据

  1. 泛型、委托和事件(建议32-45)中的建议45:为泛型类型参数指定逆变涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。

讨论

评论区加载中…