with visual studio started net mundo hola guide guia getting application c# .net compiler-construction il

visual - guia de c#



¿Por qué MSFT C#compila de forma diferente una "matriz fija a la desintegración del puntero" y una "dirección del primer elemento"? (2)

El compilador .NET c # (.NET 4.0) compila la instrucción fixed de una manera bastante peculiar.

Aquí hay un programa breve pero completo para mostrarle de lo que estoy hablando.

using System; public static class FixedExample { public static void Main() { byte [] nonempty = new byte[1] {42}; byte [] empty = new byte[0]; Good(nonempty); Bad(nonempty); try { Good(empty); } catch (Exception e){ Console.WriteLine(e.ToString()); /* continue with next example */ } Console.WriteLine(); try { Bad(empty); } catch (Exception e){ Console.WriteLine(e.ToString()); /* continue with next example */ } } public static void Good(byte[] buffer) { unsafe { fixed (byte * p = &buffer[0]) { Console.WriteLine(*p); } } } public static void Bad(byte[] buffer) { unsafe { fixed (byte * p = buffer) { Console.WriteLine(*p); } } } }

Compile con "csc.exe FixedExample.cs / unsafe / o +" si desea seguir.

Aquí está la IL generada para el método Good :

Bueno()

.maxstack 2 .locals init (uint8& pinned V_0) IL_0000: ldarg.0 IL_0001: ldc.i4.0 IL_0002: ldelema [mscorlib]System.Byte IL_0007: stloc.0 IL_0008: ldloc.0 IL_0009: conv.i IL_000a: ldind.u1 IL_000b: call void [mscorlib]System.Console::WriteLine(int32) IL_0010: ldc.i4.0 IL_0011: conv.u IL_0012: stloc.0 IL_0013: ret

Aquí está la IL generada para el método Bad :

Malo()

.locals init (uint8& pinned V_0, uint8[] V_1) IL_0000: ldarg.0 IL_0001: dup IL_0002: stloc.1 IL_0003: brfalse.s IL_000a IL_0005: ldloc.1 IL_0006: ldlen IL_0007: conv.i4 IL_0008: brtrue.s IL_000f IL_000a: ldc.i4.0 IL_000b: conv.u IL_000c: stloc.0 IL_000d: br.s IL_0017 IL_000f: ldloc.1 IL_0010: ldc.i4.0 IL_0011: ldelema [mscorlib]System.Byte IL_0016: stloc.0 IL_0017: ldloc.0 IL_0018: conv.i IL_0019: ldind.u1 IL_001a: call void [mscorlib]System.Console::WriteLine(int32) IL_001f: ldc.i4.0 IL_0020: conv.u IL_0021: stloc.0 IL_0022: ret

Esto es lo Good :

  1. Obtener la dirección del búfer [0].
  2. Desreferencia esa dirección.
  3. Llame a WriteLine con ese valor desreferenciado.

Esto es lo que hace ''Malo'':

  1. Si el buffer es nulo, GOTO 3.
  2. Si buffer.Length! = 0, GOTO 5.
  3. Almacene el valor 0 en el slot local 0,
  4. GOTO 6.
  5. Obtener la dirección del búfer [0].
  6. Deferencia esa dirección (en el slot local 0, que puede ser 0 o buffer ahora).
  7. Llame a WriteLine con ese valor desreferenciado.

Cuando el buffer es nulo ni está vacío, estas dos funciones hacen lo mismo. Tenga en cuenta que Bad solo salta algunos aros antes de llegar a la WriteLine función WriteLine .

Cuando el buffer es nulo, Good lanza una NullReferenceException en el declarador de puntero fijo ( byte * p = &buffer[0] ). Presumiblemente, este es el comportamiento deseado para la fijación de una matriz administrada, ya que, en general, cualquier operación dentro de una declaración fija dependerá de la validez del objeto que se está reparando. De lo contrario, ¿por qué ese código estaría dentro del bloque fixed ? Cuando Good pasa una referencia nula, falla inmediatamente al comienzo del bloque fixed , proporcionando un seguimiento de pila relevante e informativo. El desarrollador verá esto y se dará cuenta de que debe validar el buffer antes de usarlo, o tal vez su lógica asigna incorrectamente null al buffer . De cualquier forma, ingresar claramente en un bloque fixed con una matriz administrada null no es deseable.

Bad maneja este caso de manera diferente, incluso indeseablemente. Puede ver que Bad no lanza una excepción hasta que p se desreferencia. Lo hace en la forma indirecta de asignar null al mismo espacio local que contiene p , y luego lanzar la excepción cuando el bloque fixed desreferencia p .

