【发布时间】: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<T>的实现是不合理的。但即便如此,尝试从null值中获取Option<T>将是Exceptional,因为它被视为开发人员错误。 -
记录类型大大减少了锅炉并迫使您提供值
public record WeightedValue<T>(double Weight, T Value);
标签: c# generics c#-8.0 nullable-reference-types c#-9.0