第1章:基本语言要素(建议1-15)

覆盖字符串、转换与解析、nullable/const/enum、运算符/比较/相等/哈希、格式化、复制和dynamic反射边界。

学习目标

  • 能分析字符串、cast/pattern与Parse/TryParse的真实conversion、allocation和failure contract,而不是套用性能口号
  • 能设计nullable、const/readonly、enum、operator、comparison、Equals/GetHashCode相互一致的value contract
  • 能比较format、shallow/deep copy与dynamic/reflection的representation、identity和binding边界

为什么“建议”必须还原成Decision Boundary

原书第1章的15条建议来自C#/.NET 4时代,核心问题仍成立:转换失败是不是正常输入,default value有没有domain meaning,相等对象能否稳定进入hash collection,复制后是否共享nested state,runtime binding是否值得失去compile-time proof。但具体结论不能脱离现代compiler/runtime与API contract机械照抄。

本章保留每条原书建议的标题与问题意识,同时为它补上适用条件、反例和验收证据。先预测:"count=" + 9是否一定boxing;外部文本无效时Parse是否总是错;enum持久化时显式值是风险还是保护;两个Equals相等对象若hash不同会怎样。

字符串、转换与解析(建议1-4)

建议1:正确操作字符串

string immutable意味着每次产生不同text的operation返回新string,但compiler可能折叠constants,runtime也有不同Concat/interpolation handlers;不能从source表面断言一次拼接必然boxing。两三段表达式优先清晰的interpolation,循环或未知段数用StringBuilder,protocol text显式culture/encoding。

string label = $"count={count}";
 
var builder = new StringBuilder();
foreach (Order order in orders)
    builder.Append(order.Id).Append(':').AppendLine(order.Status);
 
string report = builder.ToString();

验收使用allocation profiler或BenchmarkDotNet,并检查warm-up、result consumption与input size;只看一段旧IL不能代表当前target runtime。StringBuilder也不是所有拼接的默认答案,小表达式反而增加object与复杂度。

建议2:使用默认转型方法

优先使用language/FCL定义的conversion:numeric checked cast、Convert、Parse/TryParse、explicit operator或TypeConverter各有不同null/overflow/culture语义。所谓“默认”不是随便选一个,而是先明确source/target与failure contract,再选择标准实现,避免手写映射漏掉boundary。

int narrowed = checked((int)longValue);
int convertedNull = Convert.ToInt32(nullableText); // null becomes 0
int parsed = int.Parse(text, CultureInfo.InvariantCulture); // null/invalid throws

Convert与Parse对null不同,checked与unchecked对overflow不同。wrapper要把这些语义写进name/tests,不能因return type相同就互换。

建议3:区别对待强制转型与as和is

explicit cast表达“contract保证compatible,不兼容就是exception”;as把reference/nullable conversion failure变null;C# 7 declaration pattern把test与typed binding合并。is后再cast会重复表达意图,现代代码常直接pattern。

if (candidate is Order order)
    Process(order);
 
Order required = (Order)candidate; // Fail immediately if contract is broken.
Order optional = candidate as Order;

不要把as概括为总比cast快。选择依据是failure semantics与null ambiguity:input本来就可能null时,as结果无法区分null source和wrong type;pattern通常更清晰。

建议4:TryParse比Parse好

来自UI、file或network的无效文本通常是normal failure path,TryParse避免为常见分支构造exception。内部配置按contract必须有效时,Parse的exception能保留错误位置;“总是TryParse”会让caller忽略false并继续使用default。

if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int count))
    return ValidationResult.Invalid("count must be an integer");
 
int requiredPort = int.Parse(configuredPort, CultureInfo.InvariantCulture);
分步1 / 3

切换concat、StringBuilder、cast、pattern与TryParse

Nullable、readonly/const与enum(建议5-8)

建议5:使用int?来确保值类型也可以为null

nullable value type表达“值或缺失”,适合unknown age、optional timeout等真实domain absence。不要用0/-1 sentinel混合数据与控制;但若field业务上必填,就应在construction拒绝null,而不是到处传播nullable。

public sealed class Registration
{
    public Registration(int? age)
    {
        if (age is < 0 or > 130) throw new ArgumentOutOfRangeException(nameof(age));
        Age = age;
    }
 
    public int? Age { get; }
}

nullable要贯穿serialization/database/API mapping,测试null、0、boundary和round-trip。GetValueOrDefault只有在default确实等价时才使用。

建议6:区别readonly和const的使用方法

const是compile-time constant,consumer assembly通常把值内联;public const升级后旧consumer不重编译会保留旧值。readonly field在constructor/static constructor赋值,runtime读取,且每instance可不同。immutable reference readonly只保证field不能改指向,不保证object内部不变。

建议7:将0值作为枚举的默认值

所有enum storage的default bits为0,因此0应有明确意义:NoneUnknown或合法首态,并按domain选择。Flags enum通常提供None=0;non-flags若0绝不合法,也要在deserialization/API boundary验证,不能假设type system拒绝未命名数值。

建议8:避免给枚举类型的元素提供显式的值

这条建议减少无必要编号与重复风险,但不是持久化/协议enum的通则。若值写入database、wire或跨版本交换,显式稳定编号能防止中间插入member导致重编号;同时应保留未知值处理。纯process内enum可依赖自动递增,但不要让ordinal承担业务排序。

