11

我有以下代码:

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;

这段代码可以编译,这让我感到惊讶。显式转换应该只接受 a byte,而不是 a double。但是以某种方式接受了双重。当我在转换中放置一个断点时,我看到它value已经是一个bytewith value 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
4

3 回答 3

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

是的。

你能解释一下为什么演员是合法的吗?

正如 Ken Kin 指出的那样,我在这里解释一下:

C# 中的链式用户定义显式转换

简而言之:用户定义的显式转换可能会在“两端”插入一个内置的显式转换。也就是说,我们可以插入从源表达式到用户定义转换方法的参数类型的显式转换,或者从用户定义的转换方法的返回类型到转换的目标类型。(或者,在极少数情况下,两者都有。)

在这种情况下,我们插入了对参数类型字节的内置显式转换,因此您的程序与您编写的程序相同:

        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# 编译器静默插入显式转换的其他情况?

是的; 事实上,这是今天第二次提出这个问题。看

C#类型转换不一致?

于 2013-05-07T19:45:23.197 回答
10

首先,让我们看看 Lippert 先生的博客:

C# 中的链式用户定义显式转换

编译器有时为我们插入显式转换:

  • 部分博文:

    ...

    当用户定义的显式转换需要在调用端或返回端进行显式转换时,编译器将根据需要插入显式转换。

    编译器认为,如果开发人员首先将显式转换放在代码中,那么开发人员就知道他们在做什么,并冒着任何转换都可能失败的风险。

    ...

作为这个问题,它只是有时的时间之一。编译器插入的显式转换就像我们在以下代码中编写的那样:

  • 使用显式转换测试泛型方法

    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,而在泛型方法测试中,是方法定义中的泛型参数。您可能想查看§II.7.1规范标准 ECMA-335的章节。

但是,这里最重要的是,它们都进入<uint8>了泛型定义的类型(即字节);正如我上面提到的,根据 Lippert 先生的博文告诉我们,编译器有时会在您明确指定它们时插入式转换!

最后,您认为这是编译器的奇怪行为,让我猜猜您认为编译器该做什么:

  • 通过指定类型参数测试泛型方法:

    public partial class TestClass {
        public static void TestGenericMethodBySpecifyingTypeParameter() {
            double d=2.5;
            Num<byte> b=NumHelper.From<byte>(d);
        }
    }
    

我猜对了吗?无论如何,我们在这里再次感兴趣的是 IL。我迫不及待地想看到 IL,它是:

0PC4l.png

Ooooops .. 似乎不像编译器认为显式运算符的行为那样。

最后,当我们明确指定转换时,说我们期望将一件事转换为另一件事是非常语义化的,编译器推断出这一点并插入所涉及类型的明显必要的转换;一旦它发现所涉及的类型不能被转换,它就会抱怨,就像我们指定了一个更简单的错误转换一样,例如(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; 

也许更容易理解。

于 2013-05-07T18:21:35.450 回答
0

C# 标准 (ECMA-334) 的相关部分是 §13.4.4。我已经用粗体强调了与您上面的代码相关的部分。

用户定义的从类型S到类型T的显式转换处理如下:

[省略]

  • 找到一组适用的转换运算符,U。该集合由用户定义的,并且如果ST都可以为空,提升隐式或显式转换运算符(第 13.7.3 节)组成,由类或结构声明,从包含或包含D的类型转换为包含或包含S的类型由T. 如果U为空,则不进行转换,并发生编译时错误。

术语包含包含在第 13.4.2 节中定义。

具体来说,转换运算符 from bytetoNum<byte>将在强制转换为时考虑doubleNum<byte>因为byte(操作符方法的实际参数类型)可以隐式转换为double(即byte包含在操作数类型中double)。像这样的用户定义的运算符仅被考虑用于显式转换,即使该运算符被标记为implicit

于 2013-05-07T19:55:04.653 回答