在CLR中使用“as”关键字进行投射
当编程接口,我发现我正在做很多铸造或对象types转换。
这两种转换方法有区别吗? 如果是这样,是有成本差异,或者这如何影响我的程序?
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引入了模式匹配,它已经在很大程度上取代了as
运算符,正如你现在写的:
if (randomObject is TargetType tt) { // Use tt here }
请注意, tt
在此之后仍然在范围之内,但是没有明确分配。 (它在if
体内是明确的。)在某些情况下,这有些恼人,所以如果你真的关心在每个范围内引入尽可能less的variables,你仍然可以使用后跟一个强制转换。
到目前为止,我认为没有任何答案(在开始这个答案的时候!)已经真正地解释了它在哪里值得使用。
-
不要这样做:
// Bad code - checks type twice for no reason if (randomObject is TargetType) { TargetType foo = (TargetType) randomObject; // Do something with foo }
这不仅是检查两次,但它可能会检查不同的事情,如果
randomObject
是一个字段,而不是一个局部variables。 如果另一个线程改变了这randomObject
之间的randomObject
的值,那么“if”可能会通过,但是如果另一个线程改变了randomObject
的值,则转换失败。 -
如果
randomObject
真的应该是TargetType
一个实例,也就是说,如果没有,那就意味着有一个bug,那么cast就是正确的解决scheme。 这会立即引发exception,这意味着在不正确的假设下没有更多的工作,exception正确地显示了错误的types。// 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
是引用types,那么使用如下代码:TargetType convertedRandomObject = randomObject as TargetType; if (convertedRandomObject != null) { // Do stuff with convertedRandomObject }
-
如果
randomObject
可能是TargetType
一个实例,并且TargetType
是一个值types,那么我们不能as
使用TargetType
本身as
使用,但是我们可以使用一个可为空的types:TargetType? convertedRandomObject = randomObject as TargetType?; if (convertedRandomObject != null) { // Do stuff with convertedRandomObject.Value }
(注意:目前这个速度实际上比“+”更慢,我认为它更加优雅和一致,但是我们走了。)
-
如果你真的不需要转换的值,但你只需要知道它是否是 TargetType的一个实例,那么
is
运算符就是你的朋友。 在这种情况下,TargetType是一个引用types还是一个值types并不重要。 -
可能还有其他涉及generics的情况,在哪里
is
有用的(因为你可能不知道T是否是引用types,所以你不能使用),但它们是比较模糊的。 -
我几乎可以肯定地使用的
is
现在的值types的情况下,没有想过使用可空types和一起:)
编辑:请注意,以上没有谈到性能,除了值types的情况下,我已经注意到拆箱到可为空值types实际上是慢 – 但一致。
根据naasking的回答,is-and-cast或is-and-as与现在的JIT一样快,而且如下所示:
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); } }
在我的笔记本电脑上,这些都在60ms左右执行。 有两件事要注意:
- 他们之间没有显着的区别。 (事实上,有些情况下,as-plus-null-check肯定是慢的,上面的代码实际上使得types检查变得容易,因为它是用于密封类的;如果你正在检查一个接口,赞成加零检查。)
- 他们都非常快速。 这根本不会是你的代码的瓶颈,除非你真的不打算对值做任何事情 。
所以,我们不要担心performance。 让我们担心正确性和一致性。
我认为is-and-cast(或is-and-as)在处理variables时都是不安全的,因为它引用的值的types可能因testing和转换之间的另一个线程而改变。 这将是一个非常罕见的情况 – 但我宁愿有一个我可以持续使用的惯例。
我还认为,as-then-null-check可以更好地分离问题。 我们有一个试图进行转换的陈述,然后是一个使用结果的陈述。 is-and-cast或is-and-as会执行testing, 然后再尝试转换值。
换句话说,任何人都会写:
int value; if (int.TryParse(text, out value)) { value = int.Parse(text); // Use value }
这是什么 – 演员正在做什么 – 虽然显然在一个相当便宜的方式。
“as”将会返回NULL,如果不行的话。
铸造之前会引发exception。
对于表演来说,举办例外通常会花费更多的时间。
这是另一个答案,一些IL比较。 考虑这个类:
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。 即使操作码对您来说没有任何意义,您可以看到一个主要区别 – 在DirectCast方法中调用isinst后跟castclass。 所以两个电话,而不是一个基本上。
.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
isinst关键字与castclass
这个博客文章对这两种方式进行了比较。 他的总结是:
- 直接比较,isinst比castclass更快(虽然只是略微)
- 当必须执行检查以确保转换成功时,isinst比castclass快得多
- 不应该使用isinst和castclass的组合,因为这比最快的“安全”转换要慢得多(慢12%以上)
我个人总是使用As,因为它易于阅读,并由.NET开发团队(或者Jeffrey Richter)推荐。
两者之间更细微的差异之一是,当涉及到投射操作符时,“as”关键字不能用于投射:
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”关键字不考虑转换操作符,所以不会在最后一行进行编译(尽pipe我认为它是在以前的版本中)。 行string cast = (string)f;
虽然工作得很好。
因为如果它不能执行返回null的转换( 如仅在引用types上操作) 那么从不抛出exception。 所以使用基本相当于
_myCls2 = _myObj is MyClass ? (MyClass)_myObj : null;
另一方面,C风格的演员在没有转换的情况下会抛出exception。
不是你的问题的答案,但我认为是一个重要的相关点。
如果你正在编程接口,你不应该需要投。 希望这些演员是非常罕见的。 如果不是,您可能需要重新考虑一些接口。
请忽略乔恩Skeet的build议,回避:testing和铸造模式,即:
if (randomObject is TargetType) { TargetType foo = randomObject as TargetType; // Do something with foo }
这个成本比铸造和空testing更成本的想法是一个神话 :
TargetType convertedRandomObject = randomObject as TargetType; if (convertedRandomObject != null) { // Do stuff with convertedRandomObject }
这是微不足道的优化。 我运行了一些真正的testing ,test-and-cast实际上比cast-and-null-comparison更快,而且它也更安全,因为你不可能在范围外有一个空引用失败。
如果你想要一个为什么testing和演员速度更快,或者至less不会更慢的原因,有一个简单和复杂的原因。
简单:即使是天真的编译器也会将两个类似的操作(如testing和投射)合并成一个testing和分支。 cast-and-null-test可能会强制执行两个testing和一个分支,一个用于typestesting,一个用于null检查。 至less,他们都将优化到一个单独的testing和分支,所以testing和投射不会比铸造和空testing更慢也不会更快。
复杂:为什么testing和转换速度更快:cast-and-null-test将另外一个variables引入到外部范围中,编译器必须跟踪活性,并且可能无法根据复杂程度来优化该variables,stream量是。 相反,test-and-cast只在分隔的范围内引入一个新的variables,所以编译器知道该范围退出后该variables已经死了,所以可以更好地优化寄存器分配。
所以请,请让这个“铸造和空testing比testing和铸造”build议DIE。 请。 testing和演员既安全又快速。
如果转换失败,“as”关键字不会抛出exception; 它将variables设置为null(或者设置为其值types的默认值)。
这不是对问题的回答,而是对问题代码示例的评论:
通常你不应该把例如IMyInterface的对象转换成MyClass。 接口的优点在于,如果将一个对象作为实现接口的input,那么您就不必关心自己正在获取哪种对象。
如果您将IMyInterface投射到MyClass,那么您已经假定您获得了MyClasstypes的对象,并且使用IMyInterface是没有意义的,因为如果您将代码与其他实现IMyInterface的类一起提供,将会破坏您的代码。
现在,我的build议是:如果你的界面devise的很好,你可以避免大量的types转换。
as
运算符只能用于引用types,不能重载,如果操作失败,则返回null
。 它永远不会抛出exception。
可以在任何兼容types上使用Casting,可以重载,如果操作失败,会抛出exception。
select哪一个取决于具体情况。 首先,这是一个问题,你是否想抛出一个失败的转换exception。
我的回答仅仅是在我们不检查types的情况下的速度,并且在铸造之后我们不检查空值。 我添加了两个额外的testingJon Skeet的代码:
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); } }
结果:
Is and Cast: 30000000 : 88 Is and As: 30000000 : 93 As and null check: 30000000 : 56 Cast: 30000000 : 66 As: 30000000 : 46
不要试图把注意力集中在速度上(因为这一切都是非常快的)。
除了这里已经暴露出来的一切之外,我认为在显式投射之间我认为值得注意的实际区别
var x = (T) ...
与使用as
运算符相比。
这是一个例子:
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将不能使用结构types。 GenericCaster将。
as
关键字与兼容引用types之间的显式转换工作方式相同,主要区别是转换失败时不会引发exception。 相反,它会在目标variables中产生一个空值。 由于exception在性能方面非常昂贵,因此被认为是更好的铸造方法。
你的select强烈取决于需要什么。 我更喜欢明确的铸造
IMyInterface = (IMyInterface)someobj;
因为如果对象应该由IMyInterfacetypes而不是 – 这肯定是问题。 尽可能早地得到错误是更好的,因为确切的错误将被固定,而不是固定其副作用。
但是,如果你处理接受object
作为参数的方法,那么你需要在执行任何代码之前检查它的确切types。 在这种情况下as
可以避免InvalidCastException
。
这取决于你是否想在使用“as”之后检查null,还是希望你的应用程序抛出exception?
我的经验法则是,如果我总是期望variables是我期望的types,那么我希望我使用一个types转换。 如果可能的话,variables不会投到我想要的,我准备处理空值使用as,我将使用as。
看看这些链接:
- 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
他们向你展示一些细节和性能testing。
如果您使用Office PIA以.NET Framework 4.X为目标,则应使用as关键字,否则将无法编译。
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时, 施法是可以的:
Microsoft.Office.Interop.Outlook.MailItem m = (Microsoft.Office.Interop.Outlook.MailItem)o.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem);
当针对.NET 4.X的错误是:
错误CS0656:缺less编译器所需的成员'Microsoft.CSharp.RuntimeBinder.Binder.Convert'
错误CS0656:缺less编译器所需的成员'Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create'
OP的问题仅限于特定的铸造情况。 标题涵盖更多的情况。
以下是我目前可以想到的所有相关铸造情况的概述:
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 }