public enum JobState
{
    Unknown = 0,
    Queued = 10,
    Running = 20,
    Completed = 30,
}
 
public const int ProtocolVersion = 3;
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);

Operator、Comparison、Equality与Hash(建议9-12)

建议9:习惯重载运算符

只有type具备自然、无歧义且低惊讶的value semantics时才重载operator,例如Money加法、Vector运算。operator应与named method、Equals/CompareTo一致;成对实现==/!=、关系operator,并明确overflow/unit/currency。为“写起来短”重载会隐藏side effect或昂贵I/O。

建议10:创建对象时需要考虑是否实现比较器

先问domain是否存在唯一natural order。type自己的稳定natural order实现IComparable;不同页面/业务排序用IComparer policy,避免把“按姓名”“按时间”都塞进type。comparison需要antisymmetry、transitivity与0语义,null policy也要一致。

建议11:区别对待==和Equals

operator ==由compile-time operand types参与选择;Equals是virtual instance/value equality contract。reference type未重载==时通常比较identity,string等type重载为value equality。调用方需要选择identity还是domain value,不应混用。

建议12:重写Equals时也要重写GetHashCode

equal objects必须在同一comparer/process contract下产生相同hash;否则Dictionary/HashSet可能放入后找不到。hash不要求全局唯一,也不应持久化。参与equality/hash的key state在collection lifetime内保持immutable。

public readonly struct Money : IEquatable<Money>, IComparable<Money>
{
    public Money(decimal amount, string currency)
        => (Amount, Currency) = (amount, currency ?? throw new ArgumentNullException(nameof(currency)));
 
    public decimal Amount { get; }
    public string Currency { get; }
 
    public bool Equals(Money other) =>
        Amount == other.Amount && StringComparer.Ordinal.Equals(Currency, other.Currency);
 
    public override bool Equals(object obj) => obj is Money other && Equals(other);
    public override int GetHashCode() => HashCode.Combine(Amount, StringComparer.Ordinal.GetHashCode(Currency));
    public int CompareTo(Money other) =>
        Currency == other.Currency ? Amount.CompareTo(other.Amount) : throw new InvalidOperationException();
 
    public static bool operator ==(Money left, Money right) => left.Equals(right);
    public static bool operator !=(Money left, Money right) => !left.Equals(right);
}
分步1 / 3

切换nullable、const/readonly、enum、equality与comparison

Formatting、Copy与Dynamic Reflection(建议13-15)

建议13:为类型输出格式化字符串

type若同时面向UI、log与protocol,应实现明确representation contract:default ToString适合诊断,IFormattable支持format/culture,machine format固定InvariantCulture并最好用专门serializer。不要让用户locale改变wire value,也不要把secret写进diagnostic string。

public string ToString(string format, IFormatProvider provider)
{
    provider ??= CultureInfo.CurrentCulture;
    return format switch
    {
        "A" => Amount.ToString("0.00", provider),
        "C" or null => $"{Currency} {Amount.ToString("0.00", provider)}",
        _ => throw new FormatException($"Unknown format: {format}"),
    };
}

建议14:正确实现浅拷贝和深拷贝

shallow copy复制top-level fields,nested references继续共享;deep copy按ownership graph复制可变owned nodes。并非“越深越安全”:immutable values可共享,database connection/file handle不能随意clone,graph cycle与alias identity要有policy。优先copy constructor/factory表达允许复制的state,而不是通用serialization hack。

public sealed class OrderDraft
{
    public OrderDraft(OrderDraft source)
    {
        CustomerId = source.CustomerId;
        Lines = source.Lines.Select(line => new OrderLine(line)).ToList();
    }
 
    public string CustomerId { get; }
    public List<OrderLine> Lines { get; }
}

建议15:使用dynamic来简化反射实现

dynamic可让runtime binder处理member lookup/overload,减少显式MemberInfo/Invoke代码,适合COM、dynamic language或已验证adapter边界。它不等于更安全的reflection:typo/signature change延迟到runtime,AOT/trimming和performance也需考虑。已知contract优先interface/generic;reflection discovery后尽快转换为typed delegate/interface。

public static IReportAdapter Bind(object plugin)
{
    if (plugin is IReportAdapter typed) return typed;
    return new DynamicReportAdapter(plugin); // dynamic remains inside this adapter.
}
分步1 / 3

切换format、shallow/deep copy、dynamic与reflection

本章回顾:15条建议共同维护Value与Boundary Contract

  1. string/convert/parse选择取决于allocation evidence和failure是否属于normal path。
  2. nullable、const/readonly与enum必须定义absence、versioning和default bits。
  3. operators、comparison、Equals与GetHashCode围绕同一equality domain,并保持key immutable。
  4. formatting区分human/machine representation,copy区分owned identity,dynamic限制在adapter。
  5. 原书建议是审查问题的入口;最终结论要由当前target、domain contract与tests决定。

练习

问题 1:外部CSV中的年龄字段应使用Parse还是TryParse,null与0怎样区分?

问题 2:设计可作为Dictionary key的Money,需要哪些一致性证明?

问题 3:何时可用dynamic替代reflection,怎样限制风险?

术语表

名词解释

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

normal failure path
semantic absence
equality domain
representation contract
deferred binding boundary

讨论

评论区加载中…