第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边界

机制总览

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

  1. 1

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

    原书第1章的15条建议来自C /.NET 4时代,核心问题仍成立:转换失败是不是正常输入,default value有没有domain meaning,相等对象能否稳定进入hash collection,复制后是否共享nested state,runtime binding是否值得失去compile…

  2. 2

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

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

  3. 3

    Nullable、readonly/const与enum(…

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

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

第1章:基本语言要素(建议1-15):机制与证据

切换《第1章:基本语言要素(建议1-15)》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 为什么“建议”必须还原成Decision Boundary

原书第1章的15条建议来自C /.NET 4时代,核心问题仍成立:转换失败是不是正常输入,default value有没有domain meaning,相等对象能否稳定进入hash collection,复制后是否共享nested state,runtime binding是否值得失去compile…

可核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么“建议”必须还原成Decision Boundary」的收益与反例。

学完《第1章:基本语言要素(建议1-15)》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

第1章:基本语言要素(建议1-15):失效与核验

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

典型失效

若把「为什么“建议”必须还原成Decision Boundary」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。

核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么“建议”必须还原成Decision Boundary」的收益与反例。

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

典型失效

若把「字符串、转换与解析(建议1-4)」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。

核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「字符串、转换与解析(建议1-4)」的收益与反例。

Nullable、readonly/const与enum(…

典型失效

若把「Nullable、readonly/const与enum(…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。

核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Nullable、readonly/const与enum(…」的收益与反例。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么“建议”必须还原成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

原版目录概念补充核对

以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。

建议2:使用默认转型方法:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议2:使用默认转型方法是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议3:区别对待强制转型与as和is:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议3:区别对待强制转型与as和is是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议4:TryParse比Parse好:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议4:TryParse比Parse好是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议5:使用int?来确保值类型也可以为null:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议5:使用int?来确保值类型也可以为null是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议6:区别readonly和const的使用方法:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议6:区别readonly和const的使用方法是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议7:将0值作为枚举的默认值:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议7:将0值作为枚举的默认值是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议8:避免给枚举类型的元素提供显式的值:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议8:避免给枚举类型的元素提供显式的值是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议9:习惯重载运算符:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议9:习惯重载运算符是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议10:创建对象时需要考虑是否实现比较器:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议10:创建对象时需要考虑是否实现比较器是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议11:区别对待==和Equals:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议11:区别对待==和Equals是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议12:重写Equals时也要重写GetHashCode:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议12:重写Equals时也要重写GetHashCode是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议13:为类型输出格式化字符串:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议13:为类型输出格式化字符串涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。

建议14:正确实现浅拷贝和深拷贝:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议14:正确实现浅拷贝和深拷贝是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议15:使用dynamic来简化反射实现:机制、边界与证据

  1. 基本语言要素(建议1-15)中的建议15:使用dynamic来简化反射实现是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

讨论

评论区加载中…