Chapter 3: Working with Generics (Items 18-28)
完整覆盖第3版Item 18-28:最小约束、runtime specialization、ordering、disposable type parameter、variance、delegate method constraint、generic API shape与extension。
学习目标
- 能设计minimal and sufficient generic constraints,并比较general path、runtime specialization与modern static abstract interface contract
- 能推导T在input/output/storage/disposal位置对应的variance与ownership,写出可替代的generic interface
- 能判断generic method、generic type、classic interface bridge与extension method的API边界并复现compatibility行为
机制总览
泛型约束与可替换性设计实验
- 1
最小约束
只声明算法实际调用的成员和构造能力。
- 2
替换方向
协变只安全地产出 T,逆变只安全地消费 T。
- 3
扩展策略
优先泛型方法和最小接口,再用扩展方法补充便利操作。
章级决策实验
泛型约束与可替换性设计实验
从调用方真正需要的能力出发,逐层检查约束、variance 与扩展点。
选择推理阶段
当前阶段 · 最小约束
只声明算法实际调用的成员和构造能力。
可核验证据
可接受/应拒绝类型的编译矩阵。
好的泛型 API 承诺的是最小能力集合;每增加一个约束、特化或接口,都必须换来可验证的语义。
失效—证据矩阵
泛型约束与可替换性设计实验
最小约束
典型失效
过强约束排除合法类型,过弱约束迫使运行时分支。
核验证据
可接受/应拒绝类型的编译矩阵。
替换方向
典型失效
把读写能力混在同一接口会阻断 variance 或产生不安全假设。
核验证据
赋值兼容测试与输入输出位置审计。
扩展策略
典型失效
为每个构造类型建特化层,造成重复和二义性。
核验证据
API surface diff、重载解析测试与版本兼容测试。
为什么Generic Design必须同时约束能力和承诺
Generic的价值不是把object换成T,而是保留caller与implementation之间的type relation。Constraint太弱,method body只能猜测;constraint太强,可用类型被无故排除;runtime specialization若改变semantics,则同一个generic API对不同T说了两套话。
先预测:为方便实现而顺手加class, new()是否无害;List<Dog>能否当作List<Animal>;为string继承一个generic base specialization是否自然?答案分别是否、不能、通常不应。
Constraint、Specialization与Ordering(Items 18-20)
Item 18: Always Define Constraints That Are Minimal and Sufficient
从method body反推能力:只比较就约束IComparable<T>,只需要non-null reference才考虑class/notnull,只有确实构造T才用new()。每多一个constraint都是public compatibility restriction,应有call-site需求和compile-time test。
Item 19: Specialize Generic Algorithms Using Runtime Type Checking
当profile证明某个closed type有显著fast path,可在generic algorithm内部做typeof(T)检查,但结果、exception、ordering和side effects必须与general path一致。Specialization是performance implementation detail,不应要求caller知道另一个subclass或API。
Item 20: Implement Ordering Relations with IComparable<T> and IComparer<T>
IComparable<T>表达type唯一、稳定的natural order;价格升序、名称culture-aware等contextual order用IComparer<T>。验证sign(Compare(a,b))=-sign(Compare(b,a))、transitivity,并明确compare-zero是否必须与equality一致,尤其是sorted collection key。
public static T Min<T>(T left, T right) where T : IComparable<T> =>
left.CompareTo(right) <= 0 ? left : right;切换constraint、fast path、ordering与generic math
Disposable T、Variance与Callable Capability(Items 21-23)
Item 21: Always Create Generic Classes That Support Disposable Type Parameters
若generic container创建或接收T的ownership,且T可释放,container必须提供清晰的cascade disposal;仅borrow T则不能擅自释放。可用where T : IDisposable建立专门owner type,或显式owner policy,避免runtime猜测后把borrowed value一起Dispose。
Item 22: Support Generic Covariance and Contravariance
Producer只返回T,可声明out T并允许IProducer<Dog>转为IProducer<Animal>;consumer只接收T,可声明in T并反向转换。List<T>同时读写所以invariant;variance只适用于reference conversions和interface/delegate type parameters。
Item 23: Use Delegates to Define Method Constraints on Type Parameters
C# 6无法约束“必须存在某个static method/operator”,原书建议把所需operation作为Func/named delegate传入。这仍适合runtime strategy与test substitution。现代C#在target framework允许时也可用static abstract interface members做compile-time generic math;二者分别表达runtime supplied behavior和type supplied static capability。
public static TResult Fold<T, TResult>(
IEnumerable<T> source,
TResult seed,
Func<TResult, T, TResult> combine) => source.Aggregate(seed, combine);切换owned T、producer、consumer与method capability
Generic API Shape与Compatibility(Items 24-28)
Item 24: Do Not Create Generic Specialization on Base Classes or Interfaces
不要以StringRepository : Repository<string>伪装template specialization并改变base semantics。优先generic implementation内受测fast path、composition strategy或清晰overload;inheritance必须满足substitutability,而非仅复用代码。
Item 25: Prefer Generic Methods Unless Type Parameters Are Instance Fields
若T只参与一个operation,不存入instance state,把T放method上让caller inference并减少constructed type数量。只有instance fields、lifetime或多methods共同依赖同一个T时,generic type才表达真实type identity。
Item 26: Implement Classic Interfaces in Addition to Generic Interfaces
面向仍消费non-generic interface的framework,generic collection可同时实现IEnumerable<T>与IEnumerable,后者桥接到同一enumerator semantics。不要复制两套逻辑;bridge必须保持order、fault和mutation behavior一致。
Item 27: Augment Minimal Interface Contracts with Extension Methods
Interface只保留implementer必须提供的primitive operations,convenience behavior由extension组合,避免每次加helper都破坏所有implementers。Extension不能访问private state,也不能替代需要override、多态或ownership的member。
Item 28: Consider Enhancing Constructed Types with Extension Methods
可以为IDictionary<string,T>等closed shape提供domain-focused extension,但要控制namespace与overload,避免与instance member或其他extensions产生解析惊喜。名称应揭示cost和enumeration,例如不要把remote I/O藏在看似cheap的Get里。
public static TValue GetRequired<TValue>(
this IReadOnlyDictionary<string, TValue> source,
string key) => source.TryGetValue(key, out var value)
? value
: throw new KeyNotFoundException(key);切换specialization、generic method、bridge和extension
本章回顾:Generic API只承诺一种Semantics
- Constraint必须最小且充分;每个额外constraint都是public compatibility cost。
- Runtime specialization只优化,不改变generic result、fault、ordering和side effects。
- Natural ordering在type上,contextual ordering由comparer提供并通过ordering laws。
- T的input/output/storage位置决定variance,ownership决定是否cascade Dispose。
- C# 6的delegate method constraint与现代static abstract members解决不同binding时机。
- Generic method、classic bridge与extension保持API surface最小,inheritance不得冒充specialization。
练习
问题 1:一个排序API同时支持默认顺序、按名称排序和byte fast path,应怎样设计?
问题 2:Buffer<T>既返回又接收T,还可能拥有T,能否声明out?
问题 3:何时使用generic method、extension和classic interface bridge?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- minimal sufficient constraint
- covariant producer
- contravariant consumer
- classic interface bridge
- extension contract