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 varios tipos más, uno de los 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, concretamente:

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 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 Windows 7+ (para aplicaciones cliente enriquecido, web, servidor y línea de comandos)

  • macOS (para aplicaciones web y de línea de comandos, y aplicaciones cliente enriquecidas mediante Mac Catalyst)

  • Linux (para aplicaciones web y de línea de comandos)

  • Android e iOS (para aplicaciones móviles)

  • Dispositivos Windows 10 (Xbox, Surface Hub y HoloLens) mediante UWP

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.

Runtime architecture
Figura 1-1. Arquitectura de tiempo de ejecución

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 Reflection 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 consiste en un CLR (con su BCL), además de 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).

Al escribir 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 8 Web Windows, Linux, macOS
Escritorio de Windows .NET 8 Windows Windows 10+
WinUI 3 .NET 8 Windows Windows 10+
MAUI .NET 8 Móvil, escritorio iOS, Android, macOS, Windows 10+
Marco .NET Marco .NET Web, Windows Windows 7+

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.

Runtimes for C#
Figura 1-2. Tiempos de ejecución para C#

.NET 8

.NET 8 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 10+ 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 8.

A diferencia de .NET Framework, .NET 8 no está preinstalado en las máquinas Windows. Si intentas ejecutar una aplicación .NET 8 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 historial de actualizaciones de .NET es el siguiente: .NET Core 1.x → . NET Core 2.x →.NET Core 3.x → .NET 5 → .NET 6 → .NET 7 → .NET 8. Después de .NET Core 3, Microsoft eliminó "Core" del nombre y omitió la versión 4 para evitar confusiones con .NET Framework 4.x, que precede a todos los tiempos de ejecución anteriores, pero sigue siendo compatible y de uso popular.

Esto significa que los ensamblados compilados con .NET Core 1.x → .NET 7 funcionarán, en la mayoría de los casos, sin modificaciones con .NET 8. En cambio, los ensamblados compilados con (cualquier versión de) .NET Framework suelen ser incompatibles con .NET 8.

Escritorio de Windows y WinUI 3

Para escribir aplicaciones de cliente enriquecido que se ejecuten en Windows 10 y superior, puedes elegir entre las API clásicas de Windows Desktop (Windows Forms y WPF) y WinUI 3. Las API de Windows Desktop forman parte del tiempo de ejecución de .NET Desktop, mientras que WinUI 3 forma parte del SDK de Windows App (una descarga independiente).

Las API clásicas de Windows Desktop existen desde 2006 y gozan de un magnífico soporte de bibliotecas de terceros, además de ofrecer una gran cantidad de preguntas respondidas en sitios como StackOverflow. WinUI 3 se lanzó en 2022 y está pensada para escribir aplicaciones inmersivas modernas que incorporen los últimos controles de Windows 10+. Es el sucesor de la Plataforma Universal de Windows (UWP).

MAUI

MAUI (Multi-platform App UI) está diseñado principalmente para crear aplicaciones móviles para iOS y Android, aunque también se puede utilizar para aplicaciones de escritorio que se ejecutan en macOS y Windows a través de Mac Catalyst y WinUI 3. MAUI es una evolución de Xamarin y permite que un solo proyecto se dirija a múltiples plataformas.

Nota

Para aplicaciones de escritorio multiplataforma, una biblioteca de terceros llamada Avalonia ofrece una alternativa a MAUI. Avalonia también funciona en Linux y es arquitectónicamente más sencilla que MAUI (ya que funciona sin la capa de indirección Catalyst/WinUI). Avalonia tiene una API similar a WPF, y también ofrece un complemento comercial llamado XPF que proporciona una compatibilidad casi total con WPF.

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 el escritorio/servidor de 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 8, aunque suelen requerir algunas modificaciones. Algunas características de .NET Framework no están presentes en .NET 8 (y viceversa).

