第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
为什么泛型、委托和变体属于同一个类型契约
泛型把“输入类型与输出类型有什么关系”交给compiler证明;delegate把“以后要调用哪段typed behavior”变成value;event再限制谁能发布这段behavior;variance则回答一个generic/delegate type能否沿继承关系安全转换。它们共同解决的不是…
- 2
Generic Contract与Default(建议32…
当同一算法适用于多种type且需要保留input/output relationship时,generic优于 object +cast:caller获得compile-time checking,value type通常避免boxing,implementation也无需type switch。优…
- 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>.Count与Metrics<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承担。class、struct、unmanaged、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误当成功数据。
切换object、generic、static-per-T、constraint与default
Delegate、Closure与Event Ownership(建议36-41)
建议36:使用FCL中的委托声明
常见callable shape优先复用Action、Func、Predicate、Comparison与EventHandler<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通常使用EventHandler或EventHandler<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等明确机制。
切换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时,可标为out:IProducer<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时可标为in。IComparer<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通常正确。
切换invariance、interface covariance/contravariance与delegate variance
本章回顾:保留Type Information,也管理Lifetime
- generic的价值是保留type relationship;constraints只声明body真正需要的capability。
- per-T static按closed type隔离,default只是language initialization,不自动是domain value。
- delegate保存method/target,closure还保存captured storage;因此callable同时是type contract和lifetime object。
- event限制raise ownership,标准模型还必须定义payload、thread、exception和unsubscribe policy。
- covariance允许安全producer widening,contravariance允许consumer narrowing,双向mutable contract保持invariant。
- 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:避免在泛型类型中声明静态成员:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议33:避免在泛型类型中声明静态成员涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。
建议34:为泛型参数设定约束:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议34:为泛型参数设定约束涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。
建议35:使用default为泛型类型变量指定初始值:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议35:使用default为泛型类型变量指定初始值涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。
建议36:使用FCL中的委托声明:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议36:使用FCL中的委托声明是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议37:使用Lambda表达式代替方法和匿名方法:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议37:使用Lambda表达式代替方法和匿名方法是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议38:小心闭包中的陷阱:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议38:小心闭包中的陷阱是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议39:了解委托的实质:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议39:了解委托的实质是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议40:使用event关键字为委托施加保护:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议40:使用event关键字为委托施加保护是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议41:实现标准的事件模型:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议41:实现标准的事件模型是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议42:使用泛型参数兼容泛型接口的不可变性:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议42:使用泛型参数兼容泛型接口的不可变性涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。
建议43:让接口中的泛型参数支持协变:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议43:让接口中的泛型参数支持协变涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。
建议44:理解委托中的协变:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议44:理解委托中的协变是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议45:为泛型类型参数指定逆变:机制、边界与证据
- 泛型、委托和事件(建议32-45)中的建议45:为泛型类型参数指定逆变涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。