You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对C# 9中用init-only setters初始化的不可变对象做整体验证?

如何用C# 9的init-only Setters验证不可变对象状态

确实,C# 9里用init属性创建不可变对象比传统构造函数要优雅太多,但正如你提到的——这种方式没法像构造函数那样直接做整体状态验证,甚至还可能不小心new出未初始化的无效实例。下面给你几个实用的解决方案,覆盖不同场景:

1. 隐藏无参构造函数,用工厂方法强制验证

这是最稳妥的方案,既能保留对象初始化器的简洁,又能确保所有实例都经过严格验证。

首先把类的构造函数设为private,断绝对外直接创建空实例的可能,然后提供一个静态工厂方法,在方法里完成初始化和整体验证:

class Immutable {
    // 把无参构造函数藏起来,防止直接new空对象
    private Immutable() {}

    public string Name { get; init; }
    public int Value { get; init; }

    public static Immutable Create(Action<Immutable> initializer) {
        var instance = new Immutable();
        initializer(instance);
        
        // 在这里做整体状态验证,想加什么规则都可以
        if (string.IsNullOrEmpty(instance.Name)) {
            throw new InvalidOperationException("Name不能为空");
        }
        if (instance.Value <= 0) {
            throw new InvalidOperationException("Value必须大于0");
        }

        return instance;
    }
}

// 使用起来和对象初始化器几乎一样
var validObj = Immutable.Create(o => {
    o.Name = "ValidName";
    o.Value = 100;
});

// 下面这种写法会直接抛出异常,根本得不到无效实例
// var invalidObj = Immutable.Create(o => {
//     o.Value = -5; // Name没赋值,触发验证报错
// });

这种方式从根源上杜绝了无效实例的产生,所有对象在返回给调用者前都经过了验证,完全符合不可变对象的设计原则。

2. 单个属性init时验证 + 后置整体验证方法

如果不想完全放弃直接new的灵活性,可以先在每个init setter里做单个属性的合法性检查,再提供一个Validate方法来处理属性间的关联约束。不过这种方式需要调用者记得主动验证,不够强制:

class Immutable {
    private string _name;
    public string Name { 
        get => _name; 
        init {
            if (string.IsNullOrEmpty(value)) {
                throw new ArgumentException("Name不能为空");
            }
            _name = value;
        }
    }

    private int _value;
    public int Value { 
        get => _value; 
        init {
            if (value <= 0) {
                throw new ArgumentOutOfRangeException(nameof(Value), "Value必须大于0");
            }
            _value = value;
        }
    }

    public void Validate() {
        // 这里处理属性组合的约束,比如Admin用户的Value必须达标
        if (Name == "Admin" && Value < 1000) {
            throw new InvalidOperationException("Admin用户的Value必须≥1000");
        }
    }
}

// 使用示例
var obj = new Immutable { Name = "Admin", Value = 500 };
obj.Validate(); // 这里会直接抛出异常,拦截无效状态

这种方式能提前拦截单个属性的错误,整体验证可以处理更复杂的关联规则,但缺点是如果调用者忘记调用Validate,还是会有漏网之鱼。

3. 结合记录类型的构造函数验证 + With语法

如果你已经在使用C# 9的记录类型(Record),可以结合记录的主构造函数验证和with语法,兼顾不可变性和验证需求:

public record Immutable(string Name, int Value) {
    // 主构造函数里直接做初始化验证
    public Immutable {
        if (string.IsNullOrEmpty(Name)) {
            throw new ArgumentException("Name不能为空");
        }
        if (Value <= 0) {
            throw new ArgumentOutOfRangeException(nameof(Value), "Value必须大于0");
        }
    }

    // 封装一个带验证的With方法,避免直接用with语法跳过验证
    public Immutable WithValidatedValue(int newValue) {
        if (newValue <= 0) {
            throw new ArgumentOutOfRangeException(nameof(newValue), "Value必须大于0");
        }
        return this with { Value = newValue };
    }
}

// 创建实例时自动触发验证
var original = new Immutable("Test", 200);
// 用封装的方法创建验证后的副本
var updated = original.WithValidatedValue(300);

记录类型的主构造函数会在创建实例时自动执行验证,with语法依然可以用,但要注意直接用with不会触发构造函数,所以如果副本需要验证,最好封装成专门的方法。

总结

  • 要绝对避免无效实例:选第一种工厂方法,从根源上控制对象创建流程;
  • 要兼顾灵活性和部分验证:第二种单个属性验证加后置方法适合;
  • 已经在用记录类型:第三种方式最贴合记录的设计思路。

内容的提问来源于stack exchange,提问作者Jacek Ostrowski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:20:10