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. 1

    最小约束

    只声明算法实际调用的成员和构造能力。

  2. 2

    替换方向

    协变只安全地产出 T,逆变只安全地消费 T。

  3. 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;
分步1 / 3

切换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);
分步1 / 3

切换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);
分步1 / 3

切换specialization、generic method、bridge和extension

本章回顾:Generic API只承诺一种Semantics

  1. Constraint必须最小且充分;每个额外constraint都是public compatibility cost。
  2. Runtime specialization只优化,不改变generic result、fault、ordering和side effects。
  3. Natural ordering在type上,contextual ordering由comparer提供并通过ordering laws。
  4. T的input/output/storage位置决定variance,ownership决定是否cascade Dispose。
  5. C# 6的delegate method constraint与现代static abstract members解决不同binding时机。
  6. 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

讨论

评论区加载中…