为啥从 <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<T>
和一个从T
到Sample<T>
的隐式转换运算符。
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<int>
定义了从int
到Sample<int>
的转换,但不是从int?
到Sample<int>
。 但它编译并运行成功!(我的意思是调用转换运算符并将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<int>
是一个类,它的实例可以被分配一个空值,并且使用这样一个隐式运算符,一个可以为空的对象的基础类型也可以被分配。所以像这样的分配是有效的:
int? a = 3;
int? b = null;
Sample<int> sampleA = a;
Sample<int> sampleB = b;
如果Sample<int>
是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 -> U
,存在一个提升运算符T? -> U?
,其中T
和U
是不可为空的值类型。但是,由于上述原因,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
成为引用类型) - 您可能会注意到它准确描述了您的案例中发生的情况。 null
值 S
(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<T>
源代码的代码示例,可以在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<int>
具有重写的显式运算符以及方法GetValueOrDefault
编译器使用这两者之一将int?
转换为T
。
之后它运行implicit operator Sample<T>(T value)
。
大概的情况是这样的:
Sample<int> sampleA = (Sample<int>)(int)a;
如果我们在Sample<T>
隐式运算符中打印typeof(T)
,它将显示:System.Int32
。
在您的第二种情况下,编译器不使用implicit operator Sample<T>
,而是简单地将null
分配给sampleB
。
【讨论】:
结构 Nullableint a = (int?)5
不起作用。
那叫explicit,不是implicit。
在编译的 IL 中没有这种隐式转换的迹象。只是针对控制分支行为的System.Nullable<System.Int32>.get_HasValue
的测试。见gist.github.com/biggyspender/653b1be91e0571613377191b6e9f6366
...这意味着编译器对 nullables 有特殊处理,并且此行为 not 在 Nullable<T>
中作为隐式运算符实现
@spender Afaik 可空值类型对编译器确实具有特殊意义。由于设计者试图混合null
和值类型,他们可能有一些没有很好讨论的极端情况(或者找不到好的解决方案),而最终导致这种“特性”隐含地导致错误。我不认为这是期望的行为,但可能是他们试图解决的某些类型系统问题的结果。以上是关于为啥从 <T> 到 <U> 的隐式转换运算符接受 <T?>?的主要内容,如果未能解决你的问题,请参考以下文章
为啥对 List<T> 的迭代没有从 SQL 视图中给我正确的值? [复制]
为啥要在 C# 中使用 Task<T> 而不是 ValueTask<T>?
为啥 WCF 会返回 myObject[] 而不是 List<T> 像我期望的那样?
为啥 `IList<T>` 不从 `IReadOnlyList<T>` 继承?