如何初始化不可为 null 的泛型类型 属性(或字段)(c# 8 或 9)

How to initialize a non-nullable generic type property (or field) (c# 8 or 9)

我正在将一些旧代码转换为启用可为空的 C# 8 和 9。我 运行 在泛型方面遇到了一些麻烦。这是一个例子。旧的 class 有一个不受约束的类型参数:

public class WeightedValue1<T>
{
    public double Weight { get; set; }
    public T Value { get; set; }
}

这给出了 CS8618:不可为空 属性 'Value' 退出构造函数时必须包含非空值。

太好了;它准确地防止了该功能旨在捕获的那种错误 - 用户取消引用 .Value 并获得 NullReferenceException。问题是很难正确初始化 属性 。在旧世界中它将是:

public class WeightedValue2<T>
{
    public double Weight { get; set; }
    public T Value { get; set; } = default;
}

这给出了 CS8601 可能的空引用赋值。也应该如此!我还没有真正解决问题。 即使 T 是 non-nullable string ,调用者也会得到 NRE no warning:

[TestClass]
public class NreTest2
{
    [TestMethod]
    public void NonNullableString_ThrowsNre()
    {
        Assert.ThrowsException<NullReferenceException>(() => new WeightedValue2<string>().Value.ToLower());
    }
}

为了完整起见,请注意指定 notnull 约束不会改变任何内容。

public class WeightedValue3<T>
    where T : notnull
{
    public double Weight { get; set; }
    public T Value { get; set; } = default;
}

[TestClass]
public class NreTest3
{
    [TestMethod]
    public void ConstrainedString_ThrowsNre()
    {
        Assert.ThrowsException<NullReferenceException>(() => new WeightedValue3<string>().Value.ToLower());
    }
}

当然,使用 null-forgiving 运算符 (!) 会隐藏警告,但对调用者根本没有帮助。

public class WeightedValue4<T>
    where T : notnull
{
    public double Weight { get; set; }
    public T Value { get; set; } = default!;
}
[TestClass]
public class NreTest4
{
    [TestMethod]
    public void ForgivenString_ThrowsNre()
    {
        Assert.ThrowsException<NullReferenceException>(()=> new WeightedValue4<string>().Value.ToLower());
    } 
}

是的,所以问题实际上是字符串的默认值为空,无论我们是否允许它。我们告诉编译器的任何事情都不会让它改变这一点。所以我们必须用一些东西来初始化它。由于它是一个通用的无约束 T,我们找不到任何有效值。我们必须 将其推送给调用者。我现有的所有调用方都将使用对象初始化程序语法,因此新的 c#9“init”属性似乎非常适合。

public class WeightedValue5<T>
{
    public double Weight { get; set; }
    public T Value { get; init; }
}

唉。不幸的是 init 没有做我想做的。这意味着您不能在对象创建后调用 setter,但它不会强制您实际初始化 属性。这是双重令人沮丧的,因为编译器 知道 它需要初始化。

[TestClass]
public class NreTest5
{
    [TestMethod]
    public void InitOnlySetter_Compiles_AndThrowsNre()
    {
        Assert.ThrowsException<NullReferenceException>(() => new WeightedValue5<string>().Value.ToLower());
    }
}

这是一个至少可以警告调用者有关可空性的版本。但它需要更改每个现有的呼叫站点。当您开始有很多属性和多个构造函数要初始化时,它会遇到常见的烦恼。

public class WeightedValue6<T>
{
    public double Weight { get; set; }
    public T Value { get; }

    public WeightedValue6(T value)
    {
        Value = value;
    }
}

我也探索了属性注释,但我看不出它们有什么帮助。据我所知,尽管新记录类型的目标是减少简单引用类型的样板文件,但它们也存在同样的问题。

我疯了吗?我可以用绝对非空值初始化通用字段的唯一方法是让每个调用者手动指定它们,这真的是真的吗?强制执行此操作的唯一方法是使用全脂构造函数吗? (当然还有将特定警告设置为错误。)

如果是这样,希望编译器用init重写我的版本是不是错了; 好像它有完整的构造函数?

您提到记录无法解决问题,但我认为他们为此减少了很多样板文件。

public record WeightedValue<T>(double Weight, T Value);

如果您将 null 传递给值,编译器将抱怨可空引用类型开启。还有 with 语法确实给你一点 init 的感觉,但你需要有一个要从中复制的值。

var weightedValue = new WeightedValue<string>(0, "Hello"); // fine
weightedValue = new WeightedValue<string>(0, null); // compiler warning
weightedValue = weightedValue with { Value = null }; // compiler warning

我也喜欢对象初始化,但命名参数看起来很不错。它们唯一不起作用的地方是表达式树(这很烦人)。

var weightedValue = new WeightedValue<string>(
    Weight: 0, 
    Value: "Hello"
);

另一个使用 with 语法的潜在选项是创建一个默认值来初始化您的类型,然后调用它并更改您对对象感兴趣的参数。

public static WeightedValue<string> defaultWeightedString = new(0, "");

//...

var weightedValue = defaultWeightedString with { Weight = 10 }; // string Value initialized

我看到这里有很多潜力。但是,如果我们有某种 { get; init required; } 来强制在实例化时进行初始化,那就太好了。

更新 好像有这个提议

https://github.com/dotnet/csharplang/issues/3630

https://github.com/dotnet/csharplang/discussions/4209