为什么从 <T> 到 <U> 的隐式转换运算符接受 <T?>?
Why does an implicit conversion operator from <T> to <U> accept <T?>?
这是一种我无法理解的奇怪行为。在我的示例中,我有一个 class 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;
}
一个很好的答案可能会分为两部分:
- 为什么第一个片段中的代码可以编译?
- 在这种情况下我可以阻止代码编译吗?
你可以看看编译器是如何降低这段代码的:
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>
是一个 class 它的实例可以被分配一个 null 值,并且使用这样一个隐式运算符,也可以分配一个可空对象的基础类型。所以这样的分配是有效的:
int? a = 3;
int? b = null;
Sample<int> sampleA = a;
Sample<int> sampleB = b;
如果Sample<int>
是struct
,那当然会报错。
编辑:
那么为什么这可能呢?我在规范中找不到它,因为它是故意违反规范的,只是为了向后兼容而保留的。您可以在 code:
中阅读相关信息
DELIBERATE SPEC VIOLATION:
The native compiler allows for a "lifted" conversion even when the return type of the conversion not a non-nullable value type. For example, if we have a conversion from struct S to string, then a "lifted" conversion from S? to string is considered by the native compiler to exist, with the semantics of "s.HasValue ? (string)s.Value : (string)null". The Roslyn compiler perpetuates this error for the sake of backwards compatibility.
这就是 "error" 在 Roslyn 中 implemented 的样子:
Otherwise, if the return type of the conversion is a nullable value type, reference type or pointer type P, then we lower this as:
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();
如果在代码中的任何地方引用,都会出错:
Error CS0619 'Sample.implicit operator Sample(int?)' is obsolete: 'Some error message'
Why does the code in the first snippet compile?
来自 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
。
我认为它已提升转换运算符的作用。规范说:
Given a user-defined conversion operator that converts from a
non-nullable value type S to a non-nullable value type T, a lifted
conversion operator exists that converts from S? to T?. This lifted
conversion operator performs an unwrapping from S? to S followed by
the user-defined conversion from S to T followed by a wrapping from T
to T?, except that a null valued S? converts directly to a null valued
T?.
这里好像不适用,因为S
这里是值类型(int
),T
不是值类型(Sample
class).但是 this issue in Roslyn repository states that it's actually a bug in specification. And Roslyn code 文档证实了这一点:
As mentioned above, here we diverge from the specification, in two
ways. First, we only check for the lifted form if the normal form was
inapplicable. Second, we are supposed to apply lifting semantics only
if the conversion parameter and return types are both non-nullable
value types.
In fact the native compiler determines whether to check for a lifted
form on the basis of:
- Is the type we are ultimately converting from a nullable value type?
- Is the parameter type of the conversion a non-nullable value type?
- Is the type we are ultimately converting to a nullable value type, pointer type, or reference type?
If the answer to all those questions is "yes" then we lift to nullable
and see if the resulting operator is applicable.
如果编译器遵循规范 - 在这种情况下它会像您预期的那样产生错误(并且在某些旧版本中会产生错误),但现在不会。
所以总结一下:我认为编译器使用你的隐式运算符的提升形式,根据规范这应该是不可能的,但是编译器在这里偏离了规范,因为:
- 它被认为是规范中的错误,而不是编译器中的错误。
- 旧的、早于 roslyn 的编译器已经违反规范,保持向后兼容性很好。
如描述提升运算符工作原理的第一条引文中所述(另外我们允许 T
是引用类型)——您可能会注意到它准确描述了您的情况。 null
值 S
(int?
) 直接赋值给 T
(Sample
) 不带转换运算符,非 null 解包为 int
和 运行 通过您的操作员(如果 T
是引用类型,显然不需要包装到 T?
)。
这是一种我无法理解的奇怪行为。在我的示例中,我有一个 class 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;
}
一个很好的答案可能会分为两部分:
- 为什么第一个片段中的代码可以编译?
- 在这种情况下我可以阻止代码编译吗?
你可以看看编译器是如何降低这段代码的:
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>
是一个 class 它的实例可以被分配一个 null 值,并且使用这样一个隐式运算符,也可以分配一个可空对象的基础类型。所以这样的分配是有效的:
int? a = 3;
int? b = null;
Sample<int> sampleA = a;
Sample<int> sampleB = b;
如果Sample<int>
是struct
,那当然会报错。
编辑: 那么为什么这可能呢?我在规范中找不到它,因为它是故意违反规范的,只是为了向后兼容而保留的。您可以在 code:
中阅读相关信息DELIBERATE SPEC VIOLATION:
The native compiler allows for a "lifted" conversion even when the return type of the conversion not a non-nullable value type. For example, if we have a conversion from struct S to string, then a "lifted" conversion from S? to string is considered by the native compiler to exist, with the semantics of "s.HasValue ? (string)s.Value : (string)null". The Roslyn compiler perpetuates this error for the sake of backwards compatibility.
这就是 "error" 在 Roslyn 中 implemented 的样子:
Otherwise, if the return type of the conversion is a nullable value type, reference type or pointer type P, then we lower this as:
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();
如果在代码中的任何地方引用,都会出错:
Error CS0619 'Sample.implicit operator Sample(int?)' is obsolete: 'Some error message'
Why does the code in the first snippet compile?
来自 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
。
我认为它已提升转换运算符的作用。规范说:
Given a user-defined conversion operator that converts from a non-nullable value type S to a non-nullable value type T, a lifted conversion operator exists that converts from S? to T?. This lifted conversion operator performs an unwrapping from S? to S followed by the user-defined conversion from S to T followed by a wrapping from T to T?, except that a null valued S? converts directly to a null valued T?.
这里好像不适用,因为S
这里是值类型(int
),T
不是值类型(Sample
class).但是 this issue in Roslyn repository states that it's actually a bug in specification. And Roslyn code 文档证实了这一点:
As mentioned above, here we diverge from the specification, in two ways. First, we only check for the lifted form if the normal form was inapplicable. Second, we are supposed to apply lifting semantics only if the conversion parameter and return types are both non-nullable value types.
In fact the native compiler determines whether to check for a lifted form on the basis of:
- Is the type we are ultimately converting from a nullable value type?
- Is the parameter type of the conversion a non-nullable value type?
- Is the type we are ultimately converting to a nullable value type, pointer type, or reference type?
If the answer to all those questions is "yes" then we lift to nullable and see if the resulting operator is applicable.
如果编译器遵循规范 - 在这种情况下它会像您预期的那样产生错误(并且在某些旧版本中会产生错误),但现在不会。
所以总结一下:我认为编译器使用你的隐式运算符的提升形式,根据规范这应该是不可能的,但是编译器在这里偏离了规范,因为:
- 它被认为是规范中的错误,而不是编译器中的错误。
- 旧的、早于 roslyn 的编译器已经违反规范,保持向后兼容性很好。
如描述提升运算符工作原理的第一条引文中所述(另外我们允许 T
是引用类型)——您可能会注意到它准确描述了您的情况。 null
值 S
(int?
) 直接赋值给 T
(Sample
) 不带转换运算符,非 null 解包为 int
和 运行 通过您的操作员(如果 T
是引用类型,显然不需要包装到 T?
)。