2010-02-12 29 views
28

Teniendo en cuenta que este es un caso de uso muy natural (si usted no sabe lo que realmente hace as),¿Por qué se 'implementa' como 'como'?

if (x is Bar) { 
    Bar y = x as Bar; 
    something(); 
} 

es efectivamente equivalente (es decir, el compilador genera CIL en el código anterior será equivalente) a:

Bar y = x as Bar; 
if (y != null) { 
    y = x as Bar; //The conversion is done twice! 
    something(); 
} 

EDIT:

supongo que no había hecho mi pregunta clara. Nunca escribiría el segundo fragmento ya que, por supuesto, es redundante. Estoy afirmando que el CIL generado por el compilador al compilar el primer fragmento es equivalente al segundo fragmento, que es redundante. Preguntas: a) ¿Es esto correcto? b) Si es así, ¿por qué se implementa is así?

Esto es porque creo que el primer fragmento mucho más claro y más bonita que la realidad bien escrito-

Bar y = x as Bar; 
if (y != null) { 
    something(); 
} 

CONCLUSIÓN:

Optimizar el caso is/as no es responsabilidad del compilador, pero los JIT.

También, como con un nulo comprobar que tiene menos (y menos caro) instrucciones que ambas de las alternativas (is y as y is y cast).

Adición:

CIL para como con nullcheck (.NET 3.5):

L_0001: ldarg.1 
L_0002: isinst string 
L_0007: stloc.0 
L_0008: ldloc.0 
L_0009: ldnull 
L_000a: ceq 
L_000c: stloc.1 
L_000d: ldloc.1 
L_000e: brtrue.s L_0019 
L_0011: ldarg.0 
L_0019: ret 

CIL para es y fundido (.NET 3.5):

L_0001: ldarg.1 
L_0002: isinst string 
L_0007: ldnull 
L_0008: cgt.un 
L_000a: ldc.i4.0 
L_000b: ceq 
L_000d: stloc.1 
L_000e: ldloc.1 
L_000f: brtrue.s L_0021 
L_0012: ldarg.1 
L_0013: castclass string 
L_0018: stloc.0 
L_0019: ldarg.0 
L_0021: ret 

CIL para es y como (.NET 3.5):

L_0001: ldarg.1 
L_0002: isinst string 
L_0007: ldnull 
L_0008: cgt.un 
L_000a: ldc.i4.0 
L_000b: ceq 
L_000d: stloc.1 
L_000e: ldloc.1 
L_000f: brtrue.s L_0021 
L_0012: ldarg.1 
L_0013: isinst string 
L_0018: stloc.0 
L_0019: ldarg.0 
L_0021: ret 

Estos han sido editados por brevedad (declaraciones de método, nops y llamadas a algo() eliminado).

+4

No lo llamaría un caso de uso típico, más típico sería Bar y = x como Bar; if (y! = null) {do_stuff(); }. Si usa _as_ de todos modos, ¿por qué verificar con _es_ primero? – dbemerlin

+0

@dbemerlin: Porque el primer fragmento es mucho más natural y agradable en los ojos que el segundo. Este es exactamente mi punto. –

+5

El primero me dice que fue escrito por alguien que no sabe lo que dice 'as', para ser honesto. – AakashM

Respuesta

12

a) ¿Es esto correcto

Sí, aunque me hubiera indicado de otra manera. Usted está diciendo que "es" es un azúcar sintáctico para un seguimiento como sigue-por-cero. Lo hubiera dicho de otra manera: que "como" es un azúcar sintáctico para "verificar la implementación del tipo, lanzar si es exitoso, nulo si falla".

Es decir, estaría más inclinado a decir

if (x is Bar) { 
    Bar y = x as Bar; 
    something(); 
} 

es efectivamente equivalente a

if (x is Bar) { 
    Bar y = (x is Bar) ? (Bar)x : (Bar) null; 
    something(); 
} 

Sede, se quiere definir "como" en términos de "es", no de la otra manera. La pregunta realmente debería ser "¿por qué está tan implementado como está?" :-)

b) Si es así, ¿por qué se implementa así?

Porque eso es una implementación correcta de la especificación.

Creo que no estoy siguiendo su línea de pensamiento aquí. ¿Hay algún problema con esa implementación? ¿Cómo prefieres que se implemente? Usted tiene las instrucciones "isinst" y "castclass" a su disposición; describa el codegen para su programa que le gustaría ver.

+0