Manejar null esta manera tiene la ventaja de mantener constante el modelo de objetos en C #. Es decir, dentro del bloque fixed , p se trata semánticamente como una especie de "puntero a una matriz administrada" que, cuando no es nula, no causará problemas hasta que (o a menos) se elimine la referencia. La consistencia está muy bien, pero el problema es que p no es un puntero a una matriz administrada . Es un puntero al primer elemento del buffer , y cualquiera que haya escrito este código ( Bad ) interpretaría su significado semántico como tal. No puede obtener el tamaño del buffer desde p , y no puede llamar a p.ToString() , entonces ¿por qué tratarlo como si fuera un objeto? En los casos en que el buffer es nulo, claramente hay un error de codificación, y creo que sería mucho más útil si Bad lanzara una excepción en el declarador de puntero fijo , en lugar de dentro del método.

Entonces parece que Good maneja null mejor que Bad . ¿Qué hay de los buffers vacíos?

Cuando el buffer tiene la Longitud 0, Good lanza IndexOutOfRangeException en el declarador de puntero fijo . Parece una forma completamente razonable de manejar el acceso a la matriz fuera de límites. Después de todo, el código &buffer[0] deberían tratarse del mismo modo que &(buffer[0]) , que obviamente debería lanzar IndexOutOfRangeException .

Bad maneja este caso de manera diferente, y de nuevo indeseablemente. Tal como sería el caso si el buffer fuera null , cuando buffer.Length == 0 , Bad no lanza una excepción hasta que p se desreferencia, y en ese momento arroja NullReferenceException, ¡no IndexOutOfRangeException! Si p nunca se desreferencia, entonces el código ni siquiera lanza una excepción. Nuevamente, parece que la idea aquí es dar p el significado semántico de "puntero a una matriz administrada". Una vez más, no creo que nadie que escriba este código piense en p esa manera. El código sería mucho más útil si lanzara IndexOutOfRangeException en el declarador de puntero fijo , notificando así al desarrollador que el arreglo pasado estaba vacío, y no null .

Parece que fixed(byte * p = buffer) debería haberse compilado con el mismo código que se fixed (byte * p = &buffer[0]) . Observe también que aunque el buffer podría haber sido cualquier expresión arbitraria, su tipo ( byte[] ) es conocido en tiempo de compilación y, por lo tanto, el código en Good funcionaría para cualquier expresión arbitraria.

Editar

De hecho, observe que la implementación de Bad hace el error comprobando en el buffer[0] dos veces . Lo hace explícitamente al comienzo del método, y luego lo hace de nuevo implícitamente en la instrucción ldelema .

Entonces vemos que lo Good y lo Bad son semánticamente diferentes. Bad es más largo, probablemente más lento, y ciertamente no nos da excepciones deseables cuando tenemos errores en nuestro código, e incluso falla mucho más tarde de lo que debería en algunos casos.

