PAYATHON 2026

C# 重载后置自增时,为什么 ToString() 得到的是自增后的值?

数据小陈

结论

operator ++ 修改了原对象,并返回同一个对象。后置自增表达式保留的是旧值;这里的 class 是引用类型,旧值是对象引用,不是对象状态的副本。

因此,indent++ 返回的旧引用仍指向已修改的对象。字符串插值随后调用 ToString(),读到的是增加后的缩进。

如果要继续使用 $"{indent++}" 这样的写法,可以让 operator ++ 返回新实例,并保持原实例不变。如果只需要控制代码生成时的输出顺序,先输出、再增加缩进更直接。

为什么整数显示旧值,而缩进显示新值?

原代码中的运算符实现是:

static public Indentation operator ++(Indentation indentation) {
    indentation.Indent++;
    return indentation;
}

它先修改传入对象的 Indent,再返回该对象的引用。Indent 的 setter 会同时更新 indentStr,因此这时该对象的 ToString() 已经会返回新的缩进字符串。

对于示例中的局部变量,可以用下面的概念性展开来理解后置自增:

Indentation oldReference = indent;
Indentation nextReference = Indentation.operator ++(oldReference);
indent = nextReference;
// indent++ 的表达式结果为 oldReference。

这里的运算符调用只是示意,不能直接作为 C# 源代码使用。由于原来的运算符返回同一个实例,oldReference 和 indent 最终仍指向同一个已修改的对象。

int 的情况不同:

int oldValue = i;
i = i + 1;
// i++ 的表达式结果为 oldValue。

oldValue 保存了整数 4,后续修改 i 不会影响它。

i++ 打印旧值,并不表示打印发生在自增之前。自增在表达式求值时已经完成,表达式的结果仍是旧整数。对于引用类型,后置自增不会自动创建对象状态的快照,也不保证在执行重载运算符前调用 ToString()。

另外,按题目中 Size = 4 的默认值,第一次自增应增加四个空格;原始输出中展示的一个空格不影响上述原因分析。

方案一:重载运算符时返回新实例

如果希望保留 indent++ 的使用方式,可以将类中的两个运算符替换为:

public static Indentation operator ++(Indentation indentation)
{
    return new Indentation(indentation.Size)
    {
        Indent = indentation.Indent + 1
    };
}

public static Indentation operator --(Indentation indentation)
{
    return new Indentation(indentation.Size)
    {
        Indent = indentation.Indent - 1
    };
}

这两个方法都在 Indentation 类内部,因此可以访问其 Size 属性。

修改后再运行原来的代码:

int i = 4;
Indentation indent = new();

Debug.WriteLine($"{i}|{indent}|public class ... {{");
Debug.WriteLine($"{i++}|{indent++}|public class ... {{");

两条语句都会输出:

4||public class ... {

第二条语句执行后,i 为 5,变量 indent 指向 Indent 为 1 的新实例。字符串插值使用的是后置自增留下的旧实例,其缩进仍为零。

前置自增 ++indent 使用运算符返回的新实例,表达式得到的是增加后的缩进。这里的静态 operator ++ 由前置和后置两种形式共用,语言规则决定各自的表达式结果。

方案二:先输出,再修改缩进

如果不需要把状态更新写进插值表达式,可以保留原来的运算符实现,把操作分开:

int i = 4;
Indentation indent = new();

Debug.WriteLine($"{i}|{indent}|public class ... {{");
i++;
indent++;

这样,输出时使用当前状态,后续语句使用更新后的状态,执行顺序也容易检查。

如果需要在输出前更新缩进,可以先保存字符串快照:

string currentIndent = indent.ToString();
indent++;

Debug.WriteLine($"{i++}|{currentIndent}|public class ... {{");

currentIndent 保存的是更新前的字符串,之后修改 indent 不会改变它。

使用时需要注意什么?

返回新实例会改变共享引用时的行为。例如:

Indentation a = new();
Indentation b = a;
a++;

使用原实现时,a 和 b 仍指向同一个对象,看到的都是新的缩进。使用返回新实例的实现时,只有 a 被赋为新引用,b 仍指向旧实例。选择哪种方案,取决于调用代码是否依赖共享的可变状态。

另外,Indent 为零时执行 -- 会尝试创建长度为负数的字符串,从而抛出 ArgumentOutOfRangeException。是否禁止负缩进、在零处保持不变,或者给出明确异常,需要单独确定。

单纯写成 (indent++).ToString() 无法修复原实现,因为调用 ToString() 之前,operator ++ 已经修改了对象。

备注:内容仅供参考。