.NET Framework viene preinstalado con Windows y se parchea automáticamente a través de Windows Update. Cuando te diriges a .NET Framework 4.8, puedes utilizar las funciones de C# 7.3 y anteriores. (Puedes anular esto especificando una versión más reciente del lenguaje en el archivo de proyecto, lo que desbloquea todas las funciones más recientes del lenguaje, excepto las que requieren la compatibilidad con un tiempo de ejecución más reciente).

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+ cuando surja una ambigüedad. 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:

  • Unity es una plataforma de desarrollo de juegos que permite programar la lógica del juego con C#.

  • La Plataforma Universal de Windows (UWP) se diseñó para escribir aplicaciones táctiles que se ejecutan en el escritorio y dispositivos Windows 10+, incluidos Xbox, Surface Hub y HoloLens. Las aplicaciones UWP están aisladas y se distribuyen a través de la Tienda Windows. UWP utiliza una versión de .NET Core 2.2 CLR/BCL, y es poco probable que esta dependencia se actualice; en su lugar, Microsoft ha recomendado a los usuarios que cambien a su moderno sustituto, WinUI 3. Pero como WinUI 3 sólo es compatible con el escritorio de Windows, UWP sigue teniendo un nicho de aplicaciones para dirigirse a Xbox, Surface Hub y HoloLens.

  • El micromarco .NET sirve para ejecutar código .NET en dispositivos integrados con recursos muy limitados (menos de un megabyte).

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# 12

C# 12 se suministra con Visual Studio 2022, y se utiliza cuando tienes como objetivo .NET 8.

Expresiones de la colección

En lugar de inicializar una matriz como se indica a continuación:

char[] vowels = {'a','e','i','o','u'};

ahora puedes utilizar corchetes (una expresión de colección):

char[] vowels = ['a','e','i','o','u'];

Las expresiones de colección tienen dos grandes ventajas. En primer lugar, la misma sintaxis funciona también con otros tipos de colecciones, como listas y conjuntos (e incluso con los tipos span de bajo nivel):

List<char> list         = ['a','e','i','o','u'];
HashSet<char> set       = ['a','e','i','o','u'];
ReadOnlySpan<char> span = ['a','e','i','o','u'];

En segundo lugar, son de tipo objetivo, lo que significa que puedes omitir el tipo en otras situaciones en las que el compilador pueda deducirlo, como al llamar a métodos:

Foo (['a','e','i','o','u']);

void Foo (char[] letters) { ... }

Para más detalles, consulta "Inicializadores de colecciones y expresiones de colecciones".

Constructores primarios en clases y structs

A partir de C# 12, puedes incluir una lista de parámetros directamente después de la declaración de una clase (o estructura):

class Person (string firstName, string lastName)
{
  public void Print() => Console.WriteLine (firstName + " " + lastName);
}

Esto indica al compilador que construya automáticamente un constructor primario, permitiendo lo siguiente:

Person p = new Person ("Alice", "Jones");
p.Print();    // Alice Jones

Esta característica existe desde C# 9 con los registros, donde se comportan de forma ligeramente distinta. Con los registros, el compilador genera (por defecto) una propiedad pública init-only para cada parámetro primario del constructor. Esto no ocurre con las clases y los structs; para conseguir el mismo resultado, debes definir esas propiedades explícitamente:

class Person (string firstName, string lastName)
{
  public string FirstName { get; set; } = firstName;
  public string LastName { get; set; } = lastName;
}

Los constructores primarios funcionan bien en situaciones sencillas. Describimos sus matices y limitaciones en "Constructores primarios (C# 12)".

Parámetros lambda por defecto

Del mismo modo que los métodos ordinarios pueden definir parámetros con valores por defecto:

void Print (string message = "") => Console.WriteLine (message);

también pueden hacerlo las expresiones lambda:

var print = (string message = "") => Console.WriteLine (message);

print ("Hello");
print ();

Esta función es útil con bibliotecas como ASP.NET Minimal API.

Alias cualquier tipo

C# siempre te ha permitido poner un alias a un tipo simple o genérico mediante la directiva using:

using ListOfInt = System.Collections.Generic.List<int>;

