我认为这是一个很好的问题,值得认真而详细地回答。类型转换是 C# 实际上有很多不同的东西。
与 C# 不同,像 C++ 这样的语言对这些非常严格,所以我将使用那里的命名作为参考。我一直认为最好了解事物的工作原理,因此我将在这里为您详细介绍。如下:
动态转换和静态转换
C# 有值类型和引用类型。引用类型始终遵循继承链,从 Object 开始。
基本上,如果你做(Foo)myObject,你实际上是在做一个动态演员表,如果你在做(object)myFoo(或简单的object o = myFoo)你在做一个静态演员表。
动态转换 需要您进行类型检查,也就是说,运行时将检查您要转换的对象是否属于该类型。毕竟,您正在向下转换继承树,因此您不妨完全转换为其他东西。如果是这种情况,您最终会得到一个InvalidCastException。因此,动态转换需要运行时类型信息(例如,它要求运行时知道什么对象具有什么类型)。
静态转换不需要类型检查。在这种情况下,我们在继承树中进行转换,所以我们已经知道类型转换会成功。永远不会抛出异常。
值类型转换 是一种特殊类型的转换,可将不同的值类型(例如从浮点数转换为整数)。我稍后再谈。
照原样
在 IL 中,唯一受支持的是 castclass (cast) 和 isinst (as)。 is 操作符被实现为带有空值检查的as,只不过是一种方便的速记符号,可以将它们结合起来。在 C# 中,您可以将 is 写为:(myObject as MyFoo) != null。
as 只是检查对象是否属于特定类型,如果不是则返回 null。对于 static cast 的情况,我们可以确定这个编译时间,对于 dynamic cast 的情况,我们必须在运行时检查这个。
(...) 再次强制转换检查类型是否正确,如果不正确则抛出异常。它与as 基本相同,但结果是抛出而不是null。这可能会让您想知道为什么as 没有被实现为异常处理程序——嗯,这可能是因为异常相对较慢。
拳击
当您将值类型box 转换为对象时,会发生一种特殊类型的转换。基本上发生的是 .NET 运行时将您的值类型复制到堆上(带有一些类型信息)并将地址作为引用类型返回。换句话说:它将值类型转换为引用类型。
当你有这样的代码时会发生这种情况:
int n = 5;
object o = n; // boxes n
int m = (int)o; // unboxes o
拆箱需要您指定类型。在拆箱操作期间,检查类型(类似于 dynamic cast 的情况,但它更简单,因为值类型的继承链是微不足道的),如果类型匹配,则将值复制回堆栈。
您可能期望值类型转换对于装箱来说是隐式的——好吧,因为上面它们不是。唯一允许的拆箱操作是拆箱到确切的值类型。换句话说:
sbyte m2 = (sbyte)o; // throws an error
值类型转换
如果您将float 转换为int,则基本上是在转换该值。对于基本类型 (IntPtr, (u)int 8/16/32/64, float, double),这些转换在 IL 中预定义为 conv_* 指令,相当于位转换 (int8 -> int16) 、截断 (int16 -> int8) 和转换 (float -> int32)。
顺便说一句,这里发生了一些有趣的事情。运行时似乎可以处理堆栈上的大量 32 位值,因此即使在您不期望它们的地方也需要转换。例如,考虑:
sbyte sum = (sbyte)(sbyte1 + sbyte2); // requires a cast. Return type is int32!
int sum = int1 + int2; // no cast required, return type is int32.
标志扩展名可能很难让您头疼。计算机将有符号整数值存储为 1 补码。在十六进制表示法 int8 中,这意味着值 -1 是 0xFF。那么如果我们将其转换为 int32 会发生什么?同样,-1 的 1 补码值为 0xFFFFFFFF - 所以我们需要将最高有效位传播到其余“添加”位。如果我们在做无符号扩展,我们需要传播零。
为了说明这一点,这里有一个简单的测试用例:
byte b1 = 0xFF;
sbyte b2 = (sbyte)b1;
Console.WriteLine((int)b1);
Console.WriteLine((int)b2);
Console.ReadLine();
第一次转换为 int 是零扩展,第二次转换为 int 是符号扩展。您可能还想使用“x8”格式字符串来获取十六进制输出。
对于位转换、截断和转换之间的确切区别,我参考了解释这些区别的LLVM documentation。查找sext/zext/bitcast/fptosi 和所有变体。
隐式类型转换
还有一个类别,那就是转换运算符。 MSDN 详细介绍了如何重载conversion operators。基本上你可以做的是通过重载一个操作符来实现你自己的转换。如果您希望用户明确指定您打算投射,请添加 explicit 关键字;如果您希望自动进行隐式转换,请添加implicit。基本上你会得到:
public static implicit operator byte(Digit d) // implicit digit to byte conversion operator
{
return d.value; // implicit conversion
}
...之后你可以做类似的事情
Digit d = new Digit(123);
byte b = d;
最佳做法
首先,了解差异,这意味着实施小型测试程序,直到您了解上述所有内容之间的区别。没有替代品可以理解 Stuff 的工作原理。
然后,我会坚持这些做法:
- 存在速记是有原因的。使用最短的符号,它可能是最好的。
- 不要将强制类型转换用于静态类型转换;仅对动态转换使用转换。
- 仅在需要时才使用拳击。这个细节远远超出了这个答案;基本上我的意思是:使用正确的类型,不要包装所有内容。
- 注意关于隐式转换(例如无符号/有符号)的编译器警告,总是通过显式转换来解决它们。您不想因为符号/零扩展而对奇怪的值感到惊讶。
- 在我看来,除非您确切地知道自己在做什么,否则最好避免隐式/显式转换——简单的方法调用通常会更好。这样做的原因是,您最终可能会得到一个松散的例外,这是您没有预见到的。