如何对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
相关产品推荐
相关产品推荐