var list = new ListOfInt();

A partir de C# 12, este enfoque también funciona con otros tipos, como matrices y tuplas:

using NumberList = double[];
using Point = (int X, int Y);

NumberList numbers = { 2.5, 3.5 };
Point p = (3, 4);

Otras novedades

C# 12 también admite matrices en línea, mediante el atributo [System.Runtime.CompilerServices.InlineArray]. Esto permite la creación de matrices de tamaño fijo en una estructura sin necesidad de un contexto inseguro, y está pensado para su uso principalmente en las API en tiempo de ejecución.

Novedades de C# 11

C# 11 se suministra con Visual Studio 2022, y se utiliza por defecto cuando se apunta a .NET 7.

Literales de cadena sin procesar

Envolver una cadena entre tres o más caracteres de comillas crea un literal de cadena sin procesar, que puede contener casi cualquier secuencia de caracteres sin escaparse ni duplicarse. Esto facilita la representación de literales JSON, XML y HTML, así como de expresiones regulares y código fuente:

string raw = """<file path="c:\temp\test.txt"></file>""";

Los literales de cadena sin procesar pueden ser multilínea y permiten la interpolación mediante el prefijo $:

string multiLineRaw = $"""
  Line 1
  Line 2
  The date and time is {DateTime.Now}
  """;

Utilizar dos (o más) caracteres $ en un prefijo literal de cadena sin procesar cambia la secuencia de interpolación de una llave a dos (o más) llaves, lo que te permite incluir llaves en la propia cadena:

Console.WriteLine ($$"""{ "TimeStamp": "{{DateTime.Now}}" }""");
// Output: { "TimeStamp": "01/01/2024 12:13:25 PM" }

Cubrimos los matices de esta función en "Literales de cadena sin procesar (C# 11)" e "Interpolación de cadenas".

Cadenas UTF-8

Con el sufijo u8, puedes crear literales de cadena codificados en UTF-8 en lugar de UTF-16. Esta función está pensada para escenarios avanzados, como el manejo a bajo nivel de texto JSON en puntos críticos de rendimiento:

ReadOnlySpan<byte> utf8 = "ab→cd"u8;  // Arrow symbol consumes 3 bytes
Console.WriteLine (utf8.Length);      // 7

El tipo subyacente es ReadOnlySpan<byte> (Capítulo 23), que puedes convertir en una matriz de bytes llamando a su método ToArray().

Patrones de la lista

Los patrones de lista coinciden con una serie de elementos entre corchetes, y funcionan con cualquier tipo de colección que sea contable (con una propiedad Count o Length ) e indexable (con un indexador de tipo int o System.Index):

int[] numbers = { 0, 1, 2, 3, 4 };
Console.WriteLine (numbers is [0, 1, 2, 3, 4]);   // True

Un guión bajo coincide con un único elemento de cualquier valor, y dos puntos coinciden con cero o más elementos (una porción):

Console.WriteLine (numbers is [_, 1, .., 4]);     // True

Una rodaja puede ir seguida del patrón var -para más detalles, consulta "Patrones de lista".

Miembros necesarios

Aplicar el modificador required a un campo o propiedad obliga a los consumidores de esa clase o estructura a poblar ese miembro mediante un inicializador de objeto al construirlo:

Asset a1 = new Asset { Name = "House" };  // OK
Asset a2 = new Asset();                   // Error: will not compile!

class Asset { public required string Name; }

Con esta función, puedes evitar escribir constructores con largas listas de parámetros, lo que puede simplificar la subclasificación. Si también deseas escribir un constructor, puedes aplicar el atributo [SetsRequiredMembers] para eludir la restricción de miembros obligatorios para ese constructor; consulta "Miembros obligatorios (C# 11)" para más detalles.

Miembros de la interfaz estática virtual/abstracta

A partir de C# 11, las interfaces pueden declarar miembros como static virtual o static abstract:

public interface IParsable<TSelf>
{
   static abstract TSelf Parse (string s);
}

Estos miembros se implementan como funciones estáticas en clases o structs, y pueden llamarse polimórficamente mediante un parámetro de tipo restringido:

T ParseAny<T> (string s) where T : IParsable<T> => T.Parse (s);

Las funciones de operador también pueden declararse como static virtual o static abstract.

Para más detalles, consulta "Miembros estáticos virtuales/abstractos de la interfaz" y "Polimorfismo estático". También describimos cómo llamar a miembros abstractos estáticos mediante reflexión en "Llamar a miembros estáticos virtuales/abstractos de la interfaz".

Matemáticas genéricas

La interfaz System.Numerics.INumber<TSelf> (nueva en .NET 7) unifica las operaciones aritméticas en todos los tipos numéricos, lo que permite escribir métodos genéricos como el siguiente:

T Sum<T> (T[] numbers) where T : INumber<T>
{
  T total = T.Zero;
  foreach (T n in numbers)
    total += n;      // Invokes addition operator for any numeric type
  return total;
}

int intSum = Sum (3, 5, 7);
double doubleSum = Sum (3.2, 5.3, 7.1);
decimal decimalSum = Sum (3.2m, 5.3m, 7.1m);

INumber<TSelf> lo implementan todos los tipos numéricos reales e integrales de .NET (así como char), y comprende varias interfaces que incluyen definiciones abstractas estáticas de operadores, como las siguientes:

static abstract TResult operator + (TSelf left, TOther right);

Lo tratamos en "Operadores polimórficos" y "Matemáticas genéricas".

Otras novedades

A un tipo con el modificador de accesibilidad file sólo se puede acceder desde dentro del mismo archivo, y está pensado para su uso dentro de generadores de código fuente:

file class Foo { ... }

C# 11 también introdujo los operadores verificados (ver "Operadores verificados"), para definir funciones de operador que se llamen dentro de los bloques checked (esto era necesario para una implementación completa de las matemáticas genéricas). C# 11 también relajó el requisito de rellenar todos los campos en el constructor de una estructura (ver "Semántica de la construcción de estructuras").

Por último, los tipos de enteros de tamaño nativo nint y nuint que se introdujeron en C# 9 para ajustarse al espacio de direcciones del proceso en tiempo de ejecución (32 ó 64 bits) se mejoraron en C# 11 cuando se apuntaba a .NET 7 o posterior. En concreto, la distinción en tiempo de compilación entre estos tipos y sus tipos subyacentes en tiempo de ejecución (IntPtr y UIntPtr) se ha disuelto cuando el objetivo es .NET 7+. Consulta "Números enteros de tamaño nativo" para obtener más información.

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 de uso global" 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")] () => { };

Para más detalles, consulta "Aplicar atributos a las expresiones lambda".

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").

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 de 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 (ver "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 introduce los punteros a función (consulta "Punteros a función" y "Devoluciones de llamada con punteros a función"). Su principal finalidad 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 introduce 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:

  • Anula un método o una propiedad de sólo lectura para que devuelva un tipo más derivado (consulta "Tipos de retorno covariantes").

  • Aplica 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ón GetEnumerator.

  • Define un método inicializador de módulo que se ejecute una vez cuando se cargue por primera vez un conjunto, aplicando el atributo [ModuleInitializer] a un método (estático vacío sin parámetros).

  • Utiliza un "descarte" (símbolo de guión bajo) como argumento de una expresión lambda.

  • Escribe métodos parciales extendidos 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 (véase "[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 y rangos".

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 readonly llama a una función que no esreadonly, 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 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 (véase "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 NullReferenceExceptions. 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 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 un NullReferenceException:

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 la posibilidad de utilizar los operadores de igualdad con tuplas, la mejora de la resolución de sobrecargas y 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 con la microoptimización y 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 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 https://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 que los métodos, propiedades, operadores e indexadores que componen una única expresión se escriban de forma más escueta, al estilo de una expresión lambda:

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 inmovilizan un recurso de hilo 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, además de permitir tipos anónimos(Capítulo 4), que son clases simples 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, 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# 12 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.