为啥从 <T> 到 <U> 的隐式转换运算符接受 <T?>?

Posted

技术标签:

【中文标题】为啥从 <T> 到 <U> 的隐式转换运算符接受 <T?>?【英文标题】:Why does an implicit conversion operator from <T> to <U> accept <T?>?为什么从 <T> 到 <U> 的隐式转换运算符接受 <T?>? 【发布时间】:2018-10-27 12:27:39 【问题描述】:

这是一种我无法理解的奇怪行为。在我的示例中,我有一个类Sample&lt;T&gt; 和一个从TSample&lt;T&gt; 的隐式转换运算符。

private class Sample<T>

   public readonly T Value;

   public Sample(T value)
   
      Value = value;
   

   public static implicit operator Sample<T>(T value) => new Sample<T>(value);

当为T 使用可空值类型(例如int?)时会出现问题。


   int? a = 3;
   Sample<int> sampleA = a;

这里是关键部分: 在我看来,这不应该编译,因为Sample&lt;int&gt; 定义了从intSample&lt;int&gt; 的转换,但不是从int?Sample&lt;int&gt;但它编译并运行成功!(我的意思是调用转换运算符并将3 分配给readonly 字段。)

而且情况变得更糟。这里没有调用转换运算符,sampleB 将被设置为null


   int? b = null;
   Sample<int> sampleB = b;

一个很好的答案可能会分为两部分:

    为什么第一个sn-p中的代码会编译? 我可以阻止代码在这种情况下编译吗?

【问题讨论】:

Here there is the documentation about C# conversions... 但我找不到哪个项目符号点在这里发生的事情。 【参考方案1】:

你可以看看编译器是如何降低这段代码的:

int? a = 3;
Sample<int> sampleA = a;

转入this:

int? nullable = 3;
int? nullable2 = nullable;
Sample<int> sample = nullable2.HasValue ? ((Sample<int>)nullable2.GetValueOrDefault()) : null;

因为Sample&lt;int&gt; 是一个类,它的实例可以被分配一个空值,并且使用这样一个隐式运算符,一个可以为空的对象的基础类型也可以被分配。所以像这样的分配是有效的:

int? a = 3;
int? b = null;
Sample<int> sampleA = a; 
Sample<int> sampleB = b;

如果Sample&lt;int&gt;struct,那当然会报错。

编辑: 那么为什么这可能呢?我在规范中找不到它,因为它是故意违反规范的,这只是为了向后兼容。你可以在code阅读它:

故意违反规范: 本机编译器允许“解除”转换,即使转换的返回类型不是不可为空的值类型。例如,如果我们有一个从 struct S 到 string 的转换,那么从 S 的“提升”转换? to string 被原生编译器认为存在,语义为“s.HasValue ? (string)s.Value : (string)null”。为了向后兼容,Roslyn 编译器会延续此错误。

这就是 Roslyn 中的 implemented 的“错误”:

否则,如果转换的返回类型是可为空的值类型、引用类型或指针类型P,那么我们将其降低为:

temp = operand
temp.HasValue ? op_Whatever(temp.GetValueOrDefault()) : default(P)

因此,根据spec,对于给定的用户定义转换运算符T -&gt; U,存在一个提升运算符T? -&gt; U?,其中TU 是不可为空的值类型。但是,由于上述原因,U 是引用类型的转换运算符也实现了这种逻辑。

PART 2 在这种情况下如何防止代码编译?嗯,有办法。您可以专门为可空类型定义一个额外的隐式运算符,并用属性Obsolete 装饰它。这需要将类型参数T 限制为struct

public class Sample<T> where T : struct

    ...

    [Obsolete("Some error message", error: true)]
    public static implicit operator Sample<T>(T? value) => throw new NotImplementedException();

该运算符将被选为可空类型的第一个转换运算符,因为它更具体。

如果你不能做出这样的限制,你必须为每个值类型分别定义每个运算符(如果你真的确定你可以利用反射和使用模板生成代码):

[Obsolete("Some error message", error: true)]
public static implicit operator Sample<T>(int? value) => throw new NotImplementedException();

如果在代码中的任何地方引用都会出错:

错误 CS0619 'Sample.implicit operator Sample(int?)' 已过时:'一些错误消息'

【讨论】:

你应该加粗最后一句(If... struct)...这是对“问题2”的回应 我的 5 美分在这里。用表达式尝试上面的例子,你会得到“System.InvalidOperationException: Nullable object must have a value”。基本上在普通代码中,c# 编译器会进行提升转换,但对于表达式树,它会引发异常。【参考方案2】:

我认为它是提升转换运算符的作用。规范说:

给定一个用户定义的转换运算符,该运算符从 非可空值类型 S 到不可空值类型 T,提升 存在从 S 转换的转换运算符?到T?。这解除了 转换运算符从 S?到 S 然后 用户定义的从 S 到 T 的转换,然后是 T 的换行 到 T?,除了一个空值 S?直接转换为空值 T?。