No hay nada INCORRECTO con la implementación. Simplemente representa un caso de uso particular que personalmente me gusta más que la alternativa correcta (creo que es más fácil de leer e intuitiva) ineficiente. Entonces me preguntaba si había una razón para eso. Y luego, para hacer que funcione para este caso de uso, el compilador debe realizar una optimización que preserve la instancia en caché utilizada en la prueba: if (x es Bar) {Bar y = x como Bar; // Me gustaría que aquí se use la misma instancia utilizada en la prueba if}. Pero eso podría no ser seguro para subprocesos en algunas situaciones y probablemente habría otros problemas ... –

+0

@Vinko: Pero se usa la misma instancia. Siempre es la misma referencia a la misma instancia. Todavía no te estoy siguiendo. ¿Estás diciendo que no quieres que la prueba de tipo se realice tres veces? Porque se realiza tres veces en el caso de éxito aquí - una vez en el primer "es", una vez en el "es" que es parte del "como", y una vez durante el reparto. Si no quiere la prueba redundante, entonces no use "como" en la consecuencia. Diga "Bar y = (Bar) x;". Entonces solo se realiza la prueba dos veces. –

+0

Aunque esto todavía no explica por qué no hay optimización en el primer caso con is y as. ¿O está ahí? –

2

No hará un segundo y = x as Bar;, porque ya tiene y que es Bar.

+0

No lo haré, pero si lo escribo de la manera más natural (es), el compilador lo hará –

5

En su ejemplo, el uso de as es redundante de todos modos. Puesto que usted ya sabe que x is Bar, usted debe utilizar un yeso:

if (x is Bar) 
{ 
    Bay y = (Bar)x; 
} 

Alternativamente, convertir usando as y simplemente comprobar la nula:

Bar y = x as Bar; 
if (y != null) 
{ 

} 
+0

El punto de la pregunta es que el compilador genera el código de redundancia. –

+0

El primer ejemplo tiene dos moldes, mientras que el segundo solo, y los moldes son caros. Entonces preferiría el segundo enfoque cuando Bar es una clase. (Para tipos de valor, solo se puede usar el primero, por supuesto.) – treaschf

5

En primer lugar estoy de acuerdo con su premisa de que esto es más típica caso de usoPuede ser su enfoque favorito, pero el enfoque idiomática es el estilo "como + cheque nulo":

Bar y = x as Bar; 
if (y != null) { 
    something(); 
} 

Como usted ha encontrado el "es" enfoque requiere el extra "como" o un yeso, que es la razón el "como" con verificación nula es la forma estándar de hacerlo en mi experiencia.

No veo nada ofensivo sobre este enfoque "como", personalmente no creo que sea más desagradable en el ojo que cualquier otro código.

En cuanto a su pregunta real, ¿por qué la palabra clave is se implementó en términos de la palabra clave as, no tengo idea, pero me gusta el juego de palabras en su pregunta :) Sospecho que ninguna de las dos se implementa en términos de el otro, pero la herramienta (Reflector supongo) que usaste para generar C# de IL interpretó el IL en términos de as.

+0

De acuerdo, cambié el caso de uso más típico a "muy natural". No encuentro nada particularmente ofensivo sobre el cheque nulo, solo encuentro el otro fragmento mucho más bonito :) –

+1

Claro, no pretendo criticar tu punto de vista, pero sí creo que el estilo "como" es más o menos el enfoque estándar. –

9

Bueno, la instrucción IL que está disponible (isinst) devolverá un objeto del tipo apropiado, o nulo si tal conversión no es posible. Y no lanza una excepción si la conversión no es posible.

Dado que, tanto "es" como "como" son triviales de implementar. No afirmaría que "es" se implementa como "como" en este caso, solo que la instrucción IL subyacente permite que ambas ocurran. Ahora, por qué el compilador no puede optimizar el "es" seguido de "como" en una única llamada directa, eso es otro asunto. Probablemente, en este caso, que está relacionado con el alcance variable (a pesar de que por el momento se trata de IL, el alcance no existe realmente)

Edición

Pensándolo bien, no se puede optimizar "se "seguido de" como "en una sola invocación, sin saber que la variable en discusión no está sujeta a actualización desde otros subprocesos.

Suponiendo que x es una cadena:

//Thread1 
if(x is string) 

//Thread2 
x = new ComplexObject(); 

//Thread1 
    y = x as string 

Aquí, y debe ser nulo.

+1

Esto es realmente una razón. ¡Gracias! :) –

+2

Siempre que la variable no sea volátil y no haya barreras entre las operaciones is y as, no hay razón para no reutilizar la primera instancia. – erikkallen

+0

@erikkallen - No voy a estar en desacuerdo. Creo que sería una optimización inusualmente específica, especialmente con el uso "normal" de "es" y "como" –

-1

tengo la fuerte sospecha de que es es más rápido que como y no requiere una asignación. Entonces, si x rara vez es Bar, entonces el primer fragmento es bueno. Si x es mayoritariamente Bar, entonces se recomienda como, ya que no se requiere un segundo molde. Depende del uso y las circunstancias del código.

+0

¿quién "haría" una asignación? el puntero que devuelve estará en la pila ya que un puntero es un tipo de ** valor **. –

+0

'as' no requiere una asignación, si el objeto bajo escrutinio implementa las características necesarias (es decir, implementa interfaz o es una subclase de) se devuelve el puntero, sino se devuelve el puntero nulo. – flindeberg

1

De acuerdo con la publicación de blog How Many Passes? de Eric Lippert que es un pase de compilación. Para citar:

corremos un paso de optimización que reescribe trivial "es" y "como" operadores.

Quizás sea por eso que está viendo el mismo CIL generado para ambos fragmentos.

+0

Enlace de interés, gracias. –

1

Se puede escribir el código de ahora como

DoIfOfType<Bar>(possibleBar, b => b.something()) 

Que yo diría que era un poco más claro, pero no tan rápido y sin magia real del compilador.

0

El alcance de 'y' se reduce si coloca la declaración dentro del ciclo.

Quien lo haya escrito probablemente prefiera lanzar 'x como T' más que '(T) x', y quería limitar el alcance de 'y'.

0

Ha olvidado los tipos de valores. Por ejemplo:

static void Main(string[] args) 
    { 
     ValueType vt; 
     FooClass f = vt as FooClass; 

    } 

    private class FooClass 
    { 
     public int Bar { get; set; } 
    } 

No se compilará ya que los tipos de valores no se pueden convertir de esta manera.

+0

No entiendo cómo esto es relevante aquí. Por supuesto, estoy hablando de dónde se puede usar ... ¿importa elaborarlo? –

+0

Importante porque es la razón por la que no puede deshacerse del operador 'is'. –

Cuestiones relacionadas