C#中的'unsafe'函数 — *(float*)(&result)与(float)(result)之间的区别

46

有人能简单地解释一下以下代码吗:

public unsafe static float sample(){    
      int result = 154 + (153 << 8) + (25 << 16) + (64 << 24);

      return *(float*)(&result); //don't know what for... please explain
}

注意:上述代码使用了不安全的函数。

对于上面的代码,我很难理解其返回值与下面的返回值有何区别:

return (float)(result);

如果你返回的是*(float*)(&result),那么是否有必要使用不安全的函数?


1
我在mscorlib/System/BitConvert/ToSingle中找到了使用ILSpy的示例代码。没有给出任何解释。我需要理解其中的流程,因为我需要将其转换为PHP。 - gchimuel
10
关于 return (float)result;reinterpret_cast<float*>(&result) 的区别 - 前者是一个转换(conversion),它把整数 123 转换成浮点数 123.0F,但这并不是一种重新解释的强制类型转换(re-interpretive cast),因为 123123.0F 的字节完全不同。而后者则是一种重新解释的强制类型转换,它表示"这里有4个字节,现在将它们视为一个 float"。 - Marc Gravell
1
使用指针重新解释数据的方式被称为“类型转换”。 - OrangeDog
1
我有点好奇是否这是将4个字节转换为浮点数的唯一方法。我知道在C语言中是这样的,但指针操作在那里更常见。 - Earlz
3
若要在 PHP 中实现此功能,请参考 http://stackoverflow.com/questions/2624869/bytes-convert-to-float-php,而不是尝试逆向工程一个完全不同的语言所做的事情。如果您已经有了一个字节数组,请使用 unpack。 - Random832
显示剩余7条评论
6个回答

77
在 .NET 中,float 使用 IEEE binary32 单精度浮点数来表示,并使用 32 位存储。代码将这个数字通过将其位组装成一个 int,然后使用 unsafe 强制转换为 float 来构建。在 C++ 中,此类强制类型转换被称为 reinterpret_cast,转换时不执行任何转换 - 只是将位重新解释为新类型。

IEEE single precision floating number

组装的数字是十六进制值 4019999A,或二进制值 01000000 00011001 10011001 10011010

  • 符号位为 0 (正数)。
  • 指数位为 10000000 (即 128),结果指数为 128 - 127 = 1 (分数乘以 2 ^ 1 = 2)。
  • 小数位为 00110011001100110011010,如果没有其他内容,几乎可以识别出零和一的模式。

返回的浮点数的位与转换为浮点数的 2.4 完全相同,整个函数可以简单地替换为文字字面常量 2.4f

最后的零有点“破坏”了小数部分的位模式,这是为了使浮点数匹配可以使用浮点数文字字面量写出的内容吗?


那么普通转换和这种奇怪的“不安全转换”之间有什么区别呢?

假设以下代码:

int result = 0x4019999A // 1075419546
float normalCast = (float) result;
float unsafeCast = *(float*) &result; // Only possible in an unsafe context
第一个转换将整数1075419546转换为其浮点表示,例如1075419546f。这涉及计算用于表示原始整数的浮点数所需的符号、指数和小数位。这是一个需要完成的复杂计算。
第二个转换更加阴险(只能在不安全的上下文中执行)。 &result获取result的地址,返回指向存储整数1075419546的位置的指针。然后可以使用指针解引用运算符*来检索指针指向的值。使用*&result将从该位置检索存储的整数,但首先将指针强制转换为float*(指向float的指针),从内存位置中检索到一个浮点数,结果是将浮点数2.4f分配给unsafeCast。因此,*(float*) &result的叙述是“给我一个指向result的指针,并假设该指针是指向float的指针,并检索指针指向的值”。
与第一个转换相反,第二个转换不需要任何计算。它只是将存储在result中的32位数据塞入unsafeCast中(它也是32位的)。
通常,执行这样的强制转换可能会失败,但通过使用unsafe,您告诉编译器您知道自己在做什么。