这里好像不适用,因为虽然S 类型在这里是值类型(int),但T 类型不是值类型(Sample 类)。然而,Roslyn 存储库中的 this issue 声明它实际上是规范中的错误。 Roslyn code 文档证实了这一点:

如上所述,这里我们与规范不同,分为两个 方法。首先,如果正常形式是,我们只检查提升形式 不适用。其次,我们应该只应用提升语义 如果转换参数和返回类型不可为空 值类型。

实际上本地编译器决定是否检查一个提升 形式基于:

我们最终要从可空值类型转换的类型吗? 转换的参数类型是不可为空的值类型吗? 我们最终要转换为可空值类型、指针类型还是引用类型的类型?

如果所有这些问题的答案都是“是”,那么我们提升为 nullable 并查看结果运算符是否适用。

如果编译器遵循规范 - 在这种情况下它会如您预期的那样产生错误(在某些旧版本中确实如此),但现在不会。

总结一下:我认为编译器使用了隐式运算符的提升形式,根据规范这应该是不可能的,但是编译器在这里与规范不同,因为:

它被认为是规范中的错误,而不是编译器中的错误。 旧的、pre-roslyn 编译器已经违反了规范,保持向后兼容性很好。

正如第一个引用中描述的提升运算符如何工作的描述(此外,我们允许 T 成为引用类型) - 您可能会注意到它准确描述了您的案例中发生的情况。 nullS (int?) 直接分配给 T (Sample) 没有转换运算符,非 null 被解包到 int 并通过你的运算符运行(包装到 T? 是如果T 是引用类型,显然不需要)。

【讨论】:

这解释了为什么我无法从规范中推断出发生了什么 :-) Comment about this on github: 这是否意味着关于上面的 sn-p (来自不可为空的值类型 S 到引用类型 T) 旧编译器的行为 S? -> T(或 S?-> S -> T)实际上是未定义的行为? 和响应:@yaakov-h 不,它不是未定义的。它被很好地定义为需要编译时错误。我们将更改语言规范和 Roslyn 编译器以使其行为与以前一样。 我们能否将您的答案总结如下?:Roslyn 文档有意偏离 C# 规范。而这反过来(可能)会导致不良行为。我们不能指望这个问题会得到解决,因为这个决定是故意的。 @NoelWidmer 基本上是的,尽管如链接问题中所述 - 它被认为是规范中的错误(“当然这是规范中的错误”),因此唯一必要的修复是修复在规范中,而不是在编译器中。【参考方案3】:

为什么第一个sn-p中的代码会编译?

来自Nullable&lt;T&gt; 源代码的代码示例,可以在here 找到:

[System.Runtime.Versioning.NonVersionable]
public static explicit operator T(Nullable<T> value) 
    return value.Value;


[System.Runtime.Versioning.NonVersionable]
public T GetValueOrDefault(T defaultValue) 
    return hasValue ? value : defaultValue;

结构Nullable&lt;int&gt; 具有重写的显式运算符以及方法GetValueOrDefault 编译器使用这两者之一将int? 转换为T

之后它运行implicit operator Sample&lt;T&gt;(T value)

大概的情况是这样的:

Sample<int> sampleA = (Sample<int>)(int)a;

如果我们在Sample&lt;T&gt; 隐式运算符中打印typeof(T),它将显示:System.Int32

在您的第二种情况下,编译器不使用implicit operator Sample&lt;T&gt;,而是简单地将null 分配给sampleB

【讨论】:

结构 Nullable 有一个重写的隐式运算符隐式转换 int?解释什么? int a = (int?)5 不起作用。 那叫explicit,不是implicit 在编译的 IL 中没有这种隐式转换的迹象。只是针对控制分支行为的System.Nullable&lt;System.Int32&gt;.get_HasValue 的测试。见gist.github.com/biggyspender/653b1be91e0571613377191b6e9f6366 ...这意味着编译器对 nullables 有特殊处理,并且此行为 notNullable&lt;T&gt; 中作为隐式运算符实现 @spender Afaik 可空值类型对编译器确实具有特殊意义。由于设计者试图混合null 和值类型,他们可能有一些没有很好讨论的极端情况(或者找不到好的解决方案),而最终导致这种“特性”隐含地导致错误。我不认为这是期望的行为,但可能是他们试图解决的某些类型系统问题的结果。

以上是关于为啥从 <T> 到 <U> 的隐式转换运算符接受 <T?>?的主要内容,如果未能解决你的问题,请参考以下文章

为啥对 List<T> 的迭代没有从 SQL 视图中给我正确的值? [复制]

为啥要在 C# 中使用 Task<T> 而不是 ValueTask<T>?

为啥 WCF 会返回 myObject[] 而不是 List<T> 像我期望的那样?

为啥 `IList<T>` 不从 `IReadOnlyList<T>` 继承?

为啥 Kotlin Array<T> 不实现 Iterable<T>

为啥 Stack<T> 和 Queue<T> 用数组实现?