C#中初始化不可空类属性是否仅依赖构造函数?含C#8场景疑问
好问题!在启用了nullable引用类型的C#项目里,初始化不可空属性真的不止构造函数这一种路子,咱们根据你的场景和C#版本来逐一拆解:
先搞懂为什么编译器报错
你当前的Request类里,三个string属性都是非可空引用类型,但自动属性的默认初始化会把它们设为null——编译器没法保证你在使用这些属性前一定会赋值,所以才抛出警告/错误。
构造函数不是唯一方案,不同C#版本有不同解法
1. C# 11及以上:用required属性(最贴合你的需求)
这是最完美的解决方案!只要给不可空属性加上required修饰符,编译器就会强制要求你在对象初始化器中给所有required属性赋值,少一个都不行。
修改你的类:
public class Request { public required string Type { get; set; } public required string Username { get; set; } public required string Key { get; set; } }
这时候如果写:
var request = new Request { Type = "Not null", Username = "Not null" };
编译器会直接报错,提示你Key属性未被初始化,完全符合你的预期。而且不用写冗长的构造函数,20个属性的话也只需要给每个加个required就行。
2. C# 9及以上:记录类型(Record)
如果你的项目能升级到C#9,记录类型会自动帮你生成包含所有属性的构造函数,同时支持位置参数和对象初始化器语法,写法极简:
public record Request(string Type, string Username, string Key);
使用时既可以用位置参数:
var request = new Request("Not null", "Not null", "Not null");
也可以用对象初始化器:
var request = new Request { Type = "Not null", Username = "Not null", Key = "Not null" };
而且记录类型默认是不可变的(属性为init-only),如果需要可变的话,也可以显式定义为set。
3. C# 8:退而求其次的方案
因为C#8没有required和记录类型,编译时强制不可空属性初始化的话,构造函数确实是最可靠的方式。但20个属性写构造函数太麻烦?可以试试这两个思路:
- Builder模式:创建一个
RequestBuilder类,每个属性的设置方法返回自身,最后提供一个Build()方法,在方法内检查所有必填属性是否都已赋值,未赋值则抛出异常。比如:
使用时链式调用:public class RequestBuilder { private string? _type; private string? _username; private string? _key; public RequestBuilder WithType(string type) { _type = type; return this; } public RequestBuilder WithUsername(string username) { _username = username; return this; } public RequestBuilder WithKey(string key) { _key = key; return this; } public Request Build() { if (_type == null || _username == null || _key == null) { throw new InvalidOperationException("所有必填属性都必须赋值"); } return new Request { Type = _type, Username = _username, Key = _key }; } }
这是运行时检查,但能避免写长构造函数,代码可读性也不错。var request = new RequestBuilder() .WithType("Not null") .WithUsername("Not null") .WithKey("Not null") .Build(); - 代码分析规则:启用CA1062规则(验证公共方法的参数),配合nullable警告,能在编译时提醒你有没有遗漏赋值,但没法像
required那样强制拦截。
为什么属性初始化器不适合你的场景
你提到的属性初始化器(比如public string Type { get; set; } = "default";)确实只适合那些有合理默认值的属性,像Type这种不能随便赋初始值的场景,强行加默认值反而会引入潜在的bug,所以这个方案确实不适用。
内容的提问来源于stack exchange,提问作者Ilya Chernomordik