最终的零似乎是为了使浮点数具有精确的小数表示,即在分数的"破位"处达到精确性。但并非完全精确——就像在十进制中我们将2/3四舍五入为0.66667而不是0.66666一样。 - Random832
1
+!使用“险恶”的字眼 - Nahum
@Random832:你说得对,用32位浮点数无法精确表示2.4(我一开始没有意识到这个问题),但是存在某些数字在十进制和二进制系统中都不是无限小数。例如,11/4=2.75十进制=10.11二进制。 - Martin Liversage
@MartinLiversage 我只是在解释为什么它会输出 ..999A 而不是 ..9999 - 这与十进制中 2/3 的末尾为 7 而不是 6 的原因相同。 - Random832
@Random832:抱歉,我误解了,但是这个例子看起来很牵强,我猜测有人通过使用位模式来制作一个随机浮点数,然后调整最后一位数字,以使其能够回传到浮点文字。 - Martin Liversage
显示剩余2条评论

19

如果我正确理解这个方法的作用,那么下面的代码是一种安全等价替代:

public static float sample() {    
   int result = 154 + (153 << 8) + (25 << 16) + (64 << 24);

   byte[] data = BitConverter.GetBytes(result);
   return BitConverter.ToSingle(data, 0);
}

正如已经说过的那样,它将int值重新解释为float


2
智能转换!我想知道安全版本的性能如何与不安全版本相比。 - Rune Grimstad
2
@RuneGrimstad 可能不太好,因为它复制了数据而不是只将其转换为不同类型的指针。不过,这可能是在不使用不安全代码的情况下完成它的最简单方法。 - Bradley Smith
2
需要中间的byte[]有点可惜。虽然有一个BitConverter.Int64BitsToDouble()方法可以使用longdouble实现同样的功能,但没有Int32BitsToSingle()方法... - Bradley Smith
1
您也可以使用“联合”来“安全地”完成此操作,即使用 StructLayout(LayoutKind.Explicit) - Mark Hurd
@MarkHurd,你能多说一些关于这个的吗? - Display Name
显示剩余3条评论

4
这似乎是一次优化尝试。您正在对浮点数的整数表示执行整数计算,而不是进行浮点运算。
请记住,浮点数与int一样以二进制值存储。
计算完成后,您将使用指针和强制转换将整数转换为浮点值。
这与将值转换为float不同。这将使int值1变为float值1.0。在这种情况下,您将int值转换为在int中存储的二进制值描述的浮点数。
这很难恰当地解释。我将寻找一个例子。 :-)
编辑: 看这里:http://en.wikipedia.org/wiki/Fast_inverse_square_root 您的代码基本上与此文章中描述的相同。

2
因为Sarge Borsch提出了要求,这里是“Union”等效的内容:
[StructLayout(LayoutKind.Explicit)]
struct ByteFloatUnion {
  [FieldOffset(0)] internal byte byte0;
  [FieldOffset(1)] internal byte byte1;
  [FieldOffset(2)] internal byte byte2;
  [FieldOffset(3)] internal byte byte3;
  [FieldOffset(0)] internal float single;
}

public static float sample() {
   ByteFloatUnion result;
   result.single = 0f;
   result.byte0 = 154;
   result.byte1 = 153;
   result.byte2 = 25;
   result.byte3 = 64;

   return result.single;
}

太棒了!我在一本书(CLR via C#)中听说过这个东西,但没想到它可以这样使用。 - Display Name

2

回复:它正在做什么?

它将存储在int中的字节值作为float解释(不经过转换)。

幸运的是,浮点数和整数具有相同的数据大小,都是4个字节。


0

正如其他人所描述的那样,它将int的字节视为float。

您可以在不使用不安全代码的情况下获得相同的结果:

public static float sample()
{
    int result = 154 + (153 << 8) + (25 << 16) + (64 << 24);
    return BitConverter.ToSingle(BitConverter.GetBytes(result), 0);
}

但这样做速度就不会很快了,你最好使用浮点数和数学函数。


2
你的答案与 Bradley Smith 的完全一样。 - Guillaume

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接