Appearance
第 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,还要把外层结构一起补齐,否则生成代码就无法正确落位。
如何验证
按下面顺序做:
- 打开
sample/01-tostring-generator/ToStringGenerator.Sample/Examples/ComplexClass.cs - 打开
GenericClass.cs - 打开
NestedClass.cs - 回到
TransformClass(...)和BuildToStringImplementation(...) - 分别找出:
- null 处理逻辑
- 泛型参数和约束处理逻辑
HasConstructorConstraint、HasReferenceTypeConstraint、ConstraintTypes这些成员各自对应哪类约束- 嵌套类处理逻辑
- 无属性类处理逻辑
动手练习
- 找出值类型和可空值类型的判断条件分别是什么
- 找出泛型约束是在哪段代码里被收集的,并说出
HasConstructorConstraint代表什么 - 找出嵌套类场景里文件名如何避免冲突
常见误解
- 误解 1:完整生成器只是把简单方法多写几行而已
- 不是,真正的复杂度来自“边界情况会不会把生成代码搞坏”
- 误解 2:泛型和嵌套类只是少数情况,可以先忽略
- 在真实项目里,这些情况很常见,忽略后生成器会很快失去实用性
- 误解 3:null 处理只是美观问题
- 不是,它直接影响生成代码是否稳定可运行
本章新名词
NullableAnnotationOriginalDefinitionTypeParametersITypeParameterSymbolContainingType- 类型约束
本章小结
完整生成器真正变复杂的地方,不是“能不能生成代码”,而是“能不能稳定处理真实场景里的边界情况”。
下一章我们就把这些边界处理全部放回完整实现中,从上到下把最终的 ToString 生成器串起来。