【问题标题】:Generics and Nullable (class vs struct)泛型和 Nullable(类与结构)
【发布时间】:2019-10-10 07:34:29
【问题描述】:

编辑: 在给我模式等选项之前,请参阅下面关于我在 C#8.0 中对 Null 的立场的更新


原始问题

我正在尝试将我的基础库升级为“启用空值”,也就是使用 C# 8.0 <Nullable>enable</Nullable> 标志。

在尝试使用抽象和特定泛型时,我遇到了一些问题。考虑以下代码 sn-p,它接收 Action 并将其转换为 Func<TResult>。这是pre-nullable-enable

public static Func<TResult> ToFunc<TResult>(this Action action)
        => () => { action(); return default; }
;

但是post-nullable-enable我似乎很挣扎,因为我不能让 TResult 可以为空(或TResult?),因为这需要where TResult: classwhere TResult: struct 的约束。我无法将这两个约束结合起来让编译器知道 TResult 可以是类或值类型。目前我觉得这很烦人 - 因为应该能够表达无论是类还是结构,它都可以为空(无论以前的 .NET 继承设计如何)。

所以,post-nullable-enable我似乎只有一个选择,那就是代码重复,示例如下:

public static Func<TResult?> ToFuncClass<TResult>(this Action action)
        where TResult : class
        => () => { action(); return null; }
;

public static Func<TResult?> ToFuncStruct<TResult>(this Action action)
        where TResult : struct
        => () => { action(); return null; }
;

代码重复以及随之而来的命名方案都让我很困扰。我可能误解了正确的用法,或者我错过了规范的另一个功能,但是您将如何解决这个问题?


更新:事实上,我认为我宁愿坚持自己的“null-handling”实现,也不愿使用 C#8.0 的可为空功能。作为“Void” 对象或充实的“Option/Maybe/None”-解决方案 似乎可以更好地传达事物。我唯一担心的是它在推动语言发展培训新的编码员和引入另一个方面并不是很有帮助 >第三方/非本地解决方案到我们都有处理空通用问题。 因为 自己的 null 处理实现 很棒,但是在每个代码库中不同,需要由社区维护,并且您有各种不同的风格。因此,如果语言完全执行它,并且如果 标准 上升,那将是非常有帮助和巨大的好处。我希望这是 - 显然不是,我理解。但我觉得这是一个错失的机会。

谢谢 伊夫

【问题讨论】:

  • 没有重复。每种情况下的类型完全不同,这就是为什么这不被视为方法重载的原因。
  • 编译器必须为可空结构和可空引用类型发出非常不同的代码。这就是为什么你不能表达“可以为空,不管是什么风格”
  • @Damien_The_Unbeliever 非常清楚我理解这种行为,但是在代码库中看到这一点相当笨拙,你曾经有效地用一种方法为每个案例并完成它。想象一下这对每个人来说都是通用方法,我觉得这被忽略了——即使底层类型(值/引用)不同,我想表达的行为在两种情况下显然是相同的。现在,当在一个类型上调用该方法时,我不能只调用 ToFunc,而是必须检查我将返回的结果类型(甚至在这里没有意义),然后选择 ToFuncX 或 To FuncY。
  • @YvesSchelpe 您想解决的真正问题是什么?函数式语言不会从方法返回 null,它们返回 unitvoid 类型。你也可以这样做。System.Threading.Channels 包含一个 VoidResult 类型,因为这个原因。
  • 编译器可以完成这项工作,只要付出足够的努力。传统上,C# 的设计方式是使用一个特性所获得的成本是预先确定的(因此没有编译器魔法会产生大量开销),但该原则早已采用迭代器方法之类的方式,异步状态机和闭包和 lambda 的整个机器。更相关的原因是“没有办法在不破坏现有代码的情况下实现这一点”。如果在此功能之前没有Nullable&lt;T&gt;,它本来可以完成的,但是现在已经是这样了,没有骰子。

标签: c# .net-core nullable .net-core-3.0 c#-8.0


【解决方案1】:

原因在Try out Nullable Reference Types 部分The issue with T? 中解释。基本上,没有办法解决这个问题。

首先,当我们使用T? 时,T 是什么?它是可空的还是不可空的?

T 的自然定义?意思是“任何可以为空的类型”。但是,这意味着 T 意味着“任何不可为空的类型”,这是不正确的!现在可以将 T 替换为可为空的值类型(例如 bool?)。

其次,每种情况下使用的类型都不同 - string? 仍然是 string,而 int?Nullable&lt;int&gt;。每种情况下生成的具体方法是完全不同的。在一种情况下,您会得到一个

Func<string> ToFuncClass<string>(this Action action)

另一方面,你得到一个

Func<Nullable<int>> ToFuncStruct<int>(this Action)

接下来,需要注意的是,可空引用类型与可空值类型不同。可空值类型映射到 .NET 中的具体类类型。所以int?实际上是可空的。但是对于 string?,它实际上是相同的字符串,但带有一个编译器生成的属性对其进行注释。这样做是为了向后兼容。换句话说,字符串?是一种“假类型”,而 int?不是。

文章的例子证明了这一点:

可空值类型和可空引用类型之间的区别体现在如下模式中:

void M<T>(T? t) where T: notnull

这意味着参数是 T 的可空版本,并且 T 被限制为非空。

如果 T 是一个字符串,那么 M 的实际签名将是:

M<string>([NullableAttribute] T t)

但如果 T 是一个 int,那么 M 就是

M<int>(Nullable<int> t)

这两个签名本质上是不同的,而且这种差异是不可调和的。

【讨论】:

  • 感谢清楚的解释,但这是否证明他们不应该引入可空值。 这样做是为了向后兼容意味着我们被困在一个功能上.我宁愿为它重新设计系统。恐怕我会坚持我的旧空模式,因为我现在根本无法使用它。如果需要向后兼容,我理解语义差异和需要它 - 但我担心它是无用的。
  • @YvesSchelpe 我在使用 C#7 时遇到了完全相同的问题。我的签名是public static T GetOrCreate&lt;T&gt;(ref T value, object syncRoot, Func&lt;T&gt; factory) where T : classpublic static T GetOrCreate&lt;T&gt;(ref T? value, object syncRoot, Func&lt;T&gt; factory) where T : struct。两种方法都包含大约 7 行代码,彼此之间完全相同。这是 C#7,我希望 C#8 能给我解决这个问题的可能性。
  • @YvesSchelpe:它确实解决了大问题:NullReferenceException,因为许多程序员没有检查空值,根本不期望它会在参数中到达。这是可空引用的主要原因。其次,你不需要两个名字,只需要一个。代码重复,当然,但付出的代价并不大(考虑到这两种类型在某种程度上是不相关的)。我希望Nullable&lt;T&gt; 能够成为可以接受类和结构的语言/CIL 结构,但现在做这样的事情还为时过早。
  • @firda 我的重载包含T 作为参数。参数包含在方法签名中。 Yves T 仅包含在返回类型中。返回类型不属于方法签名。这就是为什么他需要两个不同的方法名。
  • @PanagiotisKanavos 我同意,但情况是该功能可能比解决它的问题更有害。我认为人们需要意识到这一点。如果 75% 的官方文档是关于退出策略(null-forgiving operator,无数指令等) - 我认为应该意识到这一点。很抱歉讨论它。以防万一,您的答案是正确的,唉,我不喜欢这种哲学,但从技术上讲,它就是现在的样子。很遗憾。
猜你喜欢
  • 2023-03-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多