我有以下代码:
public struct Num<T>
{
private readonly T _Value;
public Num(T value)
{
_Value = value;
}
static public explicit operator Num<T>(T value)
{
return new Num<T>(value);
}
}
...
double d = 2.5;
Num<byte> b = (Num<byte>)d;
此代码编译,令我感到惊讶。显式转换只应接受byte
,而不是double
。但双重被接受了。当我在转换中放置断点时,我发现value
已经是byte
,其值为2
。通过从double到byte的转换应该是明确的。
如果我用ILSpy反编译我的EXE,我会看到下一个代码:
double d = 2.5;
Program.Num<byte> b = (byte)d;
我的问题是:byte
的额外演员来自何处?为什么那里有额外的演员?我的演员到Num<byte>
去了哪里?
修改
结构Num<T>
是整个结构,因此不再需要隐藏的额外方法或运算符。
修改
IL,按要求:
IL_0000: nop
IL_0001: ldc.r8 2.5 // Load the double 2.5.
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: conv.u1 // Once again the explicit cast to byte.
IL_000d: call valuetype GeneriCalculator.Program/Num`1<!0> valuetype GeneriCalculator.Program/Num`1<uint8>::op_Explicit(!0)
IL_0012: stloc.1
IL_0013: ret
答案 0 :(得分:16)
让我们退后一步,提出一些澄清问题:
这个程序合法吗?
public struct Num<T>
{
private readonly T _Value;
public Num(T value)
{
_Value = value;
}
static public explicit operator Num<T>(T value)
{
return new Num<T>(value);
}
}
class Program
{
static void Main()
{
double d = 2.5;
Num<byte> b = (Num<byte>)d;
}
}
是
你能解释为什么演员合法吗?
正如肯健所指出的,我在这里解释一下:
简而言之:用户定义的显式转换可能会在“两端”插入内置显式转换。也就是说,我们可以插入从源表达式到用户定义的转换方法的参数类型的显式转换,或者从用户定义的转换方法的返回类型到转换的目标类型。 (或者,在极少数情况下,两者都有。)
在这种情况下,我们在参数类型byte中插入一个内置的显式转换,所以你的程序和你写的一样:
Num<byte> b = (Num<byte>)(byte)d;
这是理想的行为。 double可以显式转换为byte,因此double也可以显式转换为Num<byte>
。
有关完整说明,请阅读C#4规范中的第6.4.5节“用户定义的显式转换”。
为什么IL生成了
op_Implicit
而不是op_Explicit
?
没有;这个问题是以虚假为前提的。上述程序生成:
IL_0000: nop
IL_0001: ldc.r8 2.5
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: conv.u1
IL_000d: call valuetype Num`1<!0> valuetype Num`1<uint8>::op_Explicit(!0)
IL_0012: stloc.1
IL_0013: ret
你可能正在查看程序的旧版本。做一个干净的重建。
是否存在C#编译器静默插入显式转换的其他情况?
是;事实上,这是今天第二次出现这个问题。见
答案 1 :(得分:10)
首先,让我们来看看Lippert先生的博客:
Chained user-defined explicit conversions in C#
编译器有时 1 为我们插入显式转换:
...
当用户定义的显式转换需要在调用方或返回方进行显式转换时,编译器将根据需要插入显式转换。
编译器认为,如果开发人员首先在代码中放置显式强制转换,那么开发人员就知道他们在做什么,并承担任何转换可能失败的风险。
...
作为这个问题,它有时只是的一个时间。编译器插入的显式转换就像我们在下面的代码中编写的那样:
使用显式转换测试通用方法
public static class NumHelper {
public static Num<T> From<T>(T value) {
return new Num<T>(value);
}
}
public partial class TestClass {
public static void TestGenericMethodWithExplicitConversion() {
double d=2.5;
Num<byte> b=NumHelper.From((byte)d);
}
}
并且生成的测试方法的IL是:
IL_0000: nop
IL_0001: ldc.r8 2.5
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: conv.u1
IL_000d: call valuetype Num`1<!!0> NumHelper::From<uint8>(!!0)
IL_0012: stloc.1
IL_0013: ret
让我们退后一步,将显式运算符的测试视为您的问题:
测试显式运算符
public partial class TestClass {
public static void TestExplicitOperator() {
double d=2.5;
Num<byte> b=(Num<byte>)d;
}
}
你之前已经看过IL了:
IL_0000: nop
IL_0001: ldc.r8 2.5
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: conv.u1
IL_000d: call valuetype Num`1<!0> valuetype Num`1<uint8>::op_Explicit(!0)
IL_0012: stloc.1
IL_0013: ret
你注意到他们非常相似吗?区别在于参数!0
是原始代码的类型定义中的泛型参数,而泛型方法测试中的!!0
是泛型参数in方法定义。您可能希望查看规范Standard ECMA-335的章节§II.7.1
。
但是,最重要的是,它们都进入了泛型定义的类型<uint8>
(字节);正如我上面提到的,根据Lippert先生的博客文章告诉我们,当你 指定显式时,编译器有时会插入显式转换!
最后,正如你认为这是编译器的奇怪行为,让我猜你可能认为编译器应该做什么:
通过指定类型参数来测试通用方法:
public partial class TestClass {
public static void TestGenericMethodBySpecifyingTypeParameter() {
double d=2.5;
Num<byte> b=NumHelper.From<byte>(d);
}
}
我猜对了吗?无论如何,我们在这里再次感兴趣的是IL。我迫不及待地想看看IL,它是:
Ooooops ..似乎不是编译器认为显式运算符的行为。
对于conclution,当我们明确指定转换时,说我们期望将一个事物转换为另一个事物是非常语义的,编译器推断出并插入所涉及类型的明显必要的转换;一旦发现所涉及的类型不合法转换,它就会抱怨,就像我们指定一个更简单的错误转换,例如(String)3.1415926 ..
。
希望它现在更有帮助而不会失去正确性。
1 :这是有时的个人表达,在博客帖子中实际上是根据需要说 。
以下是对比的一些测试,当人们可能期望使用现有的显式运算符转换类型时;我在代码中写了一些评论来描述每个案例:
double d=2.5;
Num<byte> b=(Num<byte>)d; // explicitly
byte x=(byte)d; // explicitly, as the case above
Num<byte> y=d; // no explicit, and won't compile
// d can be `IConvertible`, compiles
Num<IConvertible> c=(Num<IConvertible>)d;
// d can be `IConvertible`;
// but the conversion operator is explicit, requires specified explicitly
Num<IConvertible> e=d;
// d cannot be `String`, won't compile even specified explicitly
Num<String> s=(Num<String>)d;
// as the case above, won't compile even specified explicitly
String t=(String)d;
也许它更容易理解。
答案 2 :(得分:0)
C#标准(ECMA-334)的相关部分是§13.4.4。我粗略地强调了与上面代码相关的部分。
从
S
类型到T
类型的用户定义显式转换按如下方式处理:[省略]
- 找到适用的转换运算符集
U
。此集包含用户定义的,如果S
和T
都可以为空,则由D
中的类或结构声明的提升隐式或显式转换运算符(第13.7.3节)从S
包含或类型转换为T
包含或包含的类型。如果U
为空,则表示没有转换,并且发生编译时错误。
包含的和包含的和在第13.4.2节中定义。
具体而言,在byte
转换为Num<byte>
时会考虑从double
到Num<byte>
的转换运算符,因为byte
(运算符的实际参数类型)方法)可以隐式转换为double
(即byte
包含在操作数类型double
中)。像这样的用户定义的运算符仅被考虑用于显式转换,即使运算符标记为implicit
。