第7章:成员设计(建议90-101)
覆盖抽象类构造器、字段与属性、集合属性、构造期初始化、override/new和虚调用,以及接口化公开面、参数抽象、params、重写签名、静态/实例与扩展方法。
学习目标
- 能设计constructor、property和collection surface,使object在publication前满足invariant且不泄露representation ownership
- 能区分override与new的dispatch slot,分析constructor virtual call、static/instance和extension binding的真实行为
- 能比较concrete/base/interface、params与override signature,判断public input/output contract是否保持substitutability
机制总览
第7章:成员设计(建议90-101):机制路径
- 1
为什么成员签名会决定未来还能否修改实现
public member不只是当前实现的入口,它向caller暴露可构造状态、mutation capability、dispatch规则和type assumptions。public field让storage成为contract,List property让collection owners…
- 2
Construction、State与Dispatch(建…
abstract type不能直接实例化,public constructor给出一个实际上不可调用的surface;通常使用protected constructor,只允许derived construction chain建立base invariant。若外部要选择具体实现,提供factor…
- 3
Abstraction Surface与Signature…
public return/property type只暴露caller需要的capability,例如返回 IReadOnlyList 而不是List、返回Stream而不是FileStream,可保留implementation替换自由。但不要盲目返回过宽 IEnumerable 而隐藏rand…
章级决策实验
第7章:成员设计(建议90-101):机制与证据
切换《第7章:成员设计(建议90-101)》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么成员签名会决定未来还能否修改实现
public member不只是当前实现的入口,它向caller暴露可构造状态、mutation capability、dispatch规则和type assumptions。public field让storage成为contract,List property让collection owners…
可核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么成员签名会决定未来还能否修改实现」的收益与反例。
学完《第7章:成员设计(建议90-101)》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
第7章:成员设计(建议90-101):失效与核验
为什么成员签名会决定未来还能否修改实现
典型失效
若把「为什么成员签名会决定未来还能否修改实现」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么成员签名会决定未来还能否修改实现」的收益与反例。
Construction、State与Dispatch(建…
典型失效
若把「Construction、State与Dispatch(建…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Construction、State与Dispatch(建…」的收益与反例。
Abstraction Surface与Signature…
典型失效
若把「Abstraction Surface与Signature…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Abstraction Surface与Signature…」的收益与反例。
为什么成员签名会决定未来还能否修改实现
public member不只是当前实现的入口,它向caller暴露可构造状态、mutation capability、dispatch规则和type assumptions。public field让storage成为contract,List property让collection ownership外泄,concrete parameter让caller与实现共同被绑住,constructor里的virtual call则在对象尚未valid时提前开放polymorphism。
先预测:abstract class的public constructor虽然不能直接new,是否仍是合适visibility;base reference调用derived的new同名method会走哪一个;extension method能否覆盖instance member。答案分别是否、base slot、不能。
Construction、State与Dispatch(建议90-95)
建议90:不要为抽象类提供公开的构造方法
abstract type不能直接实例化,public constructor给出一个实际上不可调用的surface;通常使用protected constructor,只允许derived construction chain建立base invariant。若外部要选择具体实现,提供factory或依赖interface,而不是暴露abstract ctor。
public abstract class Report
{
protected Report(ReportId id)
=> Id = id.IsEmpty ? throw new ArgumentException("ID is required.", nameof(id)) : id;
public ReportId Id { get; }
}protected也不是越多越好。base constructor只接收base真正拥有的state,避免把derived-specific options塞进层级。
建议91:可见字段应该重构为属性
public/protected field把representation和uncontrolled write能力永久暴露;property保留相同使用形状,同时可控制getter/setter visibility、validation、lazy computation、notification和binary evolution。property也不是“字段加壳”:有expensive I/O或显著side effect的operation应是method。
public sealed class Account
{
public string DisplayName { get; private set; } = "Unnamed";
public void Rename(string value)
=> DisplayName = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Name is required.", nameof(value))
: value.Trim();
}建议92:谨慎将数组或集合作为属性
返回owned array/List会允许caller替换元素、Add/Remove或持有alias,绕过owner invariant。按语义返回immutable snapshot、IReadOnlyList<T> live view或IEnumerable<T> stream;它们限制的能力不同,且read-only interface不保证底层不变化。
private readonly List<OrderLine> _lines = new();
public ImmutableArray<OrderLine> Lines => _lines.ToImmutableArray();
public void AddLine(OrderLine line)
{
Validate(line);
_lines.Add(line);
}snapshot有copy成本,live view有temporal change,stream有lifetime/re-enumeration;contract必须明说。
建议93:构造方法应初始化主要属性和字段
required state应由constructor/factory一次validate并赋值,使object从第一刻可用;可选配置才留init/default。避免caller按特定顺序设置多个properties才能形成valid object,这会产生temporal invalidity并让并发publication更危险。
public sealed class Transfer
{
public Transfer(AccountId from, AccountId to, Money amount)
{
if (from == to) throw new ArgumentException("Accounts must differ.");
if (amount <= Money.Zero) throw new ArgumentOutOfRangeException(nameof(amount));
(From, To, Amount) = (from, to, amount);
}
public AccountId From { get; }
public AccountId To { get; }
public Money Amount { get; }
}建议94:区别对待override和new
override替换base virtual slot,base-typed reference也会按runtime type dispatch到derived override;new只隐藏同名member,选择取决于compile-time reference type,两套slot并存。除非确实要表达无关的新member并接受这种差异,通常不要用new“模拟override”。
class Base { public virtual string Name() => "base"; }
class Derived : Base
{
public override string Name() => "derived";
}
Base value = new Derived();
Console.WriteLine(value.Name()); // derived建议95:避免在构造方法中调用虚成员
base constructor调用virtual member会dispatch到derived override,但derived field initializers/body尚未完成,override观察到default/partial state,甚至把this泄露给外部。constructor只执行non-virtual invariant setup;需要polymorphic initialization时用factory在完整construction后调用明确hook。
public static async Task<T> CreateAsync<T>(Func<T> construct, CancellationToken token)
where T : IAsyncInitializable
{
T instance = construct();
await instance.InitializeAsync(token);
return instance;
}切换abstract ctor、field/property、constructor与virtual call
Abstraction Surface与Signature Compatibility(建议96-99)
建议96:成员应优先考虑公开基类型或接口
public return/property type只暴露caller需要的capability,例如返回IReadOnlyList<T>而不是List、返回Stream而不是FileStream,可保留implementation替换自由。但不要盲目返回过宽IEnumerable<T>而隐藏random access/count等稳定能力;选择最小且足够的contract。
public IReadOnlyDictionary<OrderId, Order> Snapshot()
=> _orders.ToImmutableDictionary();返回interface不自动immutable,也可能隐藏disposal/lifetime。文档和type共同表达ownership。
建议97:优先考虑将基类型或接口作为参数传递
parameter应要求实现所需的最小capability,让caller可提供不同implementation与test double。只调用Read时接收Stream而非FileStream,只枚举时接收IEnumerable<T>;若算法需要index/count,应诚实接收IReadOnlyList<T>,不要每次ElementAt。
public async Task<Hash> ComputeHashAsync(Stream source, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(source);
return await _hasher.ComputeAsync(source, token);
}建议98:用params减少重复参数
params适合零到多个同类型、语义同质且通常数量少的末尾参数,改善call-site;它不是替代collection model的万能语法。调用可能创建array,overload resolution与null调用也需测试;hot path/large input可另提供span/list overload。
public static PermissionSet Of(params Permission[] permissions)
=> new(permissions);
PermissionSet set = PermissionSet.Of(Permission.Read, Permission.Write);不要用params把不同语义的多个值塞进同一array,named options/record更清晰。
建议99:重写时不应使用子类参数
override必须保持base method signature,不能把Animal参数缩窄为Dog,否则通过base contract传入Cat会失败,违反substitutability。需要Dog-specific behavior时,在override内pattern-check并按base contract定义unsupported结果,或重构generic/interface hierarchy。
public override void Handle(Animal animal)
{
if (animal is not Dog dog)
throw new NotSupportedException("This handler supports dogs only.");
HandleDog(dog);
}更好的设计常是在type level表达IHandler<Dog>,而不是声称实现general Animal handler后拒绝大部分合法输入。
切换return interface、parameter base、params、override input与collection return
Static、Instance与Extension Binding(建议100-101)
建议100:静态方法和实例方法没有区别
原题强调二者都可承载behavior,但“没有区别”不能按字面理解。instance method有receiver、可访问instance state并参与virtual dispatch;static method在compile time绑定,没有this和override。选择依据是behavior是否属于一个有效object及其polymorphic contract,而非微小call性能猜测。
public sealed class Money
{
public Money Add(Money other) => new(Amount + other.Amount, Currency);
public static Money Zero(string currency) => new(0m, currency);
}pure factory/helper可static;依赖global mutable static state会隐藏dependency并破坏test isolation,应该注入service。不要为了“方便调用”把domain behavior剥离出owner。
建议101:使用扩展方法,向现有类型“添加”方法
extension method是static method加this首参数,由compile-time receiver type、namespace/import和overload resolution选择;它不改变原type、不能访问private state、不能真正override virtual member,且同名instance member永远优先。适合稳定、无ownership的convenience operation。
public static class OrderExtensions
{
public static Money Total(this IEnumerable<OrderLine> lines)
=> lines.Aggregate(Money.Zero("CNY"), (sum, line) => sum.Add(line.Total));
}若operation需要private invariant、mutable state或virtual specialization,应进入原type/interface/service。对第三方type扩展时使用项目特定namespace,避免“万能Extensions”污染全局发现。
切换override、new、instance、static与extension
本章回顾:公开最小能力,保留最大演进空间
- abstract constructor用protected,required state在publication前完成,constructor不调用virtual member。
- property隐藏storage但不自动隐藏collection mutation;snapshot、view和stream要分别定义ownership/lifetime。
- override共享virtual slot,new创建独立binding;base reference是验证polymorphism的关键case。
- return和parameter使用足够而不过度的base/interface contract,override不能缩窄合法输入。
- params只改善同质小参数call-site,仍有allocation和overload contract。
- static/instance按state与polymorphism选择,extension是有scope的static convenience,不是type修改。
练习
问题 1:一个abstract Aggregate怎样设计constructor和collection property,避免partial state与外部mutation?
问题 2:为什么接受FileStream和返回List通常比接受Stream、返回IReadOnlyList更难演进?
问题 3:怎样证明override、new和extension method的调用目标?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- construction invariant
- encapsulation boundary
- dispatch slot
- parameter generality
- extension scope
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
建议91:可见字段应该重构为属性:机制、边界与证据
- 成员设计(建议90-101)中的建议91:可见字段应该重构为属性是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议92:谨慎将数组或集合作为属性:机制、边界与证据
- 成员设计(建议90-101)中的建议92:谨慎将数组或集合作为属性涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。
建议93:构造方法应初始化主要属性和字段:机制、边界与证据
- 成员设计(建议90-101)中的建议93:构造方法应初始化主要属性和字段是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议94:区别对待override和new:机制、边界与证据
- 成员设计(建议90-101)中的建议94:区别对待override和new是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议95:避免在构造方法中调用虚成员:机制、边界与证据
- 成员设计(建议90-101)中的建议95:避免在构造方法中调用虚成员是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议96:成员应优先考虑公开基类型或接口:机制、边界与证据
- 成员设计(建议90-101)中的建议96:成员应优先考虑公开基类型或接口是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议97:优先考虑将基类型或接口作为参数传递:机制、边界与证据
- 成员设计(建议90-101)中的建议97:优先考虑将基类型或接口作为参数传递是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议98:用params减少重复参数:机制、边界与证据
- 成员设计(建议90-101)中的建议98:用params减少重复参数是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议99:重写时不应使用子类参数:机制、边界与证据
- 成员设计(建议90-101)中的建议99:重写时不应使用子类参数是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议100:静态方法和实例方法没有区别:机制、边界与证据
- 成员设计(建议90-101)中的建议100:静态方法和实例方法没有区别是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议101:使用扩展方法,向现有类型“添加”方法:机制、边界与证据
- 成员设计(建议90-101)中的建议101:使用扩展方法,向现有类型“添加”方法是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。