La intención de esta guía es realizar un detallado repaso por todas las novedades que han ido aportando cada una de las diferentes versiones que forman parte de C# 7, estas son:
Nos iremos parando en cada una de ellas, analizando con detalle y con ejemplos todas estas novedades de este lenguaje de programación orientado a objetos desarrollado por Microsoft. ¿Te apuntas?
En esta versión se ha mejorado la sintaxis existente que admite parámetros out. Ahora es posible declarar variables out en la lista de argumentos de una llamada a método, en lugar de escribir una instrucción de declaración distinta:
Para mayor claridad, es posible especificar el tipo de la variable out, tal y como se muestra anteriormente. Pero el lenguaje admite el uso de una variable local con tipo implícito:
Tuplas Las tuplas estaban disponibles antes de C# 7.0, pero no eran eficientes ni compatibles con ningún lenguaje. Esto significaba que solo se podía hacer referencia a los elementos tupla como Item1, Item2, por ejemplo. Ahora existe compatibilidad de lenguaje con las tuplas, que permite usar nombres semánticos en los campos de una tupla mediante tipos de tupla nuevos y más eficientes.
Es posible crear una tupla asignando un valor a cada miembro, y, opcionalmente, proporcionando nombres semánticos a cada uno de los miembros de la tupla:
La tupla namedLetters contiene campos denominados Alpha y Beta. Esos nombres solo existen en tiempo de compilación y no se conservan, por ejemplo, al inspeccionar la tupla mediante la reflexión en tiempo de ejecución.
En la asignación de una tupla, también pueden especificarse los nombres de los campos a la derecha de la asignación:
Un descarte es una variable de solo escritura con el nombre “_”. Es posible asignar todos los valores que queramos descartar a una única variable. Un descarte se parece a una variable no asignada, aunque no puede usarse en el código (excepto la instrucción de asignación).
Los descartes se admiten en los escenarios siguientes:
Por ejemplo, la siguiente llamada al método devuelve una tupla de tres donde el primer y el segundo valor se descartan y area es una variable declarada previamente para establecerse en el tercer componente correspondiente devuelto por GetCityInformation:
La coincidencia de patrones es una característica que permite implementar la distribución de métodos en propiedades distintas al tipo de un objeto.
Las clases base y derivadas proporcionan distintas implementaciones. Las expresiones de coincidencia de patrones extienden este concepto para que se puedan implementar fácilmente patrones de distribución similares para tipos y elementos de datos que no se relacionan mediante una jerarquía de herencia.
La coincidencia de patrones admite expresiones is y switch. Cada una de ellas habilita la inspección de un objeto y sus propiedades para determinar si el objeto cumple el patrón buscado. La palabra clave when se usa para especificar reglas adicionales para el patrón.
La instrucción switch actualizada tiene varias construcciones nuevas:
Esta característica habilita algoritmos que usan y devuelven referencias a variables definidas en otro lugar. Por ejemplo, trabajar con matrices de gran tamaño y buscar una sola ubicación con determinadas características.
El método siguiente devuelve una referencia a ese almacenamiento en la matriz:
Se puede declarar el valor devuelto como un elemento ref y modificar ese valor en la matriz, como se muestra en el código siguiente:
public static ref int Find(int[,] matrix, Func<int, bool> predicate)
{
for (int i = 0; i < matrix.GetLength(0); i++)
for (int j = 0; j < matrix.GetLength(1); j++)
if (predicate(matrix[i, j]))
return ref matrix[i, j];
throw new InvalidOperationException("Not found");
}
El lenguaje C# tiene varias reglas que impiden el uso incorrecto de las variables locales y devoluciones de ref:
Las funciones locales permiten declarar métodos en el contexto de otro método y facilitan que los lectores de la clase vean que el método local solo se llama desde el contexto en el que se declara.
Hay dos casos de uso comunes para las funciones locales:
Ambos tipos de métodos generan código que informa de errores más tarde de lo que los programadores podrían esperar. En los métodos de iterador, las excepciones solo se observan al llamar a código que enumera la secuencia devuelta. En los métodos asincrónicos, las excepciones solo se observan cuando se espera al elemento Task devuelto. En el ejemplo siguiente se muestra la separación de la validación de parámetros de la implementación de iteradores mediante una función local:
La misma técnica se puede emplear con métodos async para asegurarse de que las excepciones derivadas de la validación de argumentos se inician antes de comenzar el trabajo asincrónico:
En C# 6 se presentaron los miembros con forma de expresión para funciones de miembros y propiedades de solo lectura. C# 7.0 amplía los miembros permitidos que pueden implementarse como expresiones, y además se pueden implementar constructores, finalizadores y descriptores de acceso get y set en propiedades e indizadores.
En el código siguiente se muestran ejemplos de cada uno:
En C#, throw siempre ha sido una instrucción en vez de una expresión. Por lo tanto había construcciones de C# en las que no se podía usar. Esto incluye expresiones condicionales, expresiones de fusión nulas y algunas expresiones lambda.
La incorporación de miembros con forma de expresión agrega más ubicaciones donde las expresiones throw resultarían útiles. Para que se pueda escribir cualquiera de estas construcciones, C# 7.0 presenta las expresiones throw.
Esta adición facilita la escritura de código más basado en expresiones. No se necesitan instrucciones adicionales para la comprobación de errores.
La devolución de un objeto Task desde métodos asincrónicos puede presentar cuellos de botella de rendimiento en determinadas rutas de acceso. Task es un tipo de referencia, por lo que su uso implica la asignación de un objeto. En los casos en los que un método declarado con el modificador async devuelva un resultado en caché o se complete sincrónicamente, las asignaciones adicionales pueden resultar costosas si se producen en bucles ajustados.
La nueva característica de lenguaje implica que los tipos de valor devuelto de métodos asincrónicos no están limitados a Task, Task
La lectura incorrecta de constantes numéricas puede complicar la comprensión del código cuando se lee por primera vez. En C# 7.0 se incluyen dos nuevas características para escribir números de la manera más legible para el uso previsto: literales binarios y separadores de dígitos.
En aquellos casos en que cree máscaras de bits, o siempre que una representación binaria de un número aumente la legibilidad del código, escriba el número en formato binario:
El 0b al principio de la constante indica que el número está escrito como número binario. Los números binarios pueden ser muy largos, por lo que a menudo resulta más fácil ver los patrones de bits si se introduce “_” como separador de dígitos, como se ha mostrado antes en la constante binaria.
El separador de dígitos puede aparecer en cualquier parte de la constante. En números de base 10, es habitual usarlo como separador de miles:
El separador de dígitos también se puede usar con tipos decimal, float y double:
Es la primera versión secundaria del lenguaje C#. Incluye la posibilidad de configurar el compilador para que coincida con una versión especificada del lenguaje, lo que permite aislar la decisión de actualizar las herramientas de la decisión de actualizar las versiones de lenguaje. Además incorpora el elemento de configuración de selección de versión de lenguaje, tres nuevas características de lenguaje y un nuevo comportamiento del compilador.
Un método async main permite usar await en el método Main. Anteriormente, hubiera sido necesario escribir lo siguiente:
Ahora se puede escribir esto:
Si el programa no devuelve un código de salida, puede declarar un método Main que devuelva una Task:
Las expresiones literales predeterminadas constituyen una mejora con respecto a las expresiones de valor predeterminadas. Estas expresiones inicializan una variable en el valor predeterminado.
Donde anteriormente habría que escribir:
Ahora, se puede pasar por alto el tipo del lado derecho de la inicialización:
Muchas veces, cuando se inicializa una tupla, las variables usadas en el lado derecho de la asignación son las mismas que los nombres que querríamos dar a los elementos de tupla:
Ahora, los nombres de los elementos de tupla se pueden deducir de las variables empleadas para inicializar la tupla.
La expresión de patrón para is y el patrón de tipo switch pueden tener el tipo de un parámetro de tipo genérico. Esto puede ser especialmente útil al comprobar los tipos que pueden ser tipos struct o class y si quiere evitar la conversión boxing.
Existen dos nuevas opciones del compilador con las que se generan ensamblados solo de referencia:
La opción -refout especifica una ruta de archivo donde el ensamblado de referencia debe mostrarse. Esto se traduce en metadataPeStream en la API de emisión.
filepathEs la ruta de archivo del ensamblado de referencia. Generalmente debe coincidir con el ensamblado principal.
La convención recomendada es colocar el ensamblado de referencia en una subcarpeta "ref/" con relación al ensamblado principal.
La opción -refonly indica que un ensamblado de referencia debe mostrarse en lugar de un ensamblado de implementación, como el resultado principal. El parámetro -refonly deshabilita de forma automática la generación de archivos PDB, ya que los ensamblados de referencia no pueden ejecutarse.
Los ensamblados de solo metadatos tienen sus cuerpos de métodos reemplazados por un cuerpo throw null único, pero incluyen todos los miembros excepto los tipos anónimos. El motivo de usar cuerpos throw null es que PEVerify pueda ejecutar y pasar (por lo tanto, validar la integridad de los metadatos).
Las características de lenguaje permiten trabajar con tipos de valor usando la semántica de referencia. Están diseñadas para aumentar el rendimiento minimizando la copia de tipos de valor sin usar las asignaciones de memoria asociadas al uso de tipos de referencia.
Las características incluyen:
Las llamadas de método ya pueden usar argumentos con nombre que precedan a argumentos posicionales si están en la posición adecuada.
Con los argumentos con nombre ya no es necesario recordar o buscar el orden de los parámetros de la lista de parámetros de los métodos llamados. El parámetro de cada argumento se puede especificar por nombre de parámetro.
Por ejemplo, se puede llamar de la manera habitual a una función que imprime los detalles de un pedido mediante el envío de argumentos por posición, en el orden definido por la función.
Si no se conoce el orden de los parámetros pero sí sus nombres, se pueden enviar los argumentos en cualquier orden.
Los argumentos con nombre también mejoran la legibilidad del código al identificar lo que cada argumento representa.
En el método de ejemplo siguiente, sellerName no puede ser nulo ni un espacio en blanco. Como sellerName y productName son tipos de cadena, en lugar de enviar argumentos por posición, tiene sentido usar argumentos con nombre para eliminar la ambigüedad entre ambos y evitar confusiones para aquellos que lean el código.
Los argumentos con nombre, cuando se usan con argumentos posicionales, son válidos siempre que:
La implementación de la compatibilidad con separadores de dígitos en C# 7.0 no permitía que “” fuera el primer carácter del valor literal. Los literales numéricos hexadecimales y binarios ya pueden empezar con un “”.
Por ejemplo:
Presentamos un nuevo modificador de acceso compuesto: private protected. Indica que se puede tener acceso a un miembro mediante una clase o clases derivadas declaradas en un mismo ensamblado.
Mientras que protected internal permite el acceso a las clases derivadas o clases que se encuentran en un mismo ensamblado, private protected limita el acceso a los tipos derivados que se declaran en un mismo ensamblado.
La expresión condicional puede producir un resultado de referencia en lugar de un resultado de valor. Por ejemplo, podría escribir lo siguiente para recuperar una referencia al primer elemento en una de dos matrices:
La variable r es una referencia al primer valor de arr o otherArr.
Características:
La escritura de código seguro y no seguro, tiene el mismo rendimiento. El código seguro evita clases de errores, como desbordamientos de búfer, punteros perdidos y otros errores de acceso a la memoria.
Estas nuevas características amplían las capacidades de código seguro comprobable y lo facilitan en gran medida, ya que se puede escribir más código utilizando construcciones seguras.
Teniendo en cuenta esta estructura:
En versiones anteriores de C#, era necesario anclar una variable para acceder a uno de los enteros que forman parte de myFixedField. Ahora, el código siguiente se compila sin anclar la variable p dentro de una instrucción fixed independiente:
La variable p tiene acceso a un elemento en myFixedField. No es necesario declarar otra variable int* independiente. En versiones anteriores de C#, es necesario declarar un segundo puntero fijo:
Ahora, las variables locales de ref pueden reasignarse para hacer referencia a instancias diferentes después de haberse inicializado.
El código siguiente ahora compila:
Cuando inicializamos una matriz podemos especificar los valores para los elementos:
var arr2 = new int[] {1, 2, 3};
Ahora, esa misma sintaxis se puede aplicar a las matrices que se declaran con stackalloc:
La instrucción fixed admite un conjunto limitado de tipos. A partir de C# 7.3, cualquier tipo que contenga un método GetPinnableReference() que devuelve un ref T, o ref readonly T puede ser fixed.
La adición de esta característica significa que fixed puede utilizarse con System.Span
Ahora se puede especificar el tipo System.Enum o System.Delegate como restricciones de clase base para un parámetro de tipo.
También se puede usar la nueva restricción unmanaged para especificar que el parámetro de tipo debe ser un tipo no administrado. Un tipo no administrado es un tipo que no es un tipo de referencia y no contiene ningún tipo de referencia en ningún nivel de anidamiento.
Los tipos de tupla admiten los operadores y !=. Estos operadores funcionan comparando cada uno de los miembros del argumento izquierdo con los miembros del argumento derecho en orden. Estas comparaciones dejarán de evaluar a los miembros en cuanto un par no sea igual. Los siguientes ejemplos de código usan , pero todas las reglas de comparación se aplican a !=.
En el siguiente ejemplo de código se muestra una comparación de igualdad para dos pares de enteros:
Hay varias reglas que hacen que las pruebas de igualdad de tupla sean más prácticas. La igualdad de tupla realiza conversiones elevadas si una de las tuplas es una tupla que admite valores NULL, como se muestra en el siguiente código:
La igualdad de tupla también realiza conversiones implícitas en cada uno de los miembros de ambas tuplas. Entre estas se incluyen conversiones elevadas, conversiones de amplíación u otras conversiones implícitas.
Los nombres de los miembros de las tuplas no participan en las pruebas para la igualdad. Sin embargo, si uno de los operandos es un literal de tupla con nombres explícitos, el compilador genera la advertencia CS8383 si esos nombres no coinciden con los nombres del otro operando.
Cuando ambos operandos son literales de tupla, la advertencia está en el operando derecho como se muestra en el siguiente ejemplo:
Por último, las tuplas pueden contener tuplas anidadas. La igualdad de tupla compara la "forma" de cada operando a través de tuplas anidadas como se muestra en el siguiente ejemplo:
El atributo SomeThingAboutFieldAttribute se aplica al campo de respaldo generado por el compilador para SomeProperty.
Esta sintaxis ahora se admite:
Cuando se agregó el modificador de argumentos in, estos dos métodos causaron una ambigüedad:
Ahora, la sobrecarga por valor es mejor que la de la versión de referencia de solo lectura. Para llamar a la versión con el argumento de referencia de solo lectura, debe incluir el modificador in cuando llame al método.
La sintaxis que se agregó en C# 7.0 para permitir declaraciones de variable out se ha amplíado para incluir inicializadores de campo, inicializadores de propiedad, inicializadores de constructor y cláusulas de consulta.
Esto permite código como el siguiente ejemplo:
En cada versión, las reglas de resolución de sobrecarga se actualizan. Esta versión agrega tres nuevas reglas para ayudar a que el compilador elija la opción obvia:
La opción del compilador -publicsign indica al compilador que firme el ensamblado con una clave pública. El ensamblado se marca como firmado, pero la firma se toma de la clave pública. Esta opción le permite crear ensamblados firmados a partir de proyectos de código abierto utilizando una clave pública.
La opción del compilador -pathmap indica al compilador que reemplace las rutas de acceso de origen del entorno de compilación por rutas de acceso de origen asignadas. La opción -pathmap controla la ruta de acceso de origen escrita por el compilador en los archivos PDB o para CallerFilePathAttribute.