【问题标题】:How to initialize a non-nullable generic type property (or field) (c# 8 or 9) [closed]如何初始化不可为空的泛型类型属性(或字段)(c#8 或 9)[关闭]
【发布时间】:2020-12-16 01:25:08
【问题描述】:

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

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

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

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

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

这为 CS8601 提供了可能的空引用分配。也应该!我还没有真正解决问题。 即使 T 是 non-nullable 字符串,调用者也会获得带有 no warning 的 NRE:

[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;
    }
}

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

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

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

【问题讨论】:

  • 我不明白你在问什么。您是否希望该属性可以为空。 似乎,从上面看,您希望它不可为空。但在这种情况下,当然需要一种机制来在构造时使用非空值初始化对象。还能怎么办?那有什么问题呢?是的,有时不得不将不安全的代码移植到更安全的范例中,这很烦人。所以?我看不出真正的 problem 是什么。有很多杂乱无章的内容,但没有任何实际解决方案可以回答这里应该存在的任何问题的迹象。
  • 我喜欢这个问题++;当你走不可为空的路线时,它突出了思维方式和设计的变化。我同意@Peter的观点;似乎您正在尝试按照您不愿意执行的标准进行编码。
  • 我想强制执行可空性检查。我很早就希望使用该功能,并且发现它在很多情况下都很有用。我很惊讶泛型与我发现的一样难以使用,并希望我错过了一种可以使代码比我编写的更清晰的工具或技术。
  • 恕我直言,这些功能没有得到足够的指导,但考虑到它在游戏中引入的时间太晚了,强制实施需要Option&lt;T&gt; 的实现是不合理的。但即便如此,尝试从null 值中获取Option&lt;T&gt; 将是Exceptional,因为它被视为开发人员错误。
  • 记录类型大大减少了锅炉并迫使您提供值public record WeightedValue&lt;T&gt;(double Weight, T Value);

标签: c# generics c#-8.0 nullable-reference-types c#-9.0


【解决方案1】:

您提到记录无法解决问题,但我认为它们减少了相当多的样板。

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

如果您将 null 传递给 value,编译器将抱怨可空引用类型打开。还有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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-06
    • 1970-01-01
    相关资源
    最近更新 更多