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

Noe*_*mer 52 c# generics nullable value-type implicit-conversion

这是一种我无法理解的怪异行为.在我的例子我有一个类Sample<T>和隐式转换操作符TSample<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);
}
Run Code Online (Sandbox Code Playgroud)

使用空值类型为出现问题时,T例如int?.

{
   int? a = 3;
   Sample<int> sampleA = a;
}
Run Code Online (Sandbox Code Playgroud)

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

它变得更糟.这里不调用转换运算符,sampleB将其设置为null:

{
   int? b = null;
   Sample<int> sampleB = b;
}
Run Code Online (Sandbox Code Playgroud)

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

  1. 为什么第一个代码段中的代码会编译?
  2. 我可以阻止在这种情况下编译代码吗?

are*_*yla 42

您可以看看编译器如何降低此代码:

int? a = 3;
Sample<int> sampleA = a;
Run Code Online (Sandbox Code Playgroud)

进入这个:

int? nullable = 3;
int? nullable2 = nullable;
Sample<int> sample = nullable2.HasValue ? ((Sample<int>)nullable2.GetValueOrDefault()) : null;
Run Code Online (Sandbox Code Playgroud)

因为Sample<int>是一个类,所以可以为其实例分配一个空值,并且使用这样的隐式运算符,也可以分配一个可为空的对象的基础类型.所以这些作业是有效的:

int? a = 3;
int? b = null;
Sample<int> sampleA = a; 
Sample<int> sampleB = b;
Run Code Online (Sandbox Code Playgroud)

如果Sample<int>是a struct,那当然会给出错误.

编辑: 那为什么这可能?我无法在规范中找到它,因为它是故意的规范违规,这只是为了向后兼容而保留.你可以在代码中阅读它:

DELIBERATE SPEC VIOLATION:
即使转换的返回类型不是非可空值类型,本机编译器也允许"提升"转换.例如,如果我们有从struct S到string的转换,那么从S开始"转换"转换?to native被本机编译器认为存在,语义为"s.HasValue?(string)s.Value:(string)null".Roslyn编译器为了向后兼容性而使该错误永久化.

这就是在Roslyn中实现这个"错误"的方式:

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

temp = operand
temp.HasValue ? op_Whatever(temp.GetValueOrDefault()) : default(P)
Run Code Online (Sandbox Code Playgroud)

因此,根据给定用户定义的转换运算符的规范, T -> U存在提升的运算符T? -> U?,其中TU不可为空的值类型.然而,U由于上述原因,这种逻辑也被实现用于转换运算符,其中它是参考类型.

第2部分如何防止在这种情况下编译代码?那么有一种方法.您可以专门为可空类型定义其他隐式运算符,并使用属性对其进行装饰Obsolete.这需要将type参数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();
}
Run Code Online (Sandbox Code Playgroud)

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

如果不能进行这样的限制,则必须分别为每个值类型定义每个运算符(如果确实可以使用反射并使用模板生成代码):

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

如果在代码中的任何位置引用,那将会出错:

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


Evk*_*Evk 19

我认为这是提升转换运营商的行动.规格说:

给定一个用户定义的转换运算符,它从非可空值类型S转换为非可空值类型T,存在一个从S转换的提升转换运算符?到T?.这个提升的转换操作符执行从S解包?到S后跟用户定义的从S到T的转换,接着是从T到T的包装,除了空值S?直接转换为空值T?.

看起来它在这里不适用,因为虽然type S是值类型here(int),但type T不是值类型(Sampleclass).但是,Roslyn存储库中的这个问题表明它实际上是规范中的一个错误.Roslyn 代码文档证实了这一点:

如上所述,这里我们以两种方式偏离规范.首先,如果正常形式不适用,我们只检查提升的表格.其次,我们应该只适用于当转换参数和返回类型起重语义两个非空值类型.

事实上,本机编译器根据以下内容确定是否检查提升的表单:

  • 我们最终是从可空值类型转换的类型吗?
  • 转换的参数类型是否为非可空值类型?
  • 我们最终将类型转换为可空值类型,指针类型或引用类型吗?

如果所有这些问题的答案都是"是",那么我们可以提升为可空,并查看最终的运算符是否适用.

如果编译器遵循规范 - 它会在这种情况下产生一个错误,正如你所期望的那样(在某些旧版本中它会这样做),但现在却没有.

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

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

正如描述提升操作符如何工作的第一个引用中所描述的(除了我们允许T作为引用类型) - 您可能会注意到它描述了您的情况下究竟发生了什么.nullvalues S(int?)直接赋给T(Sample)而没有转换运算符,并且非null被解包int并运行在你的运算符中(T?如果T是引用类型,显然不需要换行).


Fab*_*jan 8

为什么第一个代码段中的代码会编译?

Nullable<T>可以在此处找到源代码中的代码示例:

[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;
}
Run Code Online (Sandbox Code Playgroud)

该结构Nullable<int>具有一个被覆盖的显式操作者以及方法GetValueOrDefault这两个中的一个用于通过编译器转换int?T.

之后它运行了implicit operator Sample<T>(T value).

发生的事情的粗略画面是:

Sample<int> sampleA = (Sample<int>)(int)a;
Run Code Online (Sandbox Code Playgroud)

如果我们typeof(T)Sample<T>隐式运算符内打印它将显示:System.Int32.

在第二个方案中的编译器不使用implicit operator Sample<T>,只是分配nullsampleB.