Capítulo 1. Introducción a C# y .NET
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
C# es un lenguaje de programación orientado a objetos, de propósito general y a prueba de tipos. El objetivo del lenguaje es la productividad del programador. Para ello, C# equilibra simplicidad, expresividad y rendimiento. El principal arquitecto del lenguaje desde su primera versión es Anders Hejlsberg (creador de Turbo Pascal y arquitecto de Delphi). El lenguaje C# es neutral con respecto a la plataforma y funciona con una serie de tiempos de ejecución específicos de la plataforma.
Orientación a Objetos
C# es una rica implementación del paradigma de orientación a objetos, que incluye encapsulación, herencia y polimorfismo. Encapsular significa crear un límite alrededor de un objeto para separar su comportamiento externo (público) de sus detalles de implementación internos (privados). A continuación se exponen las características distintivas de C# desde una perspectiva orientada a objetos:
- Sistema de tipos unificado
- El bloque de construcción fundamental en C# es una unidad encapsulada de datos y funciones llamada tipo. C# tiene un sistema de tipos unificado en el que todos los tipos comparten, en última instancia, un tipo base común. Esto significa que todos los tipos, tanto si representan objetos de negocio como si son tipos primitivos como los números, comparten la misma funcionalidad básica. Por ejemplo, una instancia de cualquier tipo puede convertirse en una cadena llamando a su método
ToString
. - Clases e interfaces
- En un paradigma tradicional orientado a objetos, el único tipo es una clase. En C#, hay otras clases de tipos, una de las cuales es una interfaz. Una interfaz es como una clase que no puede contener datos. Esto significa que sólo puede definir el comportamiento (y no el estado), lo que permite la herencia múltiple, así como la separación entre especificación e implementación.
- Propiedades, métodos y eventos
- En el paradigma orientado a objetos puro, todas las funciones son métodos. En C#, los métodos son sólo un tipo de miembro de función, que también incluye propiedades y eventos (también hay otros). Las propiedades son miembros de función que encapsulan una parte del estado de un objeto, como el color de un botón o el texto de una etiqueta. Los eventos son miembros de función que simplifican la actuación sobre los cambios de estado del objeto.
Aunque C# es principalmente un lenguaje orientado a objetos, también toma prestado del paradigma de la programación funcional; en concreto:
- Las funciones pueden tratarse como valores
- Mediante los delegados, C# permite pasar funciones como valores a y desde otras funciones.
- C# admite patrones de pureza
- El núcleo de la programación funcional es evitar el uso de variables cuyos valores cambien, en favor de patrones declarativos. C# tiene características clave para ayudar con esos patrones, como la capacidad de escribir sobre la marcha funciones sin nombre que "capturan" variables(expresiones lambda) y la capacidad de realizar programación de listas o reactiva mediante expresiones de consulta. C# también proporciona registros, que facilitan la escritura de tipos inmutables (sólo lectura).
Tipo Seguridad
C# es principalmente un lenguaje a prueba de tipos, lo que significa que las instancias de los tipos sólo pueden interactuar a través de los protocolos que definen, garantizando así la coherencia interna de cada tipo. Por ejemplo, C# impide que interactúes con un tipo cadena como si fuera un tipo entero.
Más concretamente, C# admite el tipado estático, lo que significa que el lenguaje aplica la seguridad de tipos en tiempo de compilación. Esto se añade a la seguridad de tipos que se aplica en tiempo de ejecución.
El tipado estático elimina una gran clase de errores incluso antes de que se ejecute un programa. Desplaza la carga de las pruebas unitarias en tiempo de ejecución al compilador para verificar que todos los tipos de un programa encajan correctamente. Esto hace que los programas grandes sean mucho más fáciles de gestionar, más predecibles y más robustos. Además, el tipado estático permite que herramientas como IntelliSense de Visual Studio te ayuden a escribir un programa, porque sabe de qué tipo es una variable determinada y, por tanto, a qué métodos puedes llamar con esa variable. Estas herramientas también pueden identificar en qué parte de tu programa se utiliza una variable, un tipo o un método, lo que permite una refactorización fiable.
Nota
C# también permite tipar dinámicamente partes de tu código mediante la palabra clave dynamic
. Sin embargo, C# sigue siendo un lenguaje predominantemente tipado estáticamente.
C# también se denomina lenguaje fuertemente tipado porque sus reglas tipográficas se aplican estrictamente (ya sea de forma estática o en tiempo de ejecución). Por ejemplo, no puedes llamar a una función diseñada para aceptar un número entero con un número de coma flotante, a menos que primero conviertas explícitamente el número de coma flotante en un número entero. Esto ayuda a evitar errores.
Gestión de la memoria
C# confía en el tiempo de ejecución para realizar la gestión automática de la memoria. El Tiempo de Ejecución del Lenguaje Común tiene un recolector de basura que se ejecuta como parte de tu programa, recuperando la memoria de los objetos a los que ya no se hace referencia. Esto libera a los programadores de desasignar explícitamente la memoria de un objeto, eliminando el problema de los punteros incorrectos que se encuentran en lenguajes como C++.
C# no elimina los punteros: simplemente los hace innecesarios para la mayoría de las tareas de programación. Para los puntos críticos de rendimiento e interoperabilidad, se permiten los punteros y la asignación explícita de memoria en los bloques marcados como unsafe
.
Plataforma de apoyo
C# tiene tiempos de ejecución compatibles con las siguientes plataformas:
Escritorio de Windows 7-11 (para aplicaciones cliente enriquecido, web, servidor y línea de comandos)
macOS (para aplicaciones cliente enriquecido, web y de línea de comandos)
Linux y macOS (para aplicaciones web y de línea de comandos)
Android e iOS (para aplicaciones móviles)
Dispositivos Windows 10 (Xbox, Surface Hub y HoloLens)
También existe una tecnología llamada Blazor que puede compilar C# a ensamblador web que se ejecuta en un navegador.
CLR, BCL y tiempos de ejecución
El soporte de tiempo de ejecución para programas C# consiste en un Tiempo de Ejecución de Lenguaje Común y una Biblioteca de Clases Base. Un tiempo de ejecución también puede incluir una capa de aplicación de nivel superior que contenga bibliotecas para desarrollar aplicaciones de cliente enriquecido, móviles o web (ver Figura 1-1). Existen diferentes tiempos de ejecución para permitir diferentes tipos de aplicaciones, así como diferentes plataformas.
Tiempo de ejecución del lenguaje común
Un Lenguaje Común en Tiempo de Ejecución (CLR) proporciona servicios esenciales en tiempo de ejecución, como la gestión automática de la memoria y el manejo de excepciones. (La palabra "común" se refiere al hecho de que el mismo tiempo de ejecución puede ser compartido por otros lenguajes de programación gestionados, como F#, Visual Basic y C++ gestionado).
C# se denomina lenguaje gestionado porque compila el código fuente en código gestionado, que se representa en lenguaje intermedio (IL). El CLR convierte el IL en el código nativo de la máquina, como X64 o X86, normalmente justo antes de la ejecución. Esto se denomina compilación Justo a Tiempo (JIT). La compilación por adelantado también está disponible para mejorar el tiempo de inicio con grandes ensamblados o dispositivos con recursos limitados (y para satisfacer las normas de la tienda de aplicaciones iOS al desarrollar aplicaciones móviles).
El contenedor del código gestionado se llama conjunto. Un ensamblado no sólo contiene IL, sino también información de tipos(metadatos). La presencia de metadatos permite a los ensamblajes hacer referencia a tipos de otros ensamblajes sin necesidad de archivos adicionales.
Nota
Puedes examinar y desensamblar el contenido de un ensamblado con la herramienta ildasm de Microsoft. Y con herramientas como ILSpy o dotPeek de JetBrain, puedes ir más allá y descompilar el IL a C#. Como el IL es de nivel superior al código máquina nativo, el descompilador puede hacer un trabajo bastante bueno reconstruyendo el C# original.
Un programa puede consultar sus propios metadatos(reflection) e incluso generar nuevo IL en tiempo de ejecución(reflection.emit).
Biblioteca de clases base
Un CLR siempre se suministra con un conjunto de ensamblados denominado Biblioteca de Clases Base (BCL). Una BCL proporciona funciones básicas a los programadores, como colecciones, entrada/salida, procesamiento de texto, manejo de XML/JSON, redes, encriptación, interoperabilidad, concurrencia y programación paralela.
Una BCL también implementa tipos que el propio lenguaje C# requiere (para funciones como la enumeración, la consulta y la asincronía) y te permite acceder explícitamente a funciones del CLR, como la reflexión y la gestión de memoria.
Tiempos de ejecución
Un tiempo de ejecución (también llamado framework) es una unidad desplegable que descargas e instalas. Un tiempo de ejecución consta de un CLR (con su BCL), más una capa de aplicación opcional específica para el tipo de aplicación que estés escribiendo: web, móvil, cliente enriquecido, etc. (Si estás escribiendo una aplicación de consola de línea de comandos o una biblioteca no UI, no necesitas una capa de aplicación).
Cuando escribes una aplicación, te diriges a un tiempo de ejecución concreto, lo que significa que tu aplicación utiliza y depende de la funcionalidad que proporciona el tiempo de ejecución. Tu elección del tiempo de ejecución también determina qué plataformas soportará tu aplicación.
La siguiente tabla enumera las principales opciones de tiempo de ejecución:
Capa de aplicación | CLR/BCL | Tipo de programa | Funciona con... |
---|---|---|---|
ASP.NET | .NET 6 | Web | Windows, Linux, macOS |
Escritorio de Windows | .NET 6 | Windows | Windows 7-10+ |
MAUI (principios de 2022) | .NET 6 | Móvil, escritorio | iOS, Android, macOS, Windows 10+ |
WinUI 3 (principios de 2022) | .NET 6 | Win10 | Escritorio Windows 10+ |
UWP | .NET Core 2.2 | Dispositivos Win10 + Win10 | Windows 10+ escritorio y dispositivos |
(Legado) .NET Framework | Marco .NET | Web, Windows | Windows 7-10+ |
La Figura 1-2 muestra esta información gráficamente y también sirve como guía de lo que se trata en el libro.
.NET 6
.NET 6 es el principal tiempo de ejecución de código abierto de Microsoft. Puedes escribir aplicaciones web y de consola que se ejecuten en Windows, Linux y macOS; aplicaciones cliente enriquecidas que se ejecuten en Windows 7 a 11 y macOS; y aplicaciones móviles que se ejecuten en iOS y Android. Este libro se centra en el CLR y la BCL de .NET 6.
A diferencia de .NET Framework, .NET 6 no está preinstalado en las máquinas Windows. Si intentas ejecutar una aplicación .NET 6 sin que esté presente el tiempo de ejecución correcto, aparecerá un mensaje dirigiéndote a una página web donde podrás descargar el tiempo de ejecución. Puedes evitarlo creando una implementación autónoma, que incluya las partes del tiempo de ejecución necesarias para la aplicación.
Nota
El predecesor de .NET 6 fue .NET 5, cuyo predecesor fue .NET Core 3. (Microsoft eliminó "Core" del nombre y se saltó la versión 4.) El motivo de saltarse una versión fue evitar la confusión con .NET Framework 4.x.
Esto significa que los ensamblados compilados con las versiones 1, 2 y 3 de .NET Core (y .NET 5) funcionarán, en la mayoría de los casos, sin modificaciones con .NET 6. En cambio, los ensamblados compilados con (cualquier versión de) .NET Framework suelen ser incompatibles con .NET 6.
El BCL y el CLR de .NET 6 son muy similares a los de .NET 5 (y .NET Core 3), y sus diferencias se centran sobre todo en el rendimiento y la implementación.
UWP y WinUI 3
La Plataforma Universal de Windows (UWP) está diseñada para escribir aplicaciones táctiles inmersivas que se ejecutan en el escritorio y dispositivos Windows 10+ (Xbox, Surface Hub y HoloLens). Las aplicaciones UWP están aisladas y se distribuyen a través de la Tienda Windows. La UWP viene preinstalada con Windows 10. Utiliza una versión de .NET Core 2.2 CLR/BCL, y es poco probable que se actualice esta dependencia. En su lugar, Microsoft ha lanzado un sucesor llamado WinUI 3, como parte del SDK de Windows App.
El SDK de aplicaciones de Windows funciona con la última versión de .NET, se integra mejor con las API de escritorio de .NET y puede ejecutarse fuera de un sandbox. Sin embargo, aún no es compatible con dispositivos como Xbox o HoloLens.
Marco .NET
.NET Framework es el tiempo de ejecución original de Microsoft, exclusivo para Windows, para escribir aplicaciones web y de cliente enriquecido que se ejecutan (sólo) en escritorios/servidores Windows. No se prevén nuevas versiones importantes, aunque Microsoft seguirá apoyando y manteniendo la versión actual 4.8 debido a la gran cantidad de aplicaciones existentes.
Con .NET Framework, el CLR/BCL se integra con la capa de aplicación. Las aplicaciones escritas en .NET Framework pueden recompilarse en .NET 6, aunque suelen requerir algunas modificaciones. Algunas características de .NET Framework no están presentes en .NET 6 (y viceversa).
.NET Framework viene preinstalado con Windows y se parchea automáticamente a través de Windows Update. Cuando te dirijas a .NET Framework 4.8, podrás utilizar las funciones de C# 7.3 y anteriores.
Nota
La palabra ".NET" se ha utilizado durante mucho tiempo como término paraguas para cualquier tecnología que incluya la palabra ".NET" (.NET Framework, .NET Core, .NET Standard, etc.).
Esto significa que el cambio de nombre de Microsoft de .NET Core a .NET ha creado una desafortunada ambigüedad. En este libro, nos referiremos a la nueva .NET como .NET 5+. Y para referirnos a .NET Core y sus sucesores, utilizaremos la frase ".NET Core y .NET 5+".
Para aumentar la confusión, .NET (5+) es un marco de trabajo, aunque es muy diferente del .NET Framework. Por ello, utilizaremos el término tiempo de ejecución en lugar de marco, siempre que sea posible.
Nicho Tiempos de ejecución
También existen los siguientes tiempos de ejecución nicho:
El micromarco .NET sirve para ejecutar código .NET en dispositivos integrados con recursos muy limitados (menos de un megabyte).
Unity es una plataforma de desarrollo de juegos que permite programar la lógica del juego con C#.
También es posible ejecutar código gestionado dentro de SQL Server. Con la integración CLR de SQL Server, puedes escribir funciones personalizadas, procedimientos almacenados y agregaciones en C# y luego llamarlos desde SQL. Esto funciona junto con .NET Framework y un CLR especial "alojado" que impone un sandbox para proteger la integridad del proceso de SQL Server.
Breve historia de C#
A continuación se presenta una cronología inversa de las nuevas características de cada versión de C#, en beneficio de los lectores que ya estén familiarizados con una versión anterior del lenguaje.
Novedades en C# 10
C# 10 se suministra con Visual Studio 2022, y se utiliza cuando tienes como objetivo .NET 6.
Espacios de nombres con ámbito de archivo
En el caso habitual de que todos los tipos de un archivo se definan en un único espacio de nombres, una declaración de espacio de nombres con ámbito de archivo en C# 10 reduce el desorden y elimina un nivel innecesario de indentación:
namespace MyNamespace; // Applies to everything that follows in the file. class Class1 {} // inside MyNamespace class Class2 {} // inside MyNamespace
La directiva global using
Cuando antepones a una directiva using
la palabra clave global
, ésta se aplica a todos los archivos del proyecto:
global using System; global using System.Collection.Generic;
Esto te permite evitar repetir las mismas directivas en cada archivo. Las directivas global using
funcionan con using static
.
Además, los proyectos .NET 6 admiten ahora directivas de uso global implícitas: si el elemento ImplicitUsings
se establece como verdadero en el archivo de proyecto, se importan automáticamente los espacios de nombres más utilizados (en función del tipo de proyecto del SDK). Consulta "La directiva global using (C# 10)" para obtener más detalles.
Mutación no destructiva para tipos anónimos
C# 9 introdujo la palabra clave with
, para realizar mutaciones no destructivas en los registros. En C# 10, la palabra clave with
también funciona con tipos anónimos:
var a1 = new { A = 1, B = 2, C = 3, D = 4, E = 5 }; var a2 = a1 with { E = 10 }; Console.WriteLine (a2); // { A = 1, B = 2, C = 3, D = 4, E = 10 }
Nueva sintaxis de deconstrucción
C# 7 introdujo la sintaxis de deconstrucción para tuplas (o cualquier tipo con un Deconstruct
método). C# 10 lleva esta sintaxis más allá, permitiéndote mezclar asignación y declaración en la misma deconstrucción:
var point = (3, 4); double x = 0; (x, double y) = point;
Inicializadores de campo y constructores sin parámetros en structs
A partir de C# 10, puedes incluir inicializadores de campo y constructores sin parámetros en los structs (ver "Structs"). Éstos sólo se ejecutan cuando se llama explícitamente al constructor, por lo que pueden omitirse fácilmente, por ejemplo, mediante la palabra clave default
. Esta función se introdujo principalmente en beneficio de los registros de estructura.
Estructuras de registro
Los registros se introdujeron por primera vez en C# 9, donde actuaban como una clase compilada mejorada. En C# 10, los registros también pueden ser structs:
record struct Point (int X, int Y);
Por lo demás, las reglas son similares: los structs de registro tienen prácticamente las mismas características que los structs de clase (ver "Registros"). Una excepción es que las propiedades generadas por el compilador en los structs de registro son escribibles, a menos que antepongas a la declaración del registro la palabra clave readonly
.
Mejoras en la expresión lambda
La sintaxis de las expresiones lambda se ha mejorado de varias maneras. En primer lugar, se permite la tipificación implícita (var
):
var greeter = () => "Hello, world";
El tipo implícito de una expresión lambda es un delegado de Action
o Func
, por lo que greeter
, en este caso, es del tipo Func<string>
. Debes indicar explícitamente cualquier tipo de parámetro:
var square = (int x) => x * x;
En segundo lugar, una expresión lambda puede especificar un tipo de retorno:
var sqr = int (int x) => x;
Esto es principalmente para mejorar el rendimiento del compilador con lambdas anidadas complejas.
En tercer lugar, puedes pasar una expresión lambda a un parámetro de método de tipo object
, Delegate,
o Expression
:
M1 (() => "test"); // Implicitly typed to Func<string> M2 (() => "test"); // Implicitly typed to Func<string> M3 (() => "test"); // Implicitly typed to Expression<Func<string>> void M1 (object x) {} void M2 (Delegate x) {} void M3 (Expression x) {}
Por último, puedes aplicar atributos al método de destino generado por compilación de una expresión lambda (así como a sus parámetros y valor de retorno):
Action a = [Description("test")] () => { };
Consulta "Aplicar atributos a las expresiones lambda (C# 10)" para obtener más detalles.
Patrones de propiedades anidadas
La siguiente sintaxis simplificada es legal en C# 10 para la concordancia de patrones de propiedades anidadas (consulta "Patrones de propiedades"):
var obj = new Uri ("https://www.linqpad.net"); if (obj is Uri { Scheme.Length: 5 }) ...
Esto equivale a
if (obj is Uri { Scheme: { Length: 5 }}) ...
LlamadorExpresiónArgumento
Un parámetro de método al que apliques el atributo [CallerArgumentExpression]
captura una expresión de argumento del sitio de llamada:
Print (Math.PI * 2); void Print (double number, [CallerArgumentExpression("number")] string expr = null) => Console.WriteLine (expr); // Output: Math.PI * 2
Esta función está pensada principalmente para las bibliotecas de validación y aserción (consulta "CallerArgumentExpression (C# 10)").
Otras novedades
La directiva #line
se ha mejorado en C# 10 para permitir especificar una columna y un rango.
Las cadenas interpoladas en C# 10 pueden ser constantes, siempre que los valores interpolados sean constantes.
Los registros pueden sellar el método ToString()
en C# 10.
Se ha mejorado el análisis de asignaciones definitivas de C# para que funcionen expresiones como la siguiente:
if (foo?.TryParse ("123", out var number) ?? false) Console.WriteLine (number);
(Antes de C# 10, el compilador generaba un error: "Uso de variable local no asignada 'número'").
Novedades en C# 9.0
C# 9.0 se incluye con Visual Studio 2019, y se utiliza cuando tienes como objetivo .NET 5.
Declaraciones de nivel superior
Con las sentencias de nivel superior (ver "Sentencias de nivel superior"), puedes escribir un programa sin el bagaje de un método Main
y una clase Program
:
using System; Console.WriteLine ("Hello, world");
Las sentencias de nivel superior pueden incluir métodos (que actúan como métodos locales). También puedes acceder a los argumentos de la línea de comandos a través de la variable "mágica" args
y devolver un valor al invocador. Las sentencias de nivel superior pueden ir seguidas de declaraciones de tipo y espacio de nombres.
Fijadores sólo init
Un definidor sólo init (véase "Definidores sólo init") en una declaración de propiedad utiliza la palabra clave init
en lugar de la palabra clave set
:
class Foo { public int ID { get; init; } }
Se comporta como una propiedad de sólo lectura, salvo que también se puede establecer mediante un inicializador de objeto:
var foo = new Foo { ID = 123 };
Esto permite crear tipos inmutables (de sólo lectura) que pueden poblarse mediante un inicializador de objeto en lugar de un constructor, y ayuda a evitar el antipatrón de los constructores que aceptan un gran número de parámetros opcionales. Los definidores sólo de inicialización también permiten la mutación no destructiva cuando se utilizan en registros.
Registros
Un registro (véase "Registros") es un tipo especial de clase diseñado para funcionar bien con datos inmutables. Su característica más especial es que admite la mutación no destructiva mediante una nueva palabra clave (with
):
Point p1 = new Point (2, 3); Point p2 = p1 with { Y = 4 }; // p2 is a copy of p1, but with Y set to 4 Console.WriteLine (p2); // Point { X = 2, Y = 4 } record Point { public Point (double x, double y) => (X, Y) = (x, y); public double X { get; init; } public double Y { get; init; } }
En casos sencillos, un registro también puede eliminar el código repetitivo de definir propiedades y escribir un constructor y un deconstructor. Podemos sustituir nuestra definición de registro Point
por la siguiente, sin pérdida de funcionalidad:
record Point (double X, double Y);
Al igual que las tuplas, los registros presentan igualdad estructural por defecto. Los registros pueden subclasificar a otros registros y pueden incluir las mismas construcciones que las clases. El compilador implementa los registros como clases en tiempo de ejecución.
Mejoras en la concordancia de patrones
El patrón relacional (ver "Patrones") permite que los operadores <
, >
, <=
, y >=
aparezcan en los patrones:
string GetWeightCategory (decimal bmi) => bmi switch { < 18.5m => "underweight", < 25m => "normal", < 30m => "overweight", _ => "obese" };
Con los combinadores de patrones, puedes combinar patrones mediante tres nuevas palabras clave (and
, or
, y not
):
bool IsVowel (char c) => c is 'a' or 'e' or 'i' or 'o' or 'u'; bool IsLetter (char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
Al igual que con los operadores &&
y ||
, and
tiene mayor precedencia que or
. Puedes anular esto con paréntesis.
El combinador not
puede utilizarse con el patrón de tipos para comprobar si un objeto es (o no) un tipo:
if (obj is not string) ...
Nuevas expresiones de tipo objetivo
Al construir un objeto, C# 9 te permite omitir el nombre del tipo cuando el compilador puede deducirlo sin ambigüedades:
System.Text.StringBuilder sb1 = new(); System.Text.StringBuilder sb2 = new ("Test");
Esto es especialmente útil cuando la declaración y la inicialización de variables están en partes distintas de tu código:
class Foo { System.Text.StringBuilder sb; public Foo (string initialValue) => sb = new (initialValue); }
Y en el siguiente supuesto
MyMethod (new ("test")); void MyMethod (System.Text.StringBuilder sb) { ... }
Para más información, consulta "Nuevas expresiones de tipo objetivo".
Mejoras de interoperabilidad
C# 9 introdujo los punteros a función (consulta "Punteros a función" y "Devoluciones de llamada con punteros a función"). Su principal objetivo es permitir que el código no gestionado llame a métodos estáticos en C# sin la sobrecarga de una instancia delegada, con la posibilidad de eludir la capa P/Invoke cuando los argumentos y los tipos de retorno son blitables (representados de forma idéntica en cada lado).
C# 9 también introdujo los tipos enteros de tamaño nativo nint
y nuint
(véase "Enteros de tamaño nativo"), que se asignan en tiempo de ejecución a System.IntPtr
y . System.UIntPtr
. En tiempo de compilación, se comportan como tipos numéricos con soporte para operaciones aritméticas.
Otras novedades
Además, C# 9 ahora te permite hacer lo siguiente:
Anula un método o una propiedad de sólo lectura para que devuelva un tipo más derivado (consulta "Tipos de retorno covariantes")
Aplicar atributos a las funciones locales (ver "Atributos")
Aplica la palabra clave
static
a las expresiones lambda o funciones locales para asegurarte de que no capturas accidentalmente variables locales o de instancia (consulta "Lambdas estáticas").Haz que cualquier tipo funcione con la declaración
foreach
, escribiendo un método de extensiónGetEnumerator
Define un método inicializador de módulo que se ejecute una vez cuando se cargue por primera vez un ensamblado, aplicando el atributo
[ModuleInitializer]
a un método (estático vacío sin parámetros)Utilizar un "descarte" (símbolo de guión bajo) como argumento de una expresión lambda
Escribe métodos parciales extendidos que son obligatorios para implementar escenarios como los nuevos generadores de código fuente de Roslyn (consulta "Métodos parciales extendidos").
Aplica un atributo a métodos, tipos o módulos para evitar que las variables locales sean inicializadas por el tiempo de ejecución (ver "[SkipLocalsInit]")
Novedades en C# 8.0
C# 8.0 se distribuyó por primera vez con Visual Studio 2019, y se sigue utilizando hoy en día cuando tienes como objetivo .NET Core 3 o .NET Standard 2.1.
Índices y rangos
Los índices y rangos simplifican el trabajo con elementos o partes de una matriz (o los tipos de bajo nivel Span<T>
y ReadOnlySpan<T>
).
Los índices te permiten referirte a elementos relativos al final de una matriz utilizando el operador ^
. ^1
se refiere al último elemento, ^2
se refiere al penúltimo elemento, y así sucesivamente:
char[] vowels = new char[] {'a','e','i','o','u'}; char lastElement = vowels [^1]; // 'u' char secondToLast = vowels [^2]; // 'o'
Los rangos te permiten "trocear" un array utilizando el operador ..
:
char[] firstTwo = vowels [..2]; // 'a', 'e' char[] lastThree = vowels [2..]; // 'i', 'o', 'u' char[] middleOne = vowels [2..3] // 'i' char[] lastTwo = vowels [^2..]; // 'o', 'u'
C# implementa índices y rangos con ayuda de los tipos Index
y Range
:
Index last = ^1; Range firstTwoRange = 0..2; char[] firstTwo = vowels [firstTwoRange]; // 'a', 'e'
Puedes admitir índices y rangos en tus propias clases definiendo un indexador con un tipo de parámetro Index
o Range
:
class Sentence { string[] words = "The quick brown fox".Split(); public string this [Index index] => words [index]; public string[] this [Range range] => words [range]; }
Para más información, consulta "Índices".
Asignación nulo-coalescente
El operador ??=
asigna una variable sólo si es nula. En lugar de
if (s == null) s = "Hello, world";
ahora puedes escribir esto:
s ??= "Hello, world";
Utilizar declaraciones
Si omites los corchetes y el bloque de sentencia que sigue a una sentencia using
, se convierte en una declaración de uso. Entonces, el recurso se elimina cuando la ejecución cae fuera del bloque de sentencia que lo encierra:
if (File.Exists ("file.txt")) { using var reader = File.OpenText ("file.txt"); Console.WriteLine (reader.ReadLine()); ... }
En este caso, reader
se eliminará cuando la ejecución caiga fuera del bloque de sentencia if
.
Miembros de sólo lectura
C# 8 te permite aplicar el modificador readonly
a las funciones de una estructura, asegurando que si la función intenta modificar algún campo, se genere un error en tiempo de compilación:
struct Point { public int X, Y; public readonly void ResetX() => X = 0; // Error! }
Si una función de readonly
llama a una función que no es dereadonly
, el compilador genera una advertencia (y copia defensivamente la estructura para evitar la posibilidad de una mutación).
Métodos locales estáticos
Añadir el modificador static
a un método local impide que vea las variables y parámetros locales del método que lo encierra. Esto ayuda a reducir el acoplamiento y permite al método local declarar variables a su antojo, sin riesgo de colisionar con las del método contenedor.
Miembros por defecto de la interfaz
C# 8 te permite añadir una implementación por defecto a un miembro de la interfaz, haciendo que su implementación sea opcional:
interface ILogger { void Log (string text) => Console.WriteLine (text); }
Esto significa que puedes añadir un miembro a una interfaz sin romper las implementaciones. Las implementaciones por defecto deben llamarse explícitamente a través de la interfaz:
((ILogger)new Logger()).Log ("message");
Las interfaces también pueden definir miembros estáticos (incluidos los campos), a los que se puede acceder desde código dentro de implementaciones predeterminadas:
interface ILogger { void Log (string text) => Console.WriteLine (Prefix + text); static string Prefix = ""; }
O desde fuera de la interfaz, a menos que se restrinja mediante un modificador de accesibilidad en el miembro estático de la interfaz (como private
, protected
o internal
):
ILogger.Prefix = "File log: ";
Los campos de instancia están prohibidos. Para más detalles, consulta "Miembros por defecto de la interfaz".
Cambiar expresiones
A partir de C# 8, puedes utilizar switch
en el contexto de una expresión:
string cardName = cardNumber switch // assuming cardNumber is an int { 13 => "King", 12 => "Queen", 11 => "Jack", _ => "Pip card" // equivalent to 'default' };
Para ver más ejemplos, consulta "Expresiones de cambio".
Patrones de tupla, posicionales y de propiedad
C# 8 admite tres nuevos patrones, principalmente en beneficio de las sentencias/expresiones de conmutación (ver "Patrones"). Los patrones tupla te permiten conmutar múltiples valores:
int cardNumber = 12; string suite = "spades"; string cardName = (cardNumber, suite) switch { (13, "spades") => "King of spades", (13, "clubs") => "King of clubs", ... };
Los patrones posicionales permiten una sintaxis similar para los objetos que exponen un deconstructor, y los patrones de propiedades te permiten hacer coincidir las propiedades de un objeto. Puedes utilizar todos los patrones tanto en conmutadores como con el operador is
. El siguiente ejemplo utiliza un patrón de propiedades para comprobar si obj
es una cadena con una longitud de 4:
if (obj is string { Length:4 }) ...
Tipos de referencia anulables
Mientras que los tipos de valor anulables aportan anulabilidad a los tipos de valor, los tipos de referencia anulables hacen lo contrario y aportan (cierto grado de) no anulabilidad a los tipos de referencia, con el fin de ayudar a evitar NullReferenceException
s. Los tipos de referencia anulables introducen un nivel de seguridad que es aplicado puramente por el compilador en forma de advertencias o errores cuando detecta código que corre el riesgo de generar un valor NullReferenceException
.
Los tipos de referencia anulables pueden activarse a nivel de proyecto (mediante el elemento Nullable
del archivo de proyecto .csproj ) o en el código (mediante la directiva #nullable
). Una vez activada, el compilador hace que la no anulabilidad sea el valor por defecto: si quieres que un tipo de referencia acepte nulos, debes aplicar el sufijo ?
para indicar un tipo de referencia anulable:
#nullable enable // Enable nullable reference types from this point on string s1 = null; // Generates a compiler warning! (s1 is non-nullable) string? s2 = null; // OK: s2 is nullable reference type
Los campos no inicializados también generan una advertencia (si el tipo no está marcado como anulable), al igual que la desreferencia a un tipo de referencia anulable, si el compilador piensa que puede producirse una NullReferenceException
puede producirse:
void Foo (string? s) => Console.Write (s.Length); // Warning (.Length)
Para eliminar la advertencia, puedes utilizar el operador de anulación (!
):
void Foo (string? s) => Console.Write (s!.Length);
Para más información, consulta "Tipos de referencia anulables".
Flujos asíncronos
Antes de C# 8, podías utilizar yield return
para escribir un iterador, o await
para escribir una función asíncrona. Pero no podías hacer ambas cosas y escribir un iterador que esperara, produciendo elementos de forma asíncrona. C# 8 soluciona esto con la introducción de los flujos asíncronos:
async IAsyncEnumerable<int> RangeAsync ( int start, int count, int delay) { for (int i = start; i < start + count; i++) { await Task.Delay (delay); yield return i; } }
La declaración await foreach
consume un flujo asíncrono:
await foreach (var number in RangeAsync (0, 10, 100)) Console.WriteLine (number);
Para más información, consulta "Flujos asíncronos".
Novedades en C# 7.x
C# 7.x se distribuyó por primera vez con Visual Studio 2017. Visual Studio 2019 sigue utilizando C# 7.3 cuando se dirige a .NET Core 2, .NET Framework 4.6 a 4.8 o .NET Standard 2.0.
C# 7.3
C# 7.3 introdujo pequeñas mejoras en las funciones existentes, como permitir el uso de los operadores de igualdad con tuplas, mejorar la resolución de sobrecargas y ofrecer la posibilidad de aplicar atributos a los campos de respaldo de las propiedades automáticas:
[field:NonSerialized] public int MyProperty { get; set; }
C# 7.3 también se basa en las funciones avanzadas de programación de baja asignación de C# 7.2, con la posibilidad de reasignar ref locales, la no necesidad de fijar al indexar campos fixed
y la compatibilidad con inicializadores de campo con stackalloc
:
int* pointer = stackalloc int[] {1, 2, 3}; Span<int> arr = stackalloc [] {1, 2, 3};
Observa que la memoria asignada a la pila puede asignarse directamente a un Span<T>
. Describimos los espacios -y por qué los utilizarías- en el capítulo 23.
C# 7.2
C# 7.2 añadió un nuevo modificador private protected
(la intersección de internal
y protected
), la posibilidad de seguir argumentos con nombre con argumentos posicionales al llamar a métodos, y los structs readonly
. Una estructura readonly
obliga a que todos los campos sean readonly
, para facilitar la declaración de intenciones y dar más libertad de optimización al compilador:
readonly struct Point { public readonly int X, Y; // X and Y must be readonly }
C# 7.2 también ha añadido funciones especializadas para ayudar a la microoptimización y a la programación de baja asignación: consulta "El modificador in", " Ref Locales", "Ref Devoluciones" y "Ref Estructuras").
C# 7.1
A partir de C# 7.1, puedes omitir el tipo cuando utilices la palabra clave default
, si el tipo puede deducirse:
decimal number = default; // number is decimal
C# 7.1 también flexibilizó las reglas de las sentencias switch (para que puedas hacer coincidir patrones en parámetros de tipo genérico), permitió que el método Main
de un programa fuera asíncrono y permitió que se dedujeran los nombres de los elementos de las tuplas:
var now = DateTime.Now; var tuple = (now.Hour, now.Minute, now.Second);
Mejoras literales numéricas
Los literales numéricos en C# 7 pueden incluir guiones bajos para mejorar la legibilidad. Se denominan separadores de dígitos y el compilador los ignora:
int million = 1_000_000;
Los literales binarios pueden especificarse con el prefijo 0b
:
var b = 0b1010_1011_1100_1101_1110_1111;
Variables de salida y descartes
C# 7 facilita la llamada a métodos que contienen parámetros out
. En primer lugar, ahora puedes declarar variables de salida sobre la marcha (consulta "Variables de salida y descartes"):
bool successful = int.TryParse ("123", out int result); Console.WriteLine (result);
Y cuando llames a un método con varios parámetros out
, puedes descartar los que no te interesen con el carácter de subrayado:
SomeBigMethod (out _, out _, out _, out int x, out _, out _, out _); Console.WriteLine (x);
Patrones de tipo y variables de patrón
También puedes introducir variables sobre la marcha con el operador is
. Se llaman variables patrón (ver "Introducir una variable patrón"):
void Foo (object x) { if (x is string s) Console.WriteLine (s.Length); }
La sentencia switch
también admite patrones de tipos, por lo que puedes conmutar tanto por tipos como por constantes (ver "Conmutación de tipos"). Puedes especificar condiciones con una cláusula when
y también activar el valor null
:
switch (x) { case int i: Console.WriteLine ("It's an int!"); break; case string s: Console.WriteLine (s.Length); // We can use the s variable break; case bool b when b == true: // Matches only when b is true Console.WriteLine ("True"); break; case null: Console.WriteLine ("Nothing"); break; }
Métodos locales
Un método local es un método declarado dentro de otra función (ver "Métodos locales"):
void WriteCubes() { Console.WriteLine (Cube (3)); Console.WriteLine (Cube (4)); Console.WriteLine (Cube (5)); int Cube (int value) => value * value * value; }
Los métodos locales sólo son visibles para la función que los contiene y pueden capturar variables locales del mismo modo que las expresiones lambda.
Miembros más expresivos
C# 6 introdujo la sintaxis de "flecha gorda" con cuerpo de expresión para métodos, propiedades de sólo lectura, operadores e indexadores. C# 7 la amplía a constructores, propiedades de lectura/escritura y finalizadores:
public class Person { string name; public Person (string name) => Name = name; public string Name { get => name; set => name = value ?? ""; } ~Person () => Console.WriteLine ("finalize"); }
Deconstructores
C# 7 introduce el patrón deconstructor (ver "Deconstructores"). Mientras que un constructor suele tomar un conjunto de valores (como parámetros) y asignarlos a campos, un deconstructor hace lo contrario y vuelve a asignar campos a un conjunto de variables. Podríamos escribir un deconstructor para la clase Person
del ejemplo anterior de la siguiente manera (sin tener en cuenta el tratamiento de excepciones):
public void Deconstruct (out string firstName, out string lastName) { int spacePos = name.IndexOf (' '); firstName = name.Substring (0, spacePos); lastName = name.Substring (spacePos + 1); }
Los deconstructores se llaman con la siguiente sintaxis especial:
var joe = new Person ("Joe Bloggs"); var (first, last) = joe; // Deconstruction Console.WriteLine (first); // Joe Console.WriteLine (last); // Bloggs
Tuplas
Quizá la mejora más notable de C# 7 sea el soporte explícito de tuplas (ver "Tuplas"). Las tuplas proporcionan una forma sencilla de almacenar un conjunto de valores relacionados:
var bob = ("Bob", 23); Console.WriteLine (bob.Item1); // Bob Console.WriteLine (bob.Item2); // 23
Las nuevas tuplas de C# son azúcar sintáctico para utilizar los structs genéricos de System.ValueTuple<…>
. Pero gracias a la magia del compilador, los elementos de las tuplas se pueden nombrar:
var tuple = (name:"Bob", age:23); Console.WriteLine (tuple.name); // Bob Console.WriteLine (tuple.age); // 23
Con las tuplas, las funciones pueden devolver varios valores sin recurrir a los parámetros de out
o a una carga tipográfica adicional:
static (int row, int column) GetFilePosition() => (3, 10); static void Main() { var pos = GetFilePosition(); Console.WriteLine (pos.row); // 3 Console.WriteLine (pos.column); // 10 }
Las tuplas admiten implícitamente el patrón de deconstrucción, por lo que puedes deconstruirlas fácilmente en variables individuales:
static void Main() { (int row, int column) = GetFilePosition(); // Creates 2 local variables Console.WriteLine (row); // 3 Console.WriteLine (column); // 10 }
tirar expresiones
Antes de C# 7, throw
era siempre una expresión. Ahora también puede aparecer como una expresión en funciones con cuerpo de expresión:
public string Foo() => throw new NotImplementedException();
Una expresión throw
también puede aparecer en una expresión condicional ternaria:
string Capitalize (string value) => value == null ? throw new ArgumentException ("value") : value == "" ? "" : char.ToUpper (value[0]) + value.Substring (1);
Novedades en C# 6.0
C# 6.0, que se distribuye con Visual Studio 2015, incluye un compilador de nueva generación, completamente escrito en C#. Conocido como proyecto "Roslyn", el nuevo compilador expone todo el proceso de compilación mediante bibliotecas, lo que te permite realizar análisis de código en código fuente arbitrario. El propio compilador es de código abierto, y el código fuente está disponible en github.com/dotnet/roslyn.
Además, C# 6.0 presenta varias mejoras menores pero significativas, destinadas principalmente a reducir el desorden del código.
El operador nulo-condicional ("Elvis") (ver "Operadores nulos") evita tener que comprobar explícitamente si es nulo antes de llamar a un método o acceder a un miembro de un tipo. En el siguiente ejemplo, result
se evalúa como nulo en lugar de lanzar un NullReferenceException
:
System.Text.StringBuilder sb = null; string result = sb?.ToString(); // result is null
Las funciones con cuerpo de expresión (véase "Métodos") permiten escribir de forma más concisa, al estilo de una expresión lambda, los métodos, propiedades, operadores e indexadores que componen una única expresión:
public int TimesTwo (int x) => x * 2; public string SomeProperty => "Property value";
Los inicializadores de propiedades(Capítulo 3) te permiten asignar un valor inicial a una propiedad automática:
public DateTime TimeCreated { get; set; } = DateTime.Now;
Las propiedades inicializadas también pueden ser de sólo lectura:
public DateTime TimeCreated { get; } = DateTime.Now;
Las propiedades de sólo lectura también pueden establecerse en el constructor, lo que facilita la creación de tipos inmutables (de sólo lectura).
Los inicializadores de índices(Capítulo 4) permiten inicializar en un solo paso cualquier tipo que exponga un indexador:
var dict = new Dictionary<int,string>() { [3] = "three", [10] = "ten" };
La interpolación de cadenas (véase "Tipo de cadena") ofrece una alternativa sucinta a string.Format
:
string s = $"It is {DateTime.Now.DayOfWeek} today";
Los filtros de excepción (ver "Sentencias try y excepciones") te permiten aplicar una condición a un bloque catch:
string html; try { html = await new HttpClient().GetStringAsync ("http://asef"); } catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout) { ... }
La directiva using static
(véase "Espacios de nombres") te permite importar todos los miembros estáticos de un tipo para que puedas utilizarlos sin calificarlos:
using static System.Console; ... WriteLine ("Hello, world"); // WriteLine instead of Console.WriteLine
El operador nameof
(Capítulo 3) devuelve el nombre de una variable, tipo u otro símbolo en forma de cadena. Esto evita que se rompa el código cuando cambies el nombre de un símbolo en Visual Studio:
int capacity = 123; string x = nameof (capacity); // x is "capacity" string y = nameof (Uri.Host); // y is "Host"
Y por último, ahora puedes await
dentro de los bloques catch
y finally
.
Novedades en C# 5.0
La gran novedad de C# 5.0 fue la compatibilidad con funciones asíncronas mediante dos nuevas palabras clave, async
y await
. Las funciones asíncronas permiten continuaciones asíncronas, que facilitan la escritura de aplicaciones de cliente enriquecido con capacidad de respuesta y a prueba de hilos. También facilitan la escritura de aplicaciones de E/S altamente concurrentes y eficientes, que no ocupan un recurso de subproceso por operación. Trataremos las funciones asíncronas en detalle en el Capítulo 14.
Novedades en C# 4.0
C# 4.0 introdujo cuatro mejoras importantes:
La vinculación dinámica (Capítulos 4 y 19) aplaza la vinculación -elproceso de resolver tipos y miembros- del tiempo de compilación al tiempo de ejecución y es útil en situaciones que, de otro modo, requerirían un código de reflexión complicado. La vinculación dinámica también es útil al interoperar con lenguajes dinámicos y componentes COM.
Los parámetros opcionales(Capítulo 2) permiten que las funciones especifiquen valores de parámetros por defecto, de modo que quien llama puede omitir argumentos, y los argumentos con nombre permiten que quien llama a una función identifique un argumento por su nombre en lugar de por su posición.
Las reglas de varianza detipos se relajaron en C# 4.0 (Capítulos 3 y 4), de modo que los parámetros de tipos en interfaces genéricas y delegados genéricos pueden marcarse como covariantes o contravariantes, lo que permite conversiones de tipos más naturales.
Lainteroperabilidad COM(Capítulo 24) se ha mejorado en C# 4.0 de tres formas. En primer lugar, los argumentos se pueden pasar por referencia sin la palabra clave ref
(especialmente útil en conjunción con parámetros opcionales). En segundo lugar, los conjuntos que contienen tipos de interoperabilidad COM pueden enlazarse en lugar de referenciarse. Los tipos de interoperabilidad enlazados admiten la equivalencia de tipos, lo que evita la necesidad de ensamblajes de interoperabilidad primarios y pone fin a los quebraderos de cabeza de las versiones y la implementación. En tercer lugar, las funciones que devuelven tipos COM Variant a partir de tipos de interoperabilidad enlazados se asignan a dynamic
en lugar de a object
, lo que elimina la necesidad de hacer castings.
Novedades en C# 3.0
Las funciones añadidas a C# 3.0 se centraron principalmente en las capacidades de Consulta Integrada en Lenguaje (LINQ). LINQ permite escribir consultas directamente en un programa C# y comprobar estáticamente su corrección, así como consultar colecciones locales (como listas o documentos XML) o fuentes de datos remotas (como una base de datos). Las funciones de C# 3.0 añadidas para admitir LINQ incluían variables locales de tipado implícito, tipos anónimos, inicializadores de objetos, expresiones lambda, métodos de extensión, expresiones de consulta y árboles de expresiones.
Las variables locales de tipado implícito (palabra clavevar
, Capítulo 2) te permiten omitir el tipo de variable en una declaración, dejando que el compilador lo deduzca. Esto reduce el desorden y permite los tipos anónimos(Capítulo 4), que son clases sencillas creadas sobre la marcha que suelen utilizarse en el resultado final de las consultas LINQ. También puedes tipar implícitamente matrices(Capítulo 2).
Los inicializadores de objetos(Capítulo 3) simplifican la construcción de objetos permitiéndote establecer propiedades en línea después de la llamada al constructor. Los inicializadores de objetos funcionan tanto con tipos con nombre como anónimos.
Las expresiones lambda(capítulo 4) son funciones en miniatura creadas por el compilador sobre la marcha; son especialmente útiles en las consultas LINQ "fluidas"(capítulo 8).
Los métodos de extensión (Capítulo 4) amplían un tipo existente con nuevos métodos (sin alterar la definición del tipo), haciendo que los métodos estáticos parezcan métodos de instancia. Los operadores de consulta de LINQ se implementan como métodos de extensión.
Las expresiones de consulta(Capítulo 8) proporcionan una sintaxis de nivel superior para escribir consultas LINQ que pueden ser sustancialmente más sencillas cuando se trabaja con varias secuencias o variables de rango.
Los árboles de expresiones(Capítulo 8) son Modelos de Objetos de Documentos (DOM) de código en miniatura que describen expresiones lambda asignadas al tipo especial Expression<TDelegate>
. Los árboles de expresiones hacen posible que las consultas LINQ se ejecuten a distancia (por ejemplo, en un servidor de bases de datos), ya que pueden ser introspeccionadas y traducidas en tiempo de ejecución (por ejemplo, a una sentencia SQL).
C# 3.0 también añadió propiedades automáticas y métodos parciales.
Propiedades automáticas(Capítulo 3) reducen el trabajo de escribir propiedades que simplemente get
/set
un campo de respaldo privado haciendo que el compilador realice ese trabajo automáticamente. Los métodos parciales(Capítulo 3) permiten que una clase parcial autogenerada proporcione ganchos personalizables para la autoría manual que "desaparecen" si no se utilizan.
Novedades en C# 2.0
Las grandes novedades de C# 2 fueron los genéricos(Capítulo 3), los tipos de valor anulables(Capítulo4), los iteradores(Capítulo 4) y los métodos anónimos (predecesores de las expresiones lambda). Estas características allanaron el camino para la introducción de LINQ en C# 3.
C# 2 también añadió soporte para clases parciales y clases estáticas, y una serie de características menores y misceláneas como el calificador de alias de espacio de nombres, los ensamblados amigos y los búferes de tamaño fijo.
La introducción de los genéricos requirió un nuevo CLR (CLR 2.0), porque los genéricos mantienen la plena fidelidad de los tipos en tiempo de ejecución.
Get C# 10 en pocas palabras now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.