Para los curiosos, la sección 18.6 de la especificación (C # 4.0) dice que el comportamiento es "Implementado-definido" en ambos casos de falla:

Un inicializador de puntero fijo puede ser uno de los siguientes:

• El token "&" seguido de una variable-referencia (§5.3.3) a una variable móvil (§18.3) de un tipo no gestionado T, siempre que el tipo T * sea implícitamente convertible al tipo de puntero dado en el enunciado fijo. En este caso, el inicializador calcula la dirección de la variable dada, y se garantiza que la variable permanecerá en una dirección fija durante la vigencia de la declaración fija.

• Una expresión de un tipo de matriz con elementos de un tipo no gestionado T, siempre que el tipo T * sea implícitamente convertible al tipo de puntero proporcionado en la instrucción fija. En este caso, el inicializador calcula la dirección del primer elemento en la matriz y se garantiza que toda la matriz permanecerá en una dirección fija durante la vigencia de la declaración fija. El comportamiento de la sentencia fija es definido por la implementación si la expresión de la matriz es nula o si la matriz tiene cero elementos.

... otros casos ...

El último punto, la documentación de MSDN sugiere que los dos son "equivalentes":

// Las siguientes dos asignaciones son equivalentes ...

fijo (doble * p = arr) {/ ... /}

fijo (doble * p = & arr [0]) {/ ... /}

Si se supone que los dos son "equivalentes", ¿por qué utilizar una semántica de manejo de errores diferente para la declaración anterior?

También parece que se hizo un esfuerzo extra al escribir las rutas de código generadas en Bad . El código compilado en Good funciona bien para todos los casos de falla, y es el mismo que el código en Bad en casos de no falla. ¿Por qué implementar nuevas rutas de código en lugar de simplemente usar el código más simple generado para Good ?

¿Por qué se implementa de esta manera?


Entonces vemos que lo bueno y lo malo son semánticamente diferentes. ¿Por qué?

Porque Good es el caso 1 y malo es el caso 2.

Good no asigna una "An expression of a array-type". Asigna "El token" y "seguido por una referencia de variable", por lo que es el caso 1. Bad asigna "Una expresión de un tipo de matriz", por lo que es el caso 2. Si esto es cierto, la documentación de MSDN es incorrecta.

En cualquier caso, esto explica por qué el compilador C # crea dos patrones de código diferentes (y en el segundo caso especializado).

¿Por qué el caso 1 genera un código tan simple? Estoy especulando aquí: tomar la dirección de un elemento de matriz probablemente se compila de la misma manera que usar array[index] en una expresión de ref . En el nivel CLR, los parámetros y las expresiones de ref son solo punteros administrados. También lo es la expresión &array[index] : se compila a un puntero administrado que no está anclado sino "interior" (este término proviene de C ++ administrado, creo). El GC lo arregla automáticamente. Se comporta como una referencia de objeto normal.

Así que el caso 1 obtiene el tratamiento de puntero administrado habitual, mientras que el caso 2 obtiene un comportamiento especial definido por la implementación (no indefinido).

Esto no responde a todas sus preguntas, pero al menos proporciona algunas razones para sus observaciones. Estoy esperando que Eric Lippert agregue su respuesta como alguien interno.


Es posible que hayas notado que el código IL que incluiste implementa la especificación casi línea por línea. Esto incluye la implementación explícita de los dos casos de excepción enumerados en la especificación en el caso en que son relevantes, y no incluye el código en el caso en que no lo son. Entonces, la razón más simple por la que el compilador se comporta de la manera en que lo hace es "porque la especificación así lo dice".

Por supuesto, eso solo lleva a dos preguntas más que podríamos preguntar:

  • ¿Por qué el grupo de idiomas C # eligió escribir las especificaciones de esta manera?
  • ¿Por qué el equipo del compilador eligió ese comportamiento específico definido por la implementación?

A menos que aparezca alguien de los equipos apropiados, no podemos esperar contestar ninguna de esas preguntas por completo. Sin embargo, podemos intentar responder al segundo tratando de seguir su razonamiento.

Recuerde que la especificación dice que, en el caso de suministrar una matriz a un inicializador de puntero fijo , ese

El comportamiento de la sentencia fija es definido por la implementación si la expresión de la matriz es nula o si la matriz tiene cero elementos.

Dado que la implementación es libre de elegir hacer lo que quiera en este caso, podemos suponer que será el comportamiento razonable más fácil y más barato para el equipo del compilador.

En este caso, lo que el equipo del compilador eligió hacer fue " lanzar una excepción en el punto donde su código hace algo mal ". Considere lo que el código estaría haciendo si no estuviera dentro de un inicializador de puntero fijo y piense en qué más está sucediendo. En su ejemplo "Bueno", está intentando tomar la dirección de un objeto que no existe: el primer elemento en una matriz nula / vacía. Eso no es algo que realmente puedas hacer, por lo que producirá una excepción. En su ejemplo "Malo", simplemente está asignando la dirección de un parámetro a una variable de puntero; byte * p = null es una declaración perfectamente legítima. Solo cuando intenta WriteLine(*p) en WriteLine(*p) ocurre un error. Dado que el inicializador de puntero fijo puede hacer lo que quiera en este caso de excepción, lo más simple es permitir que la asignación se realice, sin sentido.

Claramente, las dos declaraciones no son precisamente equivalentes. Podemos decir esto por el hecho de que el estándar los trata de manera diferente:

  • &arr[0] es: "El token" y "seguido de una referencia de variable", por lo que el compilador calcula la dirección de arr [0]
  • arr es: "Una expresión de un tipo de matriz", y así el compilador calcula la dirección del primer elemento de la matriz, con la advertencia de que una matriz nula o de longitud 0 produce el comportamiento definido por la implementación que está viendo.

Los dos producen resultados equivalentes, siempre que haya un elemento en la matriz, que es el punto que la documentación de MSDN está tratando de transmitir. Hacer preguntas sobre por qué un comportamiento explícitamente indefinido o definido por la implementación actúa de la manera en que lo hace no lo ayudará realmente a resolver ningún problema en particular, porque no puede confiar en que sea cierto en el futuro. (Habiendo dicho eso, por supuesto tengo curiosidad por saber cuál fue el proceso de pensamiento, ya que obviamente no se puede "arreglar" un valor nulo en la memoria ...)