Casting vs using the 'as' keyword in the CLR
在编程接口时,我发现我正在进行大量的转换或对象类型转换。
这两种转换方法有区别吗?如果是这样,是否存在成本差异,或者这对我的计划有何影响?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public interface IMyInterface { void AMethod(); } public class MyClass : IMyInterface { public void AMethod() { //Do work } // Other helper methods.... } public class Implementation { IMyInterface _MyObj; MyClass _myCls1; MyClass _myCls2; public Implementation() { _MyObj = new MyClass(); // What is the difference here: _myCls1 = (MyClass)_MyObj; _myCls2 = (_MyObj as MyClass); } } |
另外,"一般"的首选方法是什么?
下面的答案写在2008年。
C 7引入了模式匹配,这在很大程度上取代了
1 2 3 4 |
请注意,在此之后,
到目前为止,我认为没有任何答案(在开始这个答案的时候!)真的解释了哪些地方值得使用。
不要这样做:
1
2
3
4
5
6// Bad code - checks type twice for no reason
if (randomObject is TargetType)
{
TargetType foo = (TargetType) randomObject;
// Do something with foo
}如果
randomObject 是字段而不是局部变量,那么这种检查不仅是两次,而且可能是检查不同的事情。如果"if"可以通过,但如果另一个线程在两个线程之间更改EDOCX1的值(4),则强制转换将失败。如果
randomObject 真的应该是TargetType 的一个实例,也就是说,如果不是,那就意味着存在一个bug,那么casting就是正确的解决方案。这会立即抛出异常,这意味着在不正确的假设下不会再做任何工作,并且异常会正确地显示错误的类型。1
2
3
4// This will throw an exception if randomObject is non-null and
// refers to an object of an incompatible type. The cast is
// the best code if that's the behaviour you want.
TargetType convertedRandomObject = (TargetType) randomObject;如果
randomObject 可能是TargetType 的实例,TargetType 是引用类型,那么使用如下代码:1
2
3
4
5TargetType convertedRandomObject = randomObject as TargetType;
if (convertedRandomObject != null)
{
// Do stuff with convertedRandomObject
}如果
randomObject 可能是TargetType 的一个实例,TargetType 是一个值类型,那么我们不能将as 与TargetType 本身一起使用,但是我们可以使用一个可以为空的类型:1
2
3
4
5TargetType? convertedRandomObject = randomObject as TargetType?;
if (convertedRandomObject != null)
{
// Do stuff with convertedRandomObject.Value
}(注意:目前这实际上比is+cast慢。我觉得它更优雅,更连贯,但我们开始了。)
如果您确实不需要转换值,但只需要知道它是否是TargetType的实例,那么
is 操作符就是您的朋友。在这种情况下,不管TargetType是引用类型还是值类型。可能还有其他一些涉及泛型的案例,其中
is 是有用的(因为您可能不知道t是否是引用类型,所以不能使用as),但它们相对来说比较模糊。我以前几乎肯定使用了
is 作为值类型的情况,没有考虑使用可空类型和as 一起使用:)
编辑:请注意,除了值类型的情况外,上面没有提到性能,我注意到取消绑定到可为空的值类型实际上要慢一些,但要保持一致。
根据NAASKING的答案,IS和CAST或IS和AS都与现代JIT一样快,都是空检查,如下面的代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | using System; using System.Diagnostics; using System.Linq; class Test { const int Size = 30000000; static void Main() { object[] values = new object[Size]; for (int i = 0; i < Size - 2; i += 3) { values[i] = null; values[i + 1] ="x"; values[i + 2] = new object(); } FindLengthWithIsAndCast(values); FindLengthWithIsAndAs(values); FindLengthWithAsAndNullCheck(values); } static void FindLengthWithIsAndCast(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int len = 0; foreach (object o in values) { if (o is string) { string a = (string) o; len += a.Length; } } sw.Stop(); Console.WriteLine("Is and Cast: {0} : {1}", len, (long)sw.ElapsedMilliseconds); } static void FindLengthWithIsAndAs(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int len = 0; foreach (object o in values) { if (o is string) { string a = o as string; len += a.Length; } } sw.Stop(); Console.WriteLine("Is and As: {0} : {1}", len, (long)sw.ElapsedMilliseconds); } static void FindLengthWithAsAndNullCheck(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int len = 0; foreach (object o in values) { string a = o as string; if (a != null) { len += a.Length; } } sw.Stop(); Console.WriteLine("As and null check: {0} : {1}", len, (long)sw.ElapsedMilliseconds); } } |
在我的笔记本电脑上,这些都在60毫秒内执行。需要注意的两件事:
- 他们之间没有明显的区别。(实际上,在某些情况下,AS加零检查肯定较慢。上面的代码实际上使类型检查变得容易,因为它是针对一个密封类的;如果您要检查一个接口,平衡提示稍微偏向于a s加上NULL检查。)
- 他们都快得要命。这不会成为代码中的瓶颈,除非您以后真的不打算对值做任何事情。
所以我们不必担心性能。让我们担心正确性和一致性。
我认为在处理变量时,is和cast(或is和as)都是不安全的,因为它引用的值的类型可能会由于测试和cast之间的另一个线程而改变。这将是一个非常罕见的情况-但我宁愿有一个公约,我可以一直使用。
我还认为as-then空检查可以更好地分离关注点。我们有一个语句尝试转换,然后有一个语句使用结果。IS和CAST或IS和AS执行测试,然后再次尝试转换值。
换一种说法,有人会写:
1 2 3 4 5 6 | int value; if (int.TryParse(text, out value)) { value = int.Parse(text); // Use value } |
这就是现在和演员们正在做的事情——尽管显然是以一种更便宜的方式。
如果无法强制转换,"as"将返回空值。
之前的强制转换将引发异常。
对于性能,引发异常通常在时间上花费更大的成本。
这是另一个答案,通过一些IL比较。考虑课程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class MyClass { public static void Main() { // Call the 2 methods } public void DirectCast(Object obj) { if ( obj is MyClass) { MyClass myclass = (MyClass) obj; Console.WriteLine(obj); } } public void UsesAs(object obj) { MyClass myclass = obj as MyClass; if (myclass != null) { Console.WriteLine(obj); } } } |
现在看看每个方法产生的IL。即使这些操作代码对您没有任何意义,您也可以看到一个主要的区别:ISISist被调用,然后在DirectCast方法中被castclass调用。所以基本上是两个电话而不是一个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | .method public hidebysig instance void DirectCast(object obj) cil managed { // Code size 22 (0x16) .maxstack 8 IL_0000: ldarg.1 IL_0001: isinst MyClass IL_0006: brfalse.s IL_0015 IL_0008: ldarg.1 IL_0009: castclass MyClass IL_000e: pop IL_000f: ldarg.1 IL_0010: call void [mscorlib]System.Console::WriteLine(object) IL_0015: ret } // end of method MyClass::DirectCast .method public hidebysig instance void UsesAs(object obj) cil managed { // Code size 17 (0x11) .maxstack 1 .locals init (class MyClass V_0) IL_0000: ldarg.1 IL_0001: isinst MyClass IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: brfalse.s IL_0010 IL_000a: ldarg.1 IL_000b: call void [mscorlib]System.Console::WriteLine(object) IL_0010: ret } // end of method MyClass::UsesAs |
isist关键字与castclass
这篇博文对这两种方式进行了比较。他的总结是:
- 在直接比较中,isist比castclass快(尽管只是稍微快一点)
- 当必须执行检查以确保转换成功时,isist比castclass快得多。
- 不应使用isist和castclass的组合,因为这比最快的"安全"转换慢得多(慢12%以上)。
我个人总是使用as,因为它很容易阅读,并且是.NET开发团队(或Jeffrey Richter)推荐的。
两者之间更细微的区别之一是,当涉及到强制转换运算符时,"as"关键字不能用于强制转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class Foo { public string Value; public static explicit operator string(Foo f) { return f.Value; } } public class Example { public void Convert() { var f = new Foo(); f.Value ="abc"; string cast = (string)f; string tryCast = f as string; } } |
这不会在最后一行编译(尽管我认为在以前的版本中是这样),因为"as"关键字不考虑cast操作符。不过,
如果它不能执行返回空值的转换,则从不引发异常(仅对引用类型进行操作)。所以使用as基本上等同于
1 |
另一方面,C样式的强制转换在无法转换时抛出异常。
不是你问题的真正答案,但我认为是一个重要的相关点。
如果您正在编程到一个接口,则不需要强制转换。希望这些石膏非常罕见。如果不是,您可能需要重新考虑一些接口。
请忽略乔恩·斯基特的建议,回复:避免测试和投射模式,即:
1 2 3 4 5 | if (randomObject is TargetType) { TargetType foo = randomObject as TargetType; // Do something with foo } |
这比一个演员表和一个空测试花费更多的想法是一个神话:
1 2 3 4 5 | TargetType convertedRandomObject = randomObject as TargetType; if (convertedRandomObject != null) { // Do stuff with convertedRandomObject } |
这是一个不起作用的微观优化。我运行了一些实际的测试,并且test和cast实际上比cast和NULL比较快,而且它也更安全,因为如果强制转换失败,那么在if之外的范围内就不可能有空引用。
如果您想知道为什么测试和强制转换更快,或者至少不慢,有一个简单而复杂的原因。
简单:即使是幼稚的编译器也会将两个类似的操作(如test和cast)合并到一个测试和分支中。强制转换和空测试可以强制两个测试和一个分支,一个用于类型测试,一个用于失败时转换为空,一个用于空检查本身。至少,它们都将优化为单个测试和分支,因此测试和强制转换既不慢也不快于强制转换和空测试。
复杂:为什么测试和强制转换更快:强制转换和空测试将另一个变量引入外部作用域,编译器必须跟踪该外部作用域的活动性,并且它可能无法根据控制流的复杂程度来优化该变量。相反,test和cast只在限定的范围内引入一个新的变量,这样编译器就知道该变量在范围退出后就死了,因此可以更好地优化寄存器分配。
所以,请让这个"空测试比测试更好"的建议模具。拜托。测试和铸造既安全又快速。
如果强制转换失败,"as"关键字不会引发异常;而是将变量设置为空(或值类型的默认值)。
这不是问题的答案,而是对问题代码示例的注释:
通常不需要将对象从imyinterface强制转换为myClass。接口的伟大之处在于,如果你把一个对象作为实现接口的输入,那么你不必关心你得到的是什么类型的对象。
如果您将imyinterface强制转换为myClass,而不是假定您已经得到了一个myClass类型的对象,那么使用imyinterface是没有意义的,因为如果您将代码与实现imyinterface的其他类一起提供,它将破坏您的代码…
现在,我的建议是:如果你的接口设计得很好,你可以避免很多类型转换。
强制转换可用于任何兼容类型,它可以重载,如果操作失败,它将引发异常。
使用哪一种取决于具体情况。主要的问题是,是否要对失败的转换抛出异常。
我的答案只是关于速度,在不检查类型和不检查空值的情况下。我在jon skeet的代码中添加了两个附加测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | using System; using System.Diagnostics; class Test { const int Size = 30000000; static void Main() { object[] values = new object[Size]; for (int i = 0; i < Size; i++) { values[i] ="x"; } FindLengthWithIsAndCast(values); FindLengthWithIsAndAs(values); FindLengthWithAsAndNullCheck(values); FindLengthWithCast(values); FindLengthWithAs(values); Console.ReadLine(); } static void FindLengthWithIsAndCast(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int len = 0; foreach (object o in values) { if (o is string) { string a = (string)o; len += a.Length; } } sw.Stop(); Console.WriteLine("Is and Cast: {0} : {1}", len, (long)sw.ElapsedMilliseconds); } static void FindLengthWithIsAndAs(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int len = 0; foreach (object o in values) { if (o is string) { string a = o as string; len += a.Length; } } sw.Stop(); Console.WriteLine("Is and As: {0} : {1}", len, (long)sw.ElapsedMilliseconds); } static void FindLengthWithAsAndNullCheck(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int len = 0; foreach (object o in values) { string a = o as string; if (a != null) { len += a.Length; } } sw.Stop(); Console.WriteLine("As and null check: {0} : {1}", len, (long)sw.ElapsedMilliseconds); } static void FindLengthWithCast(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int len = 0; foreach (object o in values) { string a = (string)o; len += a.Length; } sw.Stop(); Console.WriteLine("Cast: {0} : {1}", len, (long)sw.ElapsedMilliseconds); } static void FindLengthWithAs(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int len = 0; foreach (object o in values) { string a = o as string; len += a.Length; } sw.Stop(); Console.WriteLine("As: {0} : {1}", len, (long)sw.ElapsedMilliseconds); } } |
结果:
1 2 3 4 5 |
不要像我一样把注意力集中在速度上,因为这一切都很快。
除了这里已经曝光的所有内容之外,我刚刚发现了一个我认为值得注意的实际差异,即明确的演员阵容
1 | var x = (T) ... |
而不是使用
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | class Program { static void Main(string[] args) { Console.WriteLine(GenericCaster<string>(12345)); Console.WriteLine(GenericCaster<object>(new { a = 100, b ="string" }) ??"null"); Console.WriteLine(GenericCaster<double>(20.4)); //prints: //12345 //null //20.4 Console.WriteLine(GenericCaster2<string>(12345)); Console.WriteLine(GenericCaster2<object>(new { a = 100, b ="string" }) ??"null"); //will not compile -> 20.4 does not comply due to the type constraint"T : class" //Console.WriteLine(GenericCaster2<double>(20.4)); } static T GenericCaster<T>(object value, T defaultValue = default(T)) { T castedValue; try { castedValue = (T) Convert.ChangeType(value, typeof(T)); } catch (Exception) { castedValue = defaultValue; } return castedValue; } static T GenericCaster2<T>(object value, T defaultValue = default(T)) where T : class { T castedValue; try { castedValue = Convert.ChangeType(value, typeof(T)) as T; } catch (Exception) { castedValue = defaultValue; } return castedValue; } } |
底线:GenericCaster2不能用于结构类型。普通施法者会。
如果使用面向.NET Framework 4.x的Office PIA,则应使用as关键字,否则将无法编译。
1 2 | Microsoft.Office.Interop.Outlook.Application o = new Microsoft.Office.Interop.Outlook.Application(); Microsoft.Office.Interop.Outlook.MailItem m = o.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem) as Microsoft.Office.Interop.Outlook.MailItem; |
当以.NET 2.0为目标时,强制转换是可以的,但是:
1 | Microsoft.Office.Interop.Outlook.MailItem m = (Microsoft.Office.Interop.Outlook.MailItem)o.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem); |
以.NET 4.x为目标时,错误为:
错误CS0656:缺少编译器所需的成员"Microsoft.CSharp.RuntimeBinder.Binder.Convert"
错误CS0656:缺少所需的编译器成员"microsoft.csharp.runtimebinder.csharpargumentinfo.create"
看看这些链接:
- http://gen5.info/q/2008/06/13/prefix-casting-versus-as-casting-in-c/
- http://www.codeproject.com/articles/8052/type-casting-impact-over-execution-performance-in
它们向您展示了一些细节和性能测试。
你的选择很大程度上取决于你需要什么。我更喜欢直白的演员阵容
1 | IMyInterface = (IMyInterface)someobj; |
因为如果对象应该是imyinterface类型,而不是-这绝对是个问题。最好尽早得到误差,因为准确的误差将被修正而不是修正其副作用。
但是,如果您处理的方法接受
OP的问题仅限于特定的铸造情况。这个标题涵盖了更多的情况。以下是我目前能想到的所有相关的演员阵容情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | private class CBase { } private class CInherited : CBase { } private enum EnumTest { zero, one, two } private static void Main (string[] args) { //########## classes ########## // object creation, implicit cast to object object oBase = new CBase (); object oInherited = new CInherited (); CBase oBase2 = null; CInherited oInherited2 = null; bool bCanCast = false; // explicit cast using"()" oBase2 = (CBase)oBase; // works oBase2 = (CBase)oInherited; // works //oInherited2 = (CInherited)oBase; System.InvalidCastException oInherited2 = (CInherited)oInherited; // works // explicit cast using"as" oBase2 = oBase as CBase; oBase2 = oInherited as CBase; oInherited2 = oBase as CInherited; // returns null, equals C++/CLI"dynamic_cast" oInherited2 = oInherited as CInherited; // testing with Type.IsAssignableFrom(), results (of course) equal the results of the cast operations bCanCast = typeof (CBase).IsAssignableFrom (oBase.GetType ()); // true bCanCast = typeof (CBase).IsAssignableFrom (oInherited.GetType ()); // true bCanCast = typeof (CInherited).IsAssignableFrom (oBase.GetType ()); // false bCanCast = typeof (CInherited).IsAssignableFrom (oInherited.GetType ()); // true //########## value types ########## int iValue = 2; double dValue = 1.1; EnumTest enValue = EnumTest.two; // implicit cast, explicit cast using"()" int iValue2 = iValue; // no cast double dValue2 = iValue; // implicit conversion EnumTest enValue2 = (EnumTest)iValue; // conversion by explicit cast. underlying type of EnumTest is int, but explicit cast needed (error CS0266: Cannot implicitly convert type 'int' to 'test01.Program.EnumTest') iValue2 = (int)dValue; // conversion by explicit cast. implicit cast not possible (error CS0266: Cannot implicitly convert type 'double' to 'int') dValue2 = dValue; enValue2 = (EnumTest)dValue; // underlying type is int, so"1.1" beomces"1" and then"one" iValue2 = (int)enValue; dValue2 = (double)enValue; enValue2 = enValue; // no cast // explicit cast using"as" // iValue2 = iValue as int; error CS0077: The as operator must be used with a reference type or nullable type } |
这取决于使用"as"后是否要检查空值,还是希望应用程序引发异常?
我的经验法则是,如果我总是期望变量是我在使用强制转换时所期望的类型。如果变量可能不会强制转换为我想要的值,并且我准备处理使用as的空值,那么我将使用as。