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

时间:2018-05-17 09:31:31

标签: c# generics nullable implicit-conversion value-type

这是一种我无法理解的怪异行为。在我的示例中,我有一个类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);
}

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

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

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

情况变得更糟。此处未调用转换运算符,sampleB将设置为null

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

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

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

3 个答案:

答案 0 :(得分:42)

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

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中了解相关信息:

  

审查规则违规:
  原生编译器允许提升&#34;提升&#34;转换时甚至当转换的返回类型不是非可空值类型时。例如,如果我们有从struct S到string的转换,那么&#34;解除&#34;从S转换? to native被本机编译器认为是存在的,其语义为&#34; s.HasValue? (string)s.Value:(string)null&#34;。为了向后兼容,Roslyn编译器使这个错误永久化。

这是&#34;错误&#34;在罗斯林是implemented

  

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

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

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

第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&#39; Sample.implicit operator Sample(int?)&#39;已过时:&#39;一些错误消息&#39;

答案 1 :(得分:19)

我认为它已经解除了转换运营商的作用。规范说:

  

给定一个用户定义的转换运算符,它从a转换   非可空值类型S到非可空值类型T,一个被提升   转换运算符是否存在从S转换?到T?。这解除了   转换运算符执行从S展开?到S然后   用户定义的从S到T的转换,然后从T换行   到T ?,除了空值S?直接转换为null值   T'。

看起来它在这里不适用,因为虽然类型S是值类型(int),但类型T不是值类型(Sample类)。但是,Roslyn存储库中的this issue表明它实际上是规范中的错误。罗斯林code文件确认了这一点:

  

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

     

实际上,本机编译器确定是否检查是否已解除   形式基于:

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

如果所有这些问题的答案都是“是”,那么我们可以解除可空   并查看生成的运算符是否适用。

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

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

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

如第一篇文章所述,描述了提升操作符的工作方式(另外我们允许T为引用类型) - 您可能会注意到它确切地描述了您的案例中发生的情况。 nullS int?)直接分配给TSample)而没有转换运算符,非空值将解包到int如果T?是引用类型,则显然不需要包装到T

答案 2 :(得分:8)

  

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

来自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;
}

struct Nullable<int>有一个覆盖显式运算符和方法GetValueOrDefault编译器使用这两者中的一个来将int?转换为T

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

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

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

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

在您的第二个场景中,编译器没有使用implicit operator Sample<T>,只是将null分配给sampleB