这是一种我无法理解的怪异行为。在我的示例中,我有一个类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;
}
一个好的答案可能会分为两部分:
答案 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?
,其中T
和U
不可为空价值类型。但是,由于上述原因,此类逻辑也针对转换运算符实现,其中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文件确认了这一点:
如上所述,这里我们在两个方面偏离了规范 方法。首先,我们只检查正常形式的提升形式 不适用。其次,我们应该只应用提升语义 如果转换参数和返回类型都不可为空 价值类型。
实际上,本机编译器确定是否检查是否已解除 形式基于:
- 我们最终是从可以为空的值类型转换的类型吗?
- 转换的参数类型是否为非可空值类型?
- 我们最终将类型转换为可空值类型,指针类型或引用类型吗?
如果所有这些问题的答案都是“是”,那么我们可以解除可空 并查看生成的运算符是否适用。
如果编译器遵循规范 - 在这种情况下会产生错误(正如您所期望的那样)(但在某些旧版本中它会这样做),但现在却没有。
总结一下:我认为编译器使用隐式运算符的提升形式,根据规范应该是不可能的,但编译器与规范不同,因为:
如第一篇文章所述,描述了提升操作符的工作方式(另外我们允许T
为引用类型) - 您可能会注意到它确切地描述了您的案例中发生的情况。 null
值S
int?
)直接分配给T
(Sample
)而没有转换运算符,非空值将解包到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
。