Capítulo 1. Introducción a C# y .NET Core

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 marcos de trabajo 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; 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 facilita la definición de campos y propiedades de sólo lectura para escribir tipos inmutables (de 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

Históricamente, C# se utilizaba casi exclusivamente para escribir código que se ejecutara en plataformas Windows. Sin embargo, Microsoft y otras empresas han invertido desde entonces en otras plataformas:

  • El .NET Core Framework permite el desarrollo de aplicaciones web en Linux y macOS (además de Windows).

  • Xamarin permite el desarrollo de aplicaciones móviles para iOS y Android.

  • Blazor compila C# a ensamblado web que puede ejecutarse en un navegador.

Y en la plataforma Windows:

  • .NET Core 3 permite desarrollar aplicaciones web y de cliente enriquecido en Windows 7 a 10.

  • La Plataforma Universal de Windows (UWP) es compatible con el escritorio de Windows 10 y con dispositivos como Xbox, Surface Hub y Hololens.

C# y el Common Language Runtime

C# depende de un Common Language Runtime (CLR), que proporciona servicios esenciales de 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 X86 o X64, normalmente justo antes de la ejecución. Esto se denomina compilación Justo a Tiempo (JIT). La compilación anticipada 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 de iOS al desarrollar con Xamarin).

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 JetBrains dotPeek, puedes ir más allá y descompilar el IL a C#. Como el IL es de más alto nivel que el código máquina nativo, el descompilador puede hacer un trabajo bastante bueno reconstruyendo el C# original.

Un programa puede consultar sus propios metadatos(reflexión) e incluso generar nuevo IL en tiempo de ejecución(Reflection.Emit).

Marcos y bibliotecas de clases base

Un CLR no se distribuye por sí solo, sino como parte de un framework que incluye un conjunto estándar de ensamblados. Cuando escribes una aplicación, te diriges a un marco concreto, lo que significa que tu aplicación utiliza y depende de la funcionalidad que proporciona el marco. Tu elección del marco también determina las plataformas que soportará tu aplicación.

Un framework consta de tres capas, como se ilustra en la Figura 1-1. Las Bibliotecas de Clases Base (BCL) se asientan sobre el CLR, proporcionando características útiles para cualquier tipo de aplicación (como colecciones, XML/JSON, entrada/salida [E/S], redes, serialización y programación paralela). Encima de la CLR están las capas del marco de aplicación, que proporcionan las API para un paradigma de interfaz de usuario (como ASP.NET Core para una aplicación web, o Windows Presentation Foundation [WPF] para una aplicación de cliente enriquecido). Un programa de línea de comandos no requiere una capa de aplicación.

Framework architecture
Figura 1-1. Arquitectura del marco

Cuando se lanzó C# por primera vez en 2000, sólo existía el Microsoft .NET Framework. Ahora hay cuatro grandes opciones de marco:

.NET Core
Moderno marco de código abierto para escribir aplicaciones web y de consola que se ejecutan en Windows, Linux y macOS, y aplicaciones de cliente enriquecido que se ejecutan en Windows 7 a 10 (con .NET Core 3+). Puedes instalar varias versiones de .NET Core una al lado de la otra, y las aplicaciones pueden ser autónomas, para no requerir una instalación de .NET Core.
UWP
Para escribir aplicaciones táctiles inmersivas que se ejecutan en el escritorio y los dispositivos de Windows 10 (Xbox, Surface Hub y Hololens). Las aplicaciones UWP están aisladas y se envían a través de la Tienda Windows. La UWP viene preinstalada con Windows 10.
Mono + Xamarin
Framework de código abierto para escribir aplicaciones móviles que funcionen en iOS y Android.
.NET Framework (sustituido por .NET Core 3)
Para escribir aplicaciones web y de cliente enriquecido dirigidas al 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. .NET Framework está preinstalado en Windows y es compatible con C# 7.3 y versiones anteriores.

Aunque cada uno de estos marcos difiere en su soporte de plataforma y usos previstos, todos exponen un CLR y una BCL similares.

Nota

Puedes aprovechar esta característica común y escribir bibliotecas de clases que funcionen en varios marcos de trabajo: consulta ".NET Estándar" en el capítulo 5.

Este libro se centra en C# y en la funcionalidad principal del CLR y la BCL, como se muestra en la Figura 1-2. Aunque el énfasis principal está en .NET Core 3, también cubrimos algunos de los tipos de Windows Runtime para aplicaciones UWP que proporcionan funcionalidad en paralelo a la BCL.

Topics covered in this book—the applications frameworks (shown in gray) are not covered
Figura 1-2. Temas tratados en este libro: los marcos de aplicación (en gris) no se tratan

Marcos tradicionales y especializados

Los siguientes frameworks siguen estando disponibles para plataformas más antiguas:

  • Tiempo de ejecución de Windows para Windows 8/8.1 (ahora UWP)

  • Microsoft XNA para el desarrollo de juegos (ahora UWP)

  • .NET Core 1.x y 2.x (sólo para aplicaciones web y de línea de comandos)

También existen los siguientes marcos especializados:

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

  • Mono (sobre el que se asienta Xamarin) también tiene una capa de aplicación para desarrollar aplicaciones "Windows Forms" de escritorio multiplataforma en Linux, macOS y Windows. No todas las funciones son compatibles ni funcionan plenamente. (Otra opción para el desarrollo de interfaces de usuario [UI] multiplataforma es Avalonia, que es una biblioteca inspirada en WPF que se ejecuta sobre .NET Core y .NET Framework).

  • 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.

Tiempo de ejecución de Windows

C# también interopera con la tecnología Windows Runtime (WinRT). WinRT significa dos cosas:

  • Una interfaz de ejecución orientada a objetos neutra en cuanto al lenguaje y compatible con Windows 8 y versiones superiores

  • Un conjunto de bibliotecas integradas en Windows 8 y superior que admiten esta interfaz de ejecución

Nota

De forma un tanto confusa, el término WinRT se ha utilizado históricamente para significar dos cosas más:

  • El predecesor de UWP; es decir, la plataforma de desarrollo para escribir aplicaciones de la Tienda para Windows 8/8.1, a veces llamada "Metro" o "Modern".

  • El extinto sistema operativo móvil para tabletas basadas en RISC ("Windows RT") que Microsoft lanzó en 2011

Por interfaz de ejecución, entendemos un protocolo para llamar a código escrito (potencialmente) en otro lenguaje. Microsoft Windows ha proporcionado históricamente una interfaz de ejecución primitiva en forma de llamadas a funciones de bajo nivel de estilo C que comprenden la API Win32.

WinRT es mucho más rico. En parte, es una versión mejorada del Modelo de Objetos Componentes (COM) compatible con .NET, C++ y JavaScript. A diferencia de Win32, está orientado a objetos y tiene un sistema de tipos relativamente rico. Esto significa que hacer referencia a una biblioteca WinRT desde C# se parece mucho a hacer referencia a una biblioteca .NET: puede que ni siquiera seas consciente de que estás utilizando WinRT.

Las bibliotecas WinRT de Windows 10 forman una parte esencial de la plataforma UWP (UWP depende tanto de las bibliotecas WinRT como de las bibliotecas .NET Core). Si tu objetivo es la plataforma .NET Core estándar, hacer referencia a las bibliotecas WinRT de Windows 10 es opcional y puede ser útil si necesitas acceder a funciones específicas de Windows 10 que no estén cubiertas de otro modo en .NET Core.

Las bibliotecas WinRT de Windows 10 son compatibles con la UWP UI para escribir aplicaciones inmersivas con prioridad táctil. También admiten funciones específicas de dispositivos móviles, como sensores, mensajes de texto, etc. (la nueva funcionalidad de Window 8, 8.1 y 10 se ha expuesto a través de WinRT en lugar de Win32). Las bibliotecas WinRT también proporcionan E/S de archivos adaptadas para funcionar bien dentro del sandbox de la UWP.

Lo que distingue a WinRT del COM ordinario es que WinRT proyecta sus bibliotecas en multitud de lenguajes, concretamente C#, Visual Basic, C++ y JavaScript, de modo que cada lenguaje ve los tipos de WinRT (casi) como si hubieran sido escritos especialmente para él. Por ejemplo, WinRT adaptará las reglas de uso de mayúsculas a los estándares del lenguaje de destino e incluso reasignará algunas funciones e interfaces. Los ensamblados WinRT también se envían con metadatos enriquecidos en archivos .winmd, que tienen el mismo formato que los archivos de ensamblado .NET, lo que permite un consumo transparente sin ritual especial; por eso es posible que no sepas que estás utilizando tipos WinRT en lugar de .NET, aparte de las diferencias de espacio de nombres. Otra pista es que los tipos WinRT están sujetos a restricciones del estilo COM; por ejemplo, ofrecen un soporte limitado para la herencia y los genéricos.

En C#, no sólo puedes consumir bibliotecas WinRT, sino que también puedes escribir las tuyas propias (y llamarlas desde una aplicación JavaScript).

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

C# 8.0 se suministra con Visual Studio 2019.

Í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 (C# 8)" en el capítulo 2.

Asignación nulo-coalescente

El operador ??= asigna una variable sólo si es nula. En lugar de esto

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, además de permitir que el método local declare 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:

ILogger.Prefix = "File log: ";

a menos que se restrinja mediante un modificador de accesibilidad en el miembro estático de la interfaz (como private, protected o internal). Los campos de instancia están prohibidos.

Para más detalles, consulta "Miembros por defecto de la interfaz (C# 8)" en el capítulo 3.

expresiones de conmutación

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 conmutación (C# 8)" en el capítulo 2.

Patrones de tupla, posicionales y de propiedad

C# 8 admite tres nuevos patrones, principalmente en beneficio de las declaraciones/expresiones switch (consulta "Patrones" en el Capítulo 4). Los patrones de tupla te permiten activar varios 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 objetos que exponen un deconstructor, y los patrones de propiedades te permiten coincidir en las propiedades de un objeto. Puedes utilizar todos los patrones tanto en interruptores como mediante 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 valor Null​ReferenceException.

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 Null​ReferenceException 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 (C# 8)" en el capítulo 4.

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 mediante la introducción de 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 (C# 8)" en el capítulo 14.

Novedades en C# 7.x

C# 7 viene con Visual Studio 2017.

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 una pila puede asignarse directamente a un Span<T>. Describimos los espacios -y por qué los utilizarías- en el capítulo 24.

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" y "Ref Devoluciones" en el Capítulo 2, y "Ref Estructuras" en el Capítulo 3.

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 normas 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" en el Capítulo 2):

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 denominan variables patrón (consulta "Introducir una variable patrón" en el capítulo 3):

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 (consulta "Conmutación de tipos" en el Capítulo 2). 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 (consulta "Métodos locales" en el capítulo 3):

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 (consulta "Deconstructores" en el Capítulo 3). 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 la gestión 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 (consulta "Tuplas" en el Capítulo 4). 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 envió 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 (consulta el Capítulo 27). El propio compilador es de código abierto, y el código fuente está disponible en GitHub.

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" en el Capítulo 2) 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" en el capítulo 3) 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" en el Capítulo 2) ofrece una alternativa sucinta a string.Format:

string s = $"It is {DateTime.Now.DayOfWeek} today";

Los filtros de excepción (consulta "Sentencias try y excepciones" en el Capítulo 4) te permiten aplicar una condición a un bloque catch:

string html;
try
{
  html = new WebClient().DownloadString ("http://asef");
}
catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout)
{
  ...
}

La directiva using static (véase "Espacios de nombres" en el Capítulo 2) 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 las 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.

En el capítulo 14 trataremos en detalle las funciones asíncronas.

Novedades en C# 4.0

C# 4.0 introdujo cuatro mejoras importantes:

  • Lavinculación dinámica (Capítulos 4 y 20) 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 a las funciones especificar valores de parámetros por defecto, de modo que quien llama puede omitir argumentos, y los argumentos con nombre permiten a quien llama a una función identificar 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 25) 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 junto con parámetros opcionales). En segundo lugar, los conjuntos que contienen tipos de interoperabilidad COM pueden vincularse en lugar de referenciarse. Los tipos de interoperabilidad enlazados admiten la equivalencia de tipos, lo que evita la necesidad de ensamblados 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 de tipos de interoperabilidad enlazados se asignan a dynamic en lugar de a object, eliminando 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 deextensió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 de forma remota (por ejemplo, en un servidor de base de datos) porque 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 se "funden" 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ítulo 4), 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# 8.0 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.