Casting vs usando la palabra clave ‘como’ en el CLR

Cuando programo interfaces, descubrí que estoy realizando una gran cantidad de conversiones o conversión de tipo de objeto.

¿Hay alguna diferencia entre estos dos métodos de conversión? Si es así, ¿hay una diferencia de costo o cómo afecta esto a mi progtwig?

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

Además, ¿cuál es “en general” el método preferido?

La respuesta debajo de la línea fue escrita en 2008.

C # 7 introdujo la coincidencia de patrones, que ha reemplazado en gran medida al operador as, como ahora puede escribir:

 if (randomObject is TargetType tt) { // Use tt here } 

Tenga en cuenta que tt todavía está en el scope después de esto, pero no está definitivamente asignado. (Definitivamente se asigna dentro del cuerpo if ). Eso es un poco molesto en algunos casos, por lo que si realmente te importa introducir el menor número posible de variables en cada ámbito, es posible que quieras utilizar seguido de un molde.


¡No creo que ninguna de las respuestas hasta el momento (al momento de comenzar esta respuesta!) Realmente haya explicado dónde vale la pena usarla.

  • No hagas esto:

     // Bad code - checks type twice for no reason if (randomObject is TargetType) { TargetType foo = (TargetType) randomObject; // Do something with foo } 

    Esta comprobación no solo se realiza dos veces, sino que también puede estar comprobando cosas diferentes, si randomObject es un campo en lugar de una variable local. Es posible que pase el “si” pero luego falla el lanzamiento, si otro hilo cambia el valor de randomObject entre los dos.

  • Si randomObject realmente debería ser una instancia de TargetType , es decir, si no lo es, eso significa que hay un error, entonces el casting es la solución correcta. Eso arroja una excepción de inmediato, lo que significa que no se realiza más trabajo bajo suposiciones incorrectas, y la excepción muestra correctamente el tipo de error.

     // 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; 
  • Si randomObject podría ser una instancia de TargetType y TargetType es un tipo de referencia, entonces use un código como este:

     TargetType convertedRandomObject = randomObject as TargetType; if (convertedRandomObject != null) { // Do stuff with convertedRandomObject } 
  • Si randomObject podría ser una instancia de TargetType y TargetType es un tipo de valor, no podemos usarlo as TargetType , pero podemos usar un tipo que TargetType :

     TargetType? convertedRandomObject = randomObject as TargetType?; if (convertedRandomObject != null) { // Do stuff with convertedRandomObject.Value } 

    (Nota: actualmente esto es más lento que lo que es + cast . Creo que es más elegante y consistente, pero ahí vamos).

  • Si realmente no necesita el valor convertido, pero solo necesita saber si es una instancia de TargetType, entonces el operador es su amigo. En este caso, no importa si TargetType es un tipo de referencia o un tipo de valor.

  • Puede haber otros casos relacionados con generics en los is es útil (porque es posible que no sepa si T es un tipo de referencia o no, por lo que no puede usarlo), pero son relativamente poco claros.

  • Casi con certeza he usado el caso del tipo de valor antes de ahora, sin haber pensado en usar un tipo de nullable y as conjunto 🙂


EDITAR: tenga en cuenta que ninguna de las anteriores habla sobre el rendimiento, aparte del tipo de valor de caso, donde he notado que unboxing a un tipo de valor que admite valores de nulo es realmente más lento, pero coherente.

Según la respuesta de naasking, is-and-cast o is-and-as son tan rápidos como “as-and-null-check” con los JIT modernos, como se muestra en el siguiente código:

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

En mi computadora portátil, todos estos se ejecutan en aproximadamente 60 ms. Dos cosas a tener en cuenta:

  • No hay una diferencia significativa entre ellos. (De hecho, hay situaciones en las que la verificación más-nula definitivamente es más lenta. El código anterior realmente hace que la verificación de tipo sea fácil porque es para una clase sellada; si está buscando una interfaz, el saldo se inclina levemente a favor de la verificación más-nula).
  • Todos son increíblemente rápidos. Esto simplemente no será el cuello de botella en su código a menos que realmente no vaya a hacer nada con los valores después.

Así que no nos preocupemos por el rendimiento. Preocupémonos por la corrección y la coherencia.

Yo mantengo que is-and-cast (o is-and-as) no son seguros cuando se trata de variables, ya que el tipo de valor al que se refiere puede cambiar debido a otro hilo entre la prueba y el reparto. Esa sería una situación bastante rara, pero preferiría tener una convención que pueda usar de manera consistente.

También sostengo que el "entonces-null-check" ofrece una mejor separación de preocupaciones. Tenemos una statement que intenta una conversión, y luego una statement que usa el resultado. El is-and-cast o is-and-as realiza una prueba y luego otro bash de convertir el valor.

Para decirlo de otra manera, ¿alguien escribiría alguna vez ?

 int value; if (int.TryParse(text, out value)) { value = int.Parse(text); // Use value } 

Eso es lo que está haciendo y lo que está haciendo, aunque obviamente de una manera bastante más barata.

“as” devolverá NULL si no es posible lanzar.

lanzar antes generará una excepción.

Para el rendimiento, elevar una excepción suele ser más costoso en el tiempo.

Aquí hay otra respuesta, con alguna comparación IL. Considera la clase:

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

Ahora mira la IL que produce cada método. Incluso si los códigos op no significan nada para usted, puede ver una diferencia importante: se llama isinst seguido de castclass en el método DirectCast. Así que dos llamadas en lugar de una básicamente.

 .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 

La palabra clave isinst contra castclass

Esta publicación de blog tiene una comparación decente entre las dos formas de hacerlo. Su resumen es:

  • En una comparación directa, isinst es más rápido que castclass (aunque solo ligeramente)
  • Al tener que realizar comprobaciones para garantizar que la conversión fue exitosa, isinst fue significativamente más rápido que castclass
  • No se debe utilizar una combinación de isinst y castclass ya que fue mucho más lenta que la conversión “segura” más rápida (más de un 12% más lenta)

Personalmente siempre uso As, porque es fácil de leer y es recomendado por el equipo de desarrollo de .NET (o Jeffrey Richter de todos modos)

Una de las diferencias más sutiles entre los dos es que la palabra clave “como” no se puede usar para lanzar cuando un operador de reparto está involucrado:

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

Esto no se comstackrá (aunque creo que lo hizo en versiones anteriores) en la última línea, ya que las palabras clave “como” no tienen en cuenta a los operadores de elenco. La string cast = (string)f; líneas string cast = (string)f; funciona bien sin embargo.

como nunca arroja una excepción si no puede realizar la conversión devolviendo nulo en su lugar ( ya que opera solo en tipos de referencia). Entonces usar es básicamente equivalente a

 _myCls2 = _myObj is MyClass ? (MyClass)_myObj : null; 

Los moldes estilo C, por otro lado, lanzan una excepción cuando no es posible la conversión.

No es realmente una respuesta a su pregunta, pero creo que es un punto importante relacionado.

Si está progtwigndo en una interfaz, no debería necesitar transmitir. Con suerte, estos moldes son muy raros. Si no, es probable que necesite replantear algunas de sus interfaces.

Por favor, ignore el consejo de Jon Skeet, re: evite el patrón de prueba y lanzamiento, es decir .:

 if (randomObject is TargetType) { TargetType foo = randomObject as TargetType; // Do something with foo } 

La idea de que esto cueste más que un yeso y una prueba nula es un MITO :

 TargetType convertedRandomObject = randomObject as TargetType; if (convertedRandomObject != null) { // Do stuff with convertedRandomObject } 

Es una micro-optimización que no funciona. Realicé algunas pruebas reales , y prueba y lanzamiento es en realidad más rápido que la comparación de lanzamiento y nulo, y es más seguro también porque no tienes la posibilidad de tener una referencia nula en el scope fuera del si el molde fallar.

Si desea una razón por la cual la prueba y el lanzamiento son más rápidos, o al menos no más lentos, existe un motivo simple y complejo.

Simple: incluso los comstackdores ingenuos fusionarán dos operaciones similares, como prueba y lanzamiento, en una única prueba y twig. cast-and-null-test puede forzar dos pruebas y una twig, una para la prueba de tipo y la conversión a nulo en la falla, una para la comprobación nula en sí misma. Por lo menos, ambos se optimizarán a una única prueba y twig, por lo que la prueba y el lanzamiento no serán ni más lentos ni más rápidos que la prueba de lanzamiento y prueba nula.

Complejo: por qué la prueba y el lanzamiento son más rápidos: la prueba de conversión y nula introduce otra variable en el ámbito externo, que el comstackdor debe rastrear para la vida, y puede no ser capaz de optimizar esa variable en función de la complejidad de su control. el flujo es Por el contrario, test-and-cast introduce una nueva variable solo en un ámbito delimitado, por lo que el comstackdor sabe que la variable está muerta después de que el scope finalice, y así puede optimizar mejor la asignación de registro.

Entonces, POR FAVOR, POR FAVOR, deje que este “cast y null-test es mejor que el consejo de prueba y lanzamiento” DIE. POR FAVOR. prueba y lanzamiento es más seguro y más rápido.

Si el lanzamiento falla, la palabra clave ‘como’ no lanza una excepción; establece la variable a nulo (o a su valor predeterminado para los tipos de valor) en su lugar.

Esta no es una respuesta a la pregunta, sino un comentario al ejemplo del código de la pregunta:

Por lo general, no debería tener que lanzar un objeto desde, por ejemplo, IMyInterface a MyClass. Lo bueno de las interfaces es que si toma un objeto como entrada que implementa una interfaz, entonces no tiene que importar qué tipo de objeto está recibiendo.

Si lanzas IMyInterface a MyClass, entonces ya supones que obtienes un objeto de tipo MyClass y no tiene sentido usar IMyInterface, porque si alimentas tu código con otras clases que implementan IMyInterface, rompería tu código …

Ahora, mi consejo: si sus interfaces están bien diseñadas, puede evitar una gran cantidad de encasillados.

El operador as solo se puede usar en tipos de referencia, no se puede sobrecargar y devolverá null si la operación falla. Nunca lanzará una excepción.

Casting se puede usar en cualquier tipo compatible, se puede sobrecargar y emitirá una excepción si la operación falla.

La elección de cuál usar depende de las circunstancias. En primer lugar, se trata de si desea lanzar una excepción en una conversión fallida.

Mi respuesta es solo acerca de la velocidad en los casos en que no verificamos el tipo y no verificamos los nulos después del lanzamiento. Agregué dos pruebas adicionales al código de Jon 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); } } 

Resultado:

 Is and Cast: 30000000 : 88 Is and As: 30000000 : 93 As and null check: 30000000 : 56 Cast: 30000000 : 66 As: 30000000 : 46 

No trates de enfocarte en la velocidad (como yo lo hice) porque todo esto es muy muy rápido.

Además de todo lo que ya estaba expuesto aquí, me encontré con una diferencia práctica que creo que vale la pena señalar, entre el casting explícito

 var x = (T) ... 

versus usar el operador as .

Aquí está el ejemplo:

 class Program { static void Main(string[] args) { Console.WriteLine(GenericCaster(12345)); Console.WriteLine(GenericCaster(new { a = 100, b = "string" }) ?? "null"); Console.WriteLine(GenericCaster(20.4)); //prints: //12345 //null //20.4 Console.WriteLine(GenericCaster2(12345)); Console.WriteLine(GenericCaster2(new { a = 100, b = "string" }) ?? "null"); //will not compile -> 20.4 does not comply due to the type constraint "T : class" //Console.WriteLine(GenericCaster2(20.4)); } static T GenericCaster(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(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; } } 

En pocas palabras : GenericCaster2 no funcionará con tipos de estructuras. GenericCaster lo hará.

Si usa las PIA de Office orientadas a .NET Framework 4.X, debe usar la palabra clave as , de lo contrario no se comstackrá.

 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; 

Casting está bien cuando se apunta a .NET 2.0, aunque:

 Microsoft.Office.Interop.Outlook.MailItem m = (Microsoft.Office.Interop.Outlook.MailItem)o.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem); 

Cuando se apunta a .NET 4.X, los errores son:

error CS0656: miembro requerido del comstackdor faltante ‘Microsoft.CSharp.RuntimeBinder.Binder.Convert’

error CS0656: miembro requerido del comstackdor faltante ‘Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create’

La palabra clave as funciona igual que una conversión explícita entre tipos de referencia compatibles, con la gran diferencia de que no genera una excepción si la conversión falla. Más bien, arroja un valor nulo en la variable objective. Como las Excepciones son muy costosas en términos de rendimiento, se considera un método mucho mejor de lanzamiento.

Lo que elija depende en gran medida de lo requerido. Prefiero el casting explícito

 IMyInterface = (IMyInterface)someobj; 

porque si el objeto debe estar de tipo IMyInterface y no lo es, definitivamente es un problema. Es mejor recibir el error lo antes posible porque se solucionará el error exacto en lugar de corregir su efecto secundario.

Pero si maneja métodos que aceptan object como parámetros, entonces necesita verificar su tipo exacto antes de ejecutar cualquier código. En tal caso as sería útil para evitar InvalidCastException .

Depende, ¿desea verificar null después de usar “como” o preferiría que su aplicación arrojara una excepción?

Mi regla empírica es si siempre espero que la variable sea del tipo que estoy esperando en el momento que deseo usar un cast. Si es posible que la variable no se proyecte a lo que deseo y estoy preparado para manejar los nulos de usar as, usaré como.

El problema del OP está limitado a una situación de lanzamiento específica. El título cubre muchas más situaciones.
Aquí hay una descripción general de todas las situaciones de casting relevantes que actualmente puedo pensar:

 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 }