Después de haber escrito más de un artículo sobre
Veeam Academy , decidimos abrir una pequeña cocina interna y ofrecerle algunos ejemplos en C # que estamos analizando con nuestros estudiantes. Al compilarlos, nos guiamos por el hecho de que nuestra audiencia es desarrolladores novatos, pero también puede ser interesante para los programadores experimentados mirar debajo del gato. Nuestro objetivo es mostrar qué tan profundo es el agujero del conejo, al mismo tiempo que explicamos las características de la estructura interna de C #.
Por otro lado, nos complacerá escuchar comentarios de colegas experimentados que señalarán fallas en nuestros ejemplos o compartirán los suyos. A ellos les gusta usar esas preguntas en las entrevistas, así que seguro que todos tenemos algo que contar.
Esperamos que nuestra selección le sea útil, le ayude a actualizar sus conocimientos o simplemente sonríe.

Ejemplo 1
Estructuras en C #. Con ellos, incluso los desarrolladores experimentados a menudo tienen preguntas, que a menudo utilizan todo tipo de pruebas en línea.
Nuestro primer ejemplo es un ejemplo de atención plena y conocimiento de en qué se expande el bloque de uso. Y también es un tema de comunicación durante la entrevista.
Considera el código:
public struct SDummy : IDisposable { private bool _dispose; public void Dispose() { _dispose = true; } public bool GetDispose() { return _dispose; } static void Main(string[] args) { var d = new SDummy(); using (d) { Console.WriteLine(d.GetDispose()); } Console.WriteLine(d.GetDispose()); } }
¿Qué imprimirá el método Main en la consola?Tenga en cuenta que SDummy es una estructura que implementa la interfaz IDisposable, de modo que las variables del tipo SDummy se pueden usar en el bloque de uso.
De acuerdo
con la especificación del lenguaje C #, el uso de instrucciones para tipos significativos en tiempo de compilación se expande en un bloque try-finally:
try { Console.WriteLine(d.GetDispose()); } finally { ((IDisposable)d).Dispose(); }
Entonces, en nuestro código, el método GetDispose () se llama dentro del bloque de uso, que devuelve el campo booleano _dispose, cuyo valor aún no se ha establecido para el objeto d (solo se establece en el método Dispose (), que aún no se ha llamado) y, por lo tanto, se devuelve el valor El valor predeterminado es False. Que sigue
Y luego lo más interesante.
Corriendo una línea en un bloque finalmente
((IDisposable)d).Dispose();
normalmente conduce al boxeo. Esto no es difícil de ver, por ejemplo,
aquí (en la parte superior derecha en Resultados, primero seleccione C # y luego IL):
En este caso, el método Dispose ya está llamado para otro objeto, y no para el objeto d en absoluto.
Ejecute nuestro programa y vea que el programa realmente muestra "False False" en la consola. ¿Pero es así de simple? :)
De hecho, NO ESTÁ PASANDO NINGÚN EMBALAJE. Lo que, según Eric Lippert, se hace en aras de la optimización (ver
aquí y
aquí ).
Pero, si no hay empaque (lo que en sí mismo puede parecer sorprendente), ¿por qué aparece "False False" y no "False True" en la pantalla, porque Dispose ahora debería aplicarse al mismo objeto?!?
Y aquí no a eso!
Eche un vistazo a lo que el
compilador de C # expande nuestro programa en:
public struct SDummy : IDisposable { private bool _dispose; public void Dispose() { _dispose = true; } public bool GetDispose() { return _dispose; } private static void Main(string[] args) { SDummy sDummy = default(SDummy); SDummy sDummy2 = sDummy; try { Console.WriteLine(sDummy.GetDispose()); } finally { ((IDisposable)sDummy2).Dispose(); } Console.WriteLine(sDummy.GetDispose()); } }
¡Hay una nueva variable sDummy2, a la que se aplica el método Dispose ()!
¿De dónde viene esta variable oculta?
Pasemos a las
especificaciones nuevamente :
Una declaración de uso de la forma 'declaración de uso (expresión)' tiene las mismas tres expansiones posibles. En este caso, ResourceType es implícitamente el tipo de tiempo de compilación de la expresión ... La variable 'recurso' es inaccesible e invisible para la instrucción incrustada.
T.O. la variable sDummy es invisible e inaccesible a la declaración incrustada del bloque using, y todas las operaciones dentro de esta expresión se realizan con otra variable sDummy2.
Como resultado, el método Main emite a la consola "False False" y no "False True", como creen muchos de los que encontraron este ejemplo por primera vez. En este caso, asegúrese de tener en cuenta que no hay empaquetado, sino que se crea una variable oculta adicional.
La conclusión general es esta: los tipos de valores mutables son malvados y es mejor evitarlos.
Un ejemplo similar se considera
aquí . Si el tema es interesante, recomendamos un vistazo.
Me gustaría agradecer especialmente a
SergeyT por sus valiosos comentarios sobre este ejemplo.
Ejemplo 2
Los constructores y la secuencia de sus llamadas es uno de los temas principales de cualquier lenguaje de programación orientado a objetos. A veces, tal secuencia de llamadas puede sorprender y, lo que es peor, incluso "llenar" el programa en el momento más inesperado.
Entonces, considere la clase MyLogger:
class MyLogger { static MyLogger innerInstance = new MyLogger(); static MyLogger() { Console.WriteLine("Static Logger Constructor"); } private MyLogger() { Console.WriteLine("Instance Logger Constructor"); } public static MyLogger Instance { get { return innerInstance; } } }
Supongamos que esta clase tiene cierta lógica empresarial que necesitamos para admitir el registro (la funcionalidad no es tan importante en este momento).
Veamos qué hay en nuestra clase MyLogger:
- Constructor estático especificado
- Hay un constructor privado sin parámetros.
- Variable estática cerrada innerInstance definido
- Y existe una propiedad estática abierta de Instance para comunicarse con el mundo exterior
Para facilitar el análisis de este ejemplo, agregamos una salida de consola simple a los constructores de la clase.
Fuera de la clase (sin usar trucos como la reflexión) solo podemos usar la propiedad de instancia estática pública, que podemos llamar así:
class Program { public static void Main() { var logger = MyLogger.Instance; } }
¿Qué generará este programa?Todos sabemos que se llama a un constructor estático antes de acceder a cualquier miembro de la clase (con la excepción de las constantes). En este caso, se inicia solo una vez dentro del dominio de la aplicación.
En nuestro caso, recurrimos al miembro de la clase: la propiedad Instance, que debería hacer que el constructor estático se inicie primero, y luego se llamará al constructor de la instancia de clase. Es decir el programa generará:
Constructor de registrador estático
Constructor de registrador de instancias
Sin embargo, después de iniciar el programa, llegamos a la consola:
Constructor de registrador de instancias
Constructor de registrador estático
¿Cómo es eso? El constructor de instancias funcionó antes que el constructor estático?!?
Respuesta: sí!
Y aquí está el por qué.
El estándar C # ECMA-334 establece lo siguiente para clases estáticas:
17.4.5.1: “Si existe un constructor estático (§17.11) en la clase, la ejecución de los inicializadores de campo estático ocurre inmediatamente antes de ejecutar ese constructor estático.
...
17.11: ... Si una clase contiene campos estáticos con inicializadores, esos inicializadores se ejecutan en orden de texto inmediatamente antes de ejecutar el constructor estático.(Lo que en una traducción libre significa: si hay un constructor estático en la clase, entonces la inicialización de los campos estáticos comienza inmediatamente ANTES de que comience el constructor estático.
...
Si la clase contiene campos estáticos con inicializadores, dichos inicializadores se inician en el orden en el texto del programa ANTES de ejecutar el constructor estático).
En nuestro caso, el campo estático innerInstance se declara junto con el inicializador, que es el constructor de la instancia de clase. Según el estándar ECMA, el inicializador debe llamarse ANTES de llamar al constructor estático. Lo que sucede en nuestro programa: el constructor de instancias, que es el inicializador del campo estático, se llama ANTES del constructor estático. De acuerdo, inesperadamente.
Tenga en cuenta que esto solo es cierto para los inicializadores de campo estático. En general, un constructor estático se llama ANTES de llamar al constructor de la instancia de clase.
Como, por ejemplo, aquí:
class MyLogger { static MyLogger() { Console.WriteLine("Static Logger Constructor"); } public MyLogger() { Console.WriteLine("Instance Logger Constructor"); } } class Program { public static void Main() { var logger = new MyLogger(); } }
Y se espera que el programa salga a la consola:
Constructor de registrador estático
Constructor de registrador de instancias

Ejemplo 3
Los programadores a menudo tienen que escribir funciones auxiliares (utilidades, ayudantes, etc.) para facilitarles la vida. Típicamente, tales funciones son bastante simples y a menudo solo requieren unas pocas líneas de código. Pero puedes tropezar incluso de la nada.
Supongamos que necesitamos implementar una función que verifique que el número sea impar (es decir, que el número no es divisible por 2 sin resto).
Una implementación podría verse así:
static bool isOddNumber(int i) { return (i % 2 == 1); }
A primera vista, todo está bien y, por ejemplo, para los números 5.7 y 11, esperamos obtener True.
¿Qué devolverá la función isOddNumber (-5)?-5 es un número impar, pero como respuesta a nuestra función, ¡obtenemos Falso!
Vamos a descubrir cuál es la razón.
Según
MSDN , lo siguiente está escrito sobre el resto del operador% division:
"Para operandos enteros, el resultado de a% b es el valor producido por a - (a / b) * b"
En nuestro caso, para a = -5, b = 2 obtenemos:
-5% 2 = (-5) - ((-5) / 2) * 2 = -5 + 4 = -1
Pero -1 no siempre es igual a 1, lo que explica nuestro resultado False.
El operador% es sensible al signo de los operandos. Por lo tanto, para no recibir tales "sorpresas", es mejor comparar el resultado con cero, que no tiene signo:
static bool isOddNumber(int i) { return (i % 2 != 0); }
O obtenga una función separada para verificar la paridad e implementar la lógica a través de ella:
static bool isEvenNumber(int i) { return (i % 2 == 0); } static bool isOddNumber(int i) { return !isEvenNumber(i); }
Ejemplo 4
Todos los que programaron en C #, probablemente se reunieron con LINQ, que es muy conveniente para trabajar con colecciones, crear consultas, filtrar y agregar datos ...
No miraremos bajo el capó de LINQ. Tal vez lo haremos en otro momento.
Mientras tanto, considere un pequeño ejemplo:
int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; int summResult = 0; var selectedData = dataArray.Select( x => { summResult += x; return x; }); Console.WriteLine(summResult);
¿Qué generará este código?Obtenemos en pantalla el valor de la variable summResult, que es igual al valor inicial, es decir 0.
¿Por qué sucedió esto?
Y debido a que la definición de una consulta LINQ y el inicio de esta consulta son dos operaciones que se realizan por separado. Por lo tanto, la definición de una solicitud no significa su lanzamiento / ejecución.
La variable summResult se usa dentro de un delegado anónimo en el método Select: los elementos de la matriz dataArray se ordenan secuencialmente y se agregan a la variable summResult.
Podemos suponer que nuestro código imprimirá la suma de los elementos de la matriz dataArray. Pero LINQ no funciona de esa manera.
Considere la variable selectedData. La palabra clave var es "azúcar sintáctico", que en muchos casos reduce el tamaño del código del programa y mejora su legibilidad. Y el tipo real de la variable selectedData implementa la interfaz IEnumerable. Es decir nuestro código se ve así:
IEnumerable<int> selectedData = dataArray.Select( x => { summResult += x; return x; });
Aquí definimos la consulta (Consulta), pero la consulta en sí no se inicia. De manera similar, puede trabajar con la base de datos especificando la consulta SQL como una cadena, pero para obtener el resultado, consulte la base de datos y ejecute esta consulta explícitamente.
Es decir, hasta ahora solo hemos establecido una solicitud, pero no la hemos lanzado. Es por eso que el valor de la variable summResult permanece sin cambios. Se puede iniciar una consulta, por ejemplo, utilizando los métodos ToArray, ToList o ToDictionary:
int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; int summResult = 0;
Este código ya mostrará el valor de la variable summResult, igual a la suma de todos los elementos de la matriz dataArray, igual a 15.
Lo descubrimos. Y entonces, ¿qué mostrará este programa en la pantalla?
int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 };
La variable groupedData (línea 3) en realidad implementa la interfaz IEnumerable y esencialmente define la solicitud al origen de datos dataArray. Esto significa que para que un delegado anónimo funcione, lo que cambia el valor de la variable summResult, esta solicitud debe ejecutarse explícitamente. Pero no hay tal lanzamiento en nuestro programa. Por lo tanto, el valor de la variable summResult se cambiará solo en la línea 2, y no podemos considerar todo lo demás en nuestros cálculos.
Entonces es fácil calcular el valor de la variable summResult, que es, respectivamente, 15 + 7, es decir 22)
Ejemplo 5
Digamos de inmediato: no consideramos este ejemplo en nuestras conferencias en la Academia, pero a veces lo discutimos durante las pausas para el café en lugar de una broma.
A pesar de que apenas es indicativo desde el punto de vista de determinar el nivel del desarrollador, encontramos este ejemplo en varias pruebas diferentes. Quizás se usa por versatilidad, porque funciona igual en C y C ++, así como en C # y Java.
Entonces, que haya una línea de código:
int i = (int)+(char)-(int)+(long)-1;
¿Cuál será el valor de la variable i?Respuesta: 1
Puede pensar que aquí se usa la aritmética numérica sobre los tamaños de cada tipo en bytes, ya que los signos "+" y "-" se encuentran de forma inesperada aquí para la conversión de tipos.
En C #, se sabe que el tipo entero tiene 4 bytes de longitud, 8 de longitud, char 2.
Entonces es fácil pensar que nuestra línea de código será equivalente a la siguiente expresión aritmética:
int i = (4)+(2)-(4)+(8)-1;
Sin embargo, esto no es así. Y para confundir y dirigir con un razonamiento tan falso, el ejemplo se puede cambiar, por ejemplo, de esta manera:
int i = (int)+(char)-(int)+(long)-sizeof(int);
Los signos "+" y "-" se utilizan en este ejemplo no como operaciones aritméticas binarias, sino como operadores unarios. Entonces, nuestra línea de código es solo una secuencia de conversiones de tipo explícito mezcladas con llamadas a operaciones unarias, que se pueden escribir de la siguiente manera:
int i = (int)(

¿Interesado en aprender en Veeam Academy?
Ahora hay un conjunto para la primavera intensiva en C # en San Petersburgo, e invitamos a todos a someterse a pruebas en línea en el sitio web de
Veeam Academy.El curso comienza el 18 de febrero de 2019, se extiende hasta mediados de mayo y, como siempre, será completamente gratuito. El registro para cualquier persona que quiera someterse a pruebas de ingreso ya está disponible en el sitio web de la Academia:
academy.veeam.ru