Skip to content

第 10 章:处理复杂类型和边界情况

本章目标

  • 理解为什么完整 ToString() 比简单方法复杂很多
  • 认识 null、值类型、引用类型、泛型、嵌套类带来的影响
  • 看懂主案例中的边界处理策略

本章在主线里的位置

  • 这一章承担从 Level 1 走向完整实现前的过渡
  • 重点不是再看一遍固定输出,而是理解“属性驱动生成”进入真实场景后为什么会迅速变复杂

先看现象

如果你只是生成这样一个方法:

csharp
public string GetGeneratedTypeName() => "Person";

那几乎不会遇到复杂情况。

但一旦你要生成 ToString(),问题立刻就会变多:

  • 属性值如果是 null 怎么办?
  • 属性是值类型还是引用类型,会影响输出方式吗?
  • 如果类是泛型类,类声明怎么补回去?
  • 如果类是嵌套类,外层类型要不要一起生成?
  • 如果一个类没有任何属性,还要不要生成方法?

这也是为什么很多人觉得“看懂固定输出”和“写出真实生成器”之间像隔着一条河。

再看代码

主案例中,复杂度主要集中在两处:

  • TransformClass(...)
    • 负责收集泛型信息、约束、嵌套结构、属性类型信息
  • BuildToStringImplementation(...)
    • 负责根据属性类型决定不同的字符串插值策略

1. null 处理

主案例使用的典型策略是:

csharp
{Name?.ToString() ?? "null"}

这样做的目的很直接:

  • 如果属性是 null
  • 不让生成的方法崩掉
  • 而是稳定输出字符串 "null"

2. 值类型和引用类型的区别

值类型通常可以直接插值,比如:

csharp
{Age}

但引用类型和可空类型更需要保护,因为它们可能为 null

3. 泛型类的处理

如果类是:

csharp
public partial class Container<T> where T : class

那生成代码时不能只写:

csharp
public partial class Container

而是必须把:

  • 泛型参数
  • 泛型约束

一起补回类声明中。

主案例里,泛型约束不是“猜出来”的,而是从每个类型参数对象上逐项读取的。你在代码里会看到这一类成员:

  • typeParam.HasReferenceTypeConstraint
    • 代表有没有 class
  • typeParam.HasValueTypeConstraint
    • 代表有没有 struct
  • typeParam.HasNotNullConstraint
    • 代表有没有 notnull
  • typeParam.HasConstructorConstraint
    • 代表有没有 new()
  • typeParam.ConstraintTypes
    • 代表有没有基类或接口约束

可以把它粗略理解成:

csharp
foreach (var typeParam in symbol.TypeParameters)
{
    if (typeParam.HasReferenceTypeConstraint)
    {
        constraintClauses.Add("class");
    }

    if (typeParam.HasValueTypeConstraint)
    {
        constraintClauses.Add("struct");
    }

    foreach (var constraintType in typeParam.ConstraintTypes)
    {
        constraintClauses.Add(constraintType.ToDisplayString());
    }

    if (typeParam.HasNotNullConstraint)
    {
        constraintClauses.Add("notnull");
    }

    if (typeParam.HasConstructorConstraint)
    {
        constraintClauses.Add("new()");
    }
}

这段代码真正做的事只有一件:把 where T : ... 里原本写在源码上的约束,重新恢复到生成代码里。

4. 嵌套类的处理

如果目标类是:

csharp
public partial class Outer
{
    [GenerateToString]
    public partial class Inner
    {
    }
}

那生成代码时不能只生成 Inner,还要把外层结构一起补齐,否则生成代码就无法正确落位。

如何验证

按下面顺序做:

  1. 打开 sample/01-tostring-generator/ToStringGenerator.Sample/Examples/ComplexClass.cs
  2. 打开 GenericClass.cs
  3. 打开 NestedClass.cs
  4. 回到 TransformClass(...)BuildToStringImplementation(...)
  5. 分别找出:
    • null 处理逻辑
    • 泛型参数和约束处理逻辑
    • HasConstructorConstraintHasReferenceTypeConstraintConstraintTypes 这些成员各自对应哪类约束
    • 嵌套类处理逻辑
    • 无属性类处理逻辑

动手练习

  1. 找出值类型和可空值类型的判断条件分别是什么
  2. 找出泛型约束是在哪段代码里被收集的,并说出 HasConstructorConstraint 代表什么
  3. 找出嵌套类场景里文件名如何避免冲突

常见误解

  • 误解 1:完整生成器只是把简单方法多写几行而已
    • 不是,真正的复杂度来自“边界情况会不会把生成代码搞坏”
  • 误解 2:泛型和嵌套类只是少数情况,可以先忽略
    • 在真实项目里,这些情况很常见,忽略后生成器会很快失去实用性
  • 误解 3:null 处理只是美观问题
    • 不是,它直接影响生成代码是否稳定可运行

本章新名词

  • NullableAnnotation
  • OriginalDefinition
  • TypeParameters
  • ITypeParameterSymbol
  • ContainingType
  • 类型约束

本章小结

完整生成器真正变复杂的地方,不是“能不能生成代码”,而是“能不能稳定处理真实场景里的边界情况”。

下一章我们就把这些边界处理全部放回完整实现中,从上到下把最终的 ToString 生成器串起来。

上一章 | 返回主教程目录 | 下一章:完整 ToString 实现

基于当前仓库文档副本构建的 VitePress 站点