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