如何使用对象展开/基类继承简化含必填字段的对象构建?
我明白你现在的困扰——带required关键字的基类给子类实例化带来了一些约束,既要复用公共属性的初始化逻辑,又要根据条件创建不同子类,还要保留必填字段的校验。咱们一步步来解决这个问题。
首先分析你当前代码的核心问题:因为BaseFoo里的Name是required字段,C#会强制要求实例化时必须显式初始化这个属性。你原来的CreateRecord方法里用new T { Name = "" }其实是符合要求的,但如果直接用new T()(无参构造)就会编译报错,因为没有初始化必填的Name。另外,你用with表达式给Bar加Id的写法虽然可行,但可以更简洁。
下面给你几个优化方案,按需选择:
方案一:用配置委托简化子类属性初始化
这个方案能保留你原来的CreateRecord复用逻辑,同时支持在创建子类时直接配置额外属性,不用额外的with表达式:
private BaseFoo Blah() { // 直接在CreateRecord里配置Bar的Id,不用再链式调用with return 1 == 1 ? CreateRecord<Foo>() : CreateRecord<Bar>(bar => bar.Id = ""); } // 新增可选的configure委托,用来初始化子类的额外属性 private T CreateRecord<T>(Action<T>? configure = null) where T : BaseFoo, new() { var record = new T { Name = "" }; // 初始化基类的必填字段 configure?.Invoke(record); // 按需配置子类属性 return record; } public record BaseFoo { public required string Name { get; set; } } public record Foo : BaseFoo { } public record Bar : BaseFoo { public string Id { get; set; } }
这个写法既复用了Name的初始化逻辑,又能灵活给子类添加属性,代码更紧凑。
方案二:利用基类构造函数统一初始化必填字段
如果你的必填字段Name有固定的默认值,或者希望通过构造函数强制初始化,可以给基类添加构造函数,子类继承后直接调用:
// 基类添加带参数的构造函数,强制初始化必填的Name public record BaseFoo(string Name); // 子类继承基类构造函数,不用重复写Name的初始化逻辑 public record Foo(string name) : BaseFoo(name); public record Bar(string name, string id) : BaseFoo(name) { public string Id { get; set; } = id; } private BaseFoo Blah() { // 直接传入参数创建实例,逻辑清晰 return 1 == 1 ? CreateRecord<Foo>("") : CreateRecord<Bar>("", ""); } // 用Activator处理不同参数的构造函数(如果子类构造函数参数固定,也可以写重载方法) private T CreateRecord<T>(params object[] args) where T : BaseFoo { return (T)Activator.CreateInstance(typeof(T), args); }
这个方案的好处是通过构造函数强制保证必填字段被初始化,避免了对象初始化器的写法,更符合record的设计初衷。
方案三:给基类添加受保护的无参构造函数(适合固定默认值场景)
如果Name的默认值是固定的(比如空字符串),可以给基类加一个受保护的无参构造函数,直接初始化Name,这样子类就能直接无参实例化:
public record BaseFoo { public required string Name { get; set; } // 受保护的无参构造函数,初始化默认值 protected BaseFoo() => Name = ""; } // 子类直接继承,无需额外代码 public record Foo : BaseFoo { } public record Bar : BaseFoo { public string Id { get; set; } = ""; } private BaseFoo Blah() { // 直接new实例,最简洁 return 1 == 1 ? new Foo() : new Bar(); }
这个写法最简洁,但缺点是Name的默认值被硬编码在基类里,灵活性稍差,适合默认值固定的场景。
总结一下,如果你需要灵活配置子类属性,方案一最适合;如果希望通过构造函数强制定义初始化逻辑,选方案二;如果默认值固定,方案三最省事。
内容来源于stack exchange

