Compartir a través de


C#

Cómo usar C# 6.0 para simplificar, aclarar y condensar su código

Mark Michaelis

C# 6.0 no representa ninguna revolución radical en la programación en C#. A diferencia de C# 2.0 y C# 3.0, con la introducción de métodos y clases genéricos y el innovador método para programar recopilaciones con LINQ, o de C# 5.0, con la simplificación de los patrones de programación asincrónicos, C# 6.0 no supone ninguna transformación drástica de desarrollo. No obstante, C# 6.0 sí que cambiará la forma de escribir código C# para determinados escenarios, ya que incluye características mucho más eficaces que harán que olvide cualquier otro método de escribir código existente para estos casos. C# 6.0 introduce nuevos accesos directos de sintaxis, reduce la redundancia en determinadas situaciones y, en última instancia, simplifica la escritura de código C#. En este artículo, analizaremos en profundidad los detalles del nuevo conjunto de características de C# 6.0 que hacen que esto sea posible. Haremos especial hincapié en los elementos destacados en el mapa mental que se muestra en la Ilustración 1.

Mapa mental de C# 6.0
Ilustración 1: Mapa mental de C# 6.0

Es necesario tener en cuenta que muchos de los ejemplos que se describen aquí están tomados de la próxima edición de mi libro "Essential C# 6.0" (Addison-Wesley Professional).

Using Static

Muchas de las características de C# 6.0 pueden usarse en la mayoría de los programas de consola básicos. Por ejemplo, using se admite ahora en clases específicas de una característica que se conoce como directiva using static, que representa los métodos estáticos disponibles en el ámbito global, sin prefijos de tipo, tal como se muestra en la Ilustración 2. Because System.Console es una clase estática que ofrece un excelente ejemplo de uso de esta característica.

Ilustración 2: Reducción del ruido del código con la directiva Using Static

using System;
using static System.ConsoleColor;
using static System.IO.Directory;
using static System.Threading.Interlocked;
using static System.Threading.Tasks.Parallel;
public static class Program
{
  // ...
  public static void Main(string[] args)
  {
    // Parameter checking eliminated for elucidation.
    EncryptFiles(args[0], args[1]);
  }
  public static int EncryptFiles(
    string directoryPath, string searchPattern = "*")
  {
    ConsoleColor color = ForegroundColor;
    int fileCount = 0;
    try
    {
      ForegroundColor = Yellow
      WriteLine("Start encryption...");
      string[] files = GetFiles(directoryPath, searchPattern,
        System.IO.SearchOption.AllDirectories);
      ForegroundColor = White
      ForEach(files, (filename) =>
      {
        Encrypt(filename);
        WriteLine("\t'{0}' encrypted", filename);
        Increment(ref fileCount);
      });
      ForegroundColor = Yellow
      WriteLine("Encryption completed");
    }
    finally
    {
      ForegroundColor = color;
    }
    return fileCount;
  }
}

En este ejemplo, pueden verse directivas using static para las clases System.ConsoleColor, System.IO.Directory, System.Threading.Interlocked y System.Threading.Tasks.Parallel. Estas directivas permiten invocar directamente numerosos métodos, propiedades y enumeraciones: ForegroundColor, WriteLine, GetFiles, Increment, Yellow, White y ForEach. En cada uno de estos casos, se elimina la necesidad de calificar el miembro estático junto con su tipo. (Para los que usen Visual Studio 2015 Preview o versiones anteriores, la sintaxis no incluye la adición de la palabra clave “static” después de using, por lo que basta con escribir por ejemplo “using System.Console”. Además, la directiva using static solo puede usarse para enumeraciones y estructuras, además de para clases estáticas, a partir de Visual Studio 2015 Preview.)

Para la mayoría, la eliminación del calificador de tipo no reduce en gran medida la claridad del código, a pesar de que la cantidad de código es menor. El uso de WriteLine en programas de consola resulta bastante obvio, al igual que la llamada a GetFiles. Además, puesto que la incorporación de la directiva using static en System.Threading.Tasks.Parallel era intencional, ForEach es una manera de definir un bucle foreach paralelo que, en cada versión de C#, se parece (si se mira desde el punto de vista adecuado) cada vez más a una instrucción foreach de C#.

La precaución obvia que ya que tomar con la directiva using static es la de no mermar la claridad. Observe, por ejemplo, la función Encrypt definida en la Ilustración 3.

Ilustración 3: Invocación de Exists ambigua (con el operador nameof)

private static void Encrypt(string filename)
  {
    if (!Exists(filename)) // LOGIC ERROR: Using Directory rather than File
    {
      throw new ArgumentException("The file does not exist.", 
        nameof(filename));
    }
    // ...
  }

Puede parece que la llamada a Exists es justo lo que necesitamos en este caso. Sin embargo, de manera explícita, la llamada es Directory.Exists cuando, en realidad, lo que se necesita es una llamada a File.Exists. En otras palabras, pese a ser legible, el código es incorrecto, por lo que, al menos en este caso, es preferible evitar la sintaxis using static.

Tenga en cuenta que si se especifican directivas using static para System.IO.Directory y System.IO.File, el compilador emitirá un error al llamar a Exists, lo que obliga a modificar el código con un prefijo que permita eliminar y resolver la ambigüedad.

Una característica adicional de la directiva using static es el comportamiento con los métodos de extensión. Los métodos de extensión no se mueven al ámbito global, como ocurriría normalmente con los métodos estáticos. Por ejemplo, una directiva using static ParallelEnumerable no pondría el método Select en el ámbito global, lo que implica la imposibilidad de llamar al método Select(files, (filename) => { ... }). Esta es una restricción estructural. En primer lugar, los métodos de extensión están pensados para representarse como métodos de instancia en un objeto (files.Select((filename)=>{ ... }), por ejemplo), por lo que su llamada como métodos estáticos directamente fuera del tipo no es un patrón normal. En segundo lugar, existen bibliotecas como, por ejemplo, System.Linq, con tipos como Enumerable y ParallelEnumerable con nombres de métodos que se solapan como Select. Agregar todos estos tipos al ámbito global genera una aglomeración innecesaria en IntelliSense e introduce, probablemente, invocaciones ambiguas (aunque no en el caso de las clases basadas en System.Linq).

Pese a que los métodos de extensión no se ubicarán en el ámbito global, C# 6.0 permite el uso de clases con métodos de extensión en directivas using static. La directiva using static permite lograr lo mismo que la directiva using (espacio de nombres) excepto por la clase específica dirigida por la directiva using static. En otras palabras, using static permite al desarrollador restringir los métodos de extensiones disponibles a la clase particular identificada, en lugar de a todo el espacio de nombres. Por ejemplo, veamos el fragmento de código de la ilustración 4.

Ilustración 4: Inclusión en el ámbito solo de los métodos de extensión de ParallelEnumerable

using static System.Linq.ParallelEnumerable;
using static System.IO.Console;
using static System.Threading.Interlocked;
// ...
    string[] files = GetFiles(directoryPath, searchPattern,
      System.IO.SearchOption.AllDirectories);
    files.AsParallel().ForAll( (filename) =>
    {
      Encrypt(filename);
      WriteLine($"\t'{filename}' encrypted");
      Increment(ref fileCount);
    });
// ...

Tenga en cuenta que en el fragmento de código no se instrucción using System.Linq. En su lugar, se usa la directiva using static System.Linq.ParallelEnumerable, que permite incluir en el ámbito solo los métodos de extensión de ParallelEnumerable como métodos de extensión. Por lo tanto, los métodos de extensión de las clases como, por ejemplo System.Linq.Enumerable, no estarán disponibles como métodos de extensión. Por ejemplo, se producirá un error de compilación con files.Select(...), puesto que Select no está en el ámbito de una matriz de cadena (o incluso IEnumerable<string>). Por el contrario, AsParallel está en el ámbito a través de System.Linq.ParallelEnumerable. En resumen, el uso de la directiva using static en clases con métodos de extensión incorporará los métodos de extensión de dichas clases al ámbito como métodos de extensión. (Los métodos que no sean de extensión de la misma clase se incluirán en el ámbito global con normalidad.)

En general, recomendamos limitar el uso de la directiva using static a un número reducido de clases usadas de forma repetitiva en todo el ámbito (a diferencia de Parallel) como, por ejemplo, System.Console o System.Math. Del mismo modo, cuando use using static para enumeraciones, asegúrese de que los elementos de la enumeración se expliquen por si solos sin su identificador de tipo. Por ejemplo, especifique using Microsoft.VisualStudio.TestTools.UnitTesting.Assert en archivos de pruebas unitarias para habilitar invocaciones de aserción de pruebas como IsTrue, AreEqual<T>, Fail e IsNotNull.

Operador nameof

La Ilustración 3 incluye otra nueva característica de C# 6.0: el operador nameof. Se trata de una nueva palabra clave contextual que permite identificar un valor literal de cadena que extrae un constante (durante la compilación) para el nombre no cualificado de cualquier identificador especificado como argumento. En la Ilustración 3, nameof(filename) devuelve “filename,” que corresponde al nombre del parámetro del método Encrypt. Sin embargo, nameof puede usarse con cualquier identificador creado con programación. Por ejemplo, en la Ilustración 5 se usa nameof para pasar el nombre de la propiedad a INotifyPropertyChanged.PropertyChanged. (De todas formas, el uso del atributo CallerMemberName para recuperar un nombre de propiedad para la invocación PropertyChanged sigue siendo un enfoque válido para la recuperación del nombre de la propiedad; consulte itl.tc/?p=11661.)

Ilustración 5: Uso del operador nameof Operator para INotifyPropertyChanged.PropertyChanged

public class Person : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  public Person(string name)
  {
    Name = name;
  }
  private string _Name;
  public string Name
  {
    get{ return _Name; }
    set
    {
      if (_Name != value)
      {
        _Name = value;
        PropertyChangedEventHandler propertyChanged = PropertyChanged;
        if (propertyChanged != null)
        {
          propertyChanged(this,
            new PropertyChangedEventArgs(nameof(Name)));
        }
      }
    }
  }
  // ...
}
[TestClass]
public class PersonTests
{
  [TestMethod]
  public void PropertyName()
  {
    bool called = false;
    Person person = new Person("Inigo Montoya");
    person.PropertyChanged += (sender, eventArgs) =>
    {
      AreEqual(nameof(CSharp6.Person.Name), eventArgs.PropertyName);
      called = true;
    };
    person.Name = "Princess Buttercup";
    IsTrue(called);
  }   
}

Tenga en cuenta que independientemente de si se proporciona solo el “nombre” no cualificado (puesto que no entra dentro del ámbito) como si se usa el identificador CSharp6.Person.Name completo en la prueba, el resultado será solo el identificador final (el último elemento del nombre separado por puntos).

Con el operador nameof se puede eliminar la gran mayoría de cadenas “mágicas” que hacen referencia a los identificadores de código siempre que formen parte del ámbito. Esto no solo elimina los errores de tiempo de ejecución provocados por errores de ortografía en las cadenas mágicas que nunca comprueba el compilador, sino que permite que herramientas de refactorización como, por ejemplo, Rename, actualicen todas las referencias al identificador de cambio de nombre. Además, si un nombre cambia sin herramienta de refactorización, el compilador generará un error que indica que el identificador ya no existe.

Interpolación de cadenas

El código de la ilustración 3 podría mejorarse no solo si se especifica un mensaje de excepción para indicar que no se encontró el archivo, sino mostrando el nombre del archivo en sí mismo. Antes de C# 6.0, esto se realizaba con la llamada string.Format, que permitía incrustar el nombre de archivo en el valor literal de cadena. Sin embargo, el formato compuesto no resulta especialmente legible. La asignación de formato al nombre de una persona, por ejemplo, requería usar marcadores de posición de sustitución basados en el orden de los parámetros, tal como se muestra en la asignación de mensaje de la Ilustración 6.

Ilustración 6: Formato de cadena compuesta frente a interpolación de cadenas

[TestMethod]
public void InterpolateString()
{
  Person person = new Person("Inigo", "Montoya") { Age = 42 };
  string message =
    string.Format("Hello!  My name is {0} {1} and I am {2} years old.",
    person.FirstName, person.LastName, person.Age);
  AreEqual<string>
    ("Hello!  My name is Inigo Montoya and I am 42 years old.", message);
  string messageInterpolated =
    $"Hello!  My name is {person.FirstName} {person.LastName} and I am
    {person.Age} years old.";
  AreEqual<string>(message, messageInterpolated);
}

Preste atención al enfoque alternativo del formato compuesto con la asignación a messageInterpolated. En este ejemplo, la expresión asignada a messageInterpolated es un valor literal de cadena escrito con el prefijo “$” y un código de identificación entre llaves que se incrusta en línea con la cadena. En este caso, se usan las propiedades de person para hacer que esta cadena resulte más legible que una cadena compuesta. Además, la sintaxis de interpolación de cadenas reduce los errores causados por los argumentos situados después de la cadena de formato en orden incorrecto o que faltan y, por ello, generan una excepción. (En Visual Studio 2015 Preview, no se usa el carácter $, lo que exige usar una barra diagonal antes de cada llave de apertura. Las versiones posteriores a Visual Studio 2015 Preview se han actualizado para permitir el uso del carácter $ delante de la sintaxis del valor literal de cadena.)

La interpolación de cadenas se transforma en el tiempo de compilación para invocar una llamada a string.Format equivalente. Esto proporciona la misma compatibilidad para la localización que con versiones anteriores (aunque con cadenas de formato tradicionales) y no introduce ninguna inyección de código posterior a la compilación mediante cadenas.

La Ilustración 7 muestra dos ejemplos de interpolación de cadenas.

Ilustración 7: Uso de interpolación de cadenas en lugar de string.Format

public Person(string firstName, string lastName, int? age=null)
{
  Name = $"{firstName} {lastName}";
  Age = age;
}
private static void Encrypt(string filename)
{
  if (!System.IO.File.Exists(filename))
  {
    throw new ArgumentException(
      $"The file, '{filename}', does not exist.", nameof(filename));
  }
  // ...
}

Tenga en cuenta que en el segundo caso, en la instrucción throw, se usan tanto la interpolación de cadenas como el operador nameof. La interpolación de cadenas es que hace que el mensaje ArgumentException incluya el nombre de archivo (es decir, “The file, ‘c:\data\missingfile.txt’ does not exist”). El uso del operador nameof permite identificar el nombre del parámetro Encrypt (“filename”), que es el segundo argumento del constructor ArgumentException. Visual Studio 2015 es compatible con la sintaxis de interpolación de cadenas, por lo que proporciona codificación de color e IntelliSense para los bloques de código incrustados en la cadena interpolada.

Operador condicional de NULL

Aunque en la Ilustración 2 se ha eliminado este operador para ofrecer mayor claridad, prácticamente todos los métodos Main que aceptan argumentos requieren comprobar el parámetro de null antes de invocar el miembro Length para determinar cuántos parámetros se han pasado. En general, este es un patrón muy usado para comprobar el operador null antes de invocar un miembro y, de este modo, evitar la excepción System.NullReferenceException (que casi siempre indica un error en la lógica de programación). Debido a la frecuencia de uso de este patrón, C# 6.0 introduce el operador “?.” conocido como operador condicional de null:

public static void Main(string[] args)
{
  switch (args?.Length)
  {
  // ...
  }
}

El operador condicional de null permite comprobar si el operador tiene el valor null antes de invocar el método o la propiedad (Length en este caso). El código explícito equivalente desde el punto de vista lógico sería (aunque en la sintaxis de C# 6.0 el valor de args solo se evalúa una vez):

(args != null) ? (int?)args.Length : null

Lo que hace que el uso del operador condicional de null resulte especialmente cómodo es su capacidad para el encadenamiento. Si, por ejemplo, invoca el código string[] names = person?.Name?.Split(' '), Split solo se invocará si person y person.Name no son null. Al encadenarse, si el primer operando es null, se producirá un cortocircuito en la evaluación de la expresión y no se ejecutará ninguna invocación más en la cadena de llamada de la expresión. No obstante, debe tener cuidado de no pasar por alto sin darse cuenta los demás operadores condicionales de null. Considere, por ejemplo, el código names = person?.Name.Split(' '). Si hay una instancia de person pero Name es null, se producirá una excepción NullReferenceException al invocar Split. Esto no implica que tenga que usar una cadena de operadores condicionales de null; sin embargo, debe usar la lógica de forma intencionada. En el caso de Person, por ejemplo, si se valida Name y nunca puede ser null, no será necesario ningún operador condicional de null adicional.

Un aspecto importante que debe tenerse en cuenta sobre el operador condicional null es que, cuando se usa con un miembro que devuelve un tipo de valor, siempre devuelve una versión de dicho tipo que admite valores null. Así por ejemplo, args?.Length devuelve int? y no simplemente int. Aunque quizá resulte peculiar (en comparación con el comportamiento de otros operadores), la devolución de un tipo de valor que admite valores null solo se produce al final de la cadena de llamada. Como resultado, la llamada al operador de punto (“.”) en Length solo permite invocar miembros de int (no de int?). Sin embargo, al encapsular args?.Length entre paréntesis (lo que fuerza el resultado int? a través de la precedencia del operador entre paréntesis) se invocará la devolución de int? y hará que los miembros específicos de Nullable<T> (HasValue y Value) estén disponibles.

El operador condicional null es una característica que ofrece importantes ventajas por sí sola. Sin embargo, si se usa en combinación con una invocación de delegados, permite resolver un punto débil de C# que ha existido desde C# 1.0. Observe cómo en la Ilustración 5 he asignado el controlador de eventos PropertyChange a una copia local (propertyChanged) antes de comprobar el valor null para, finalmente, activar el evento. Esta es la manera más sencilla de invocar eventos de manera segura para los subprocesos sin correr el riesgo de que se produzca una cancelación de suscripción de evento entre la comprobación de valores null y la activación del evento. Por desgracia, este proceso no es intuitivo y a menudo me encuentro con código que no sigue este patrón, lo que da lugar a excepciones NullReferenceExceptions incoherentes. Afortunadamente, este problema se resuelve con la introducción del operador condicional null en C# 6.0.

Con C# 6.0, el fragmento de código cambia de:

PropertyChangedEventHandler propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
  propertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));
}

A:

PropertyChanged?.Invoke(propertyChanged(
  this, new PropertyChangedEventArgs(nameof(Name)));

Además, puesto que un evento no es más que un delegado, siempre es posible aplicar el mismo patrón de invocar un delegado con el operador condicional null junto con Invoke. Más que cualquier otra característica de C# 6.0, seguro que cambiará su forma de escribir código C#. Cuando aprenda a usar operadores condicionales null en delegados, lo más probable es que nuca vuelva a escribir el código como antes (a menos, claro está, que esté anclado en el pasado sin C# 6.0).

Los operadores condicionales Null también pueden usarse combinados con un operador de índice. Por ejemplo, cuando se usan combinados con Newtonsoft.JObject, es posible atravesar objetos JSON para recuperar elementos específicos, tal como se muestra en la Ilustración 8.

Ilustración 8: Ejemplo de configuración de Color Configuration

string jsonText =
    @"{
      'ForegroundColor':  {
        'Error':  'Red',
        'Warning':  'Red',
        'Normal':  'Yellow',
        'Verbose':  'White'
      }
    }";
  JObject consoleColorConfiguration = JObject.Parse(jsonText);
  string colorText = consoleColorConfiguration[
    "ForegroundColor"]?["Normal"]?.Value<string>();
  ConsoleColor color;
  if (Enum.TryParse<ConsoleColor>(colorText, out color))
  {
    Console.ForegroundColor = colorText;
  }

Es importante tener en cuenta que, a diferencia de la mayoría de las recopilaciones de MSCORLIB, JObject no genera ninguna excepción si el índice no es válido. Si, por ejemplo, ForegroundColor no existe, JObject devuelve el valor null en lugar de generar una excepción. Esto es importante, ya que el uso del operador condicional null en recopilaciones que generan una excepción IndexOutOfRangeException es casi siempre innecesario y puede indicar seguridad cuando en realidad dicha seguridad no existe. Volviendo al fragmento de código con el ejemplo de Main y args, tenga en cuenta lo siguiente:

public static void Main(string[] args)
{
  string directoryPath = args?[0];
  string searchPattern = args?[1];
  // ...
}

Lo que hace que este ejemplo sea peligroso es que el operador condicional null da una falsa sensación de seguridad, lo que implica que si args no tiene el valor null, el elemento debe existir. Este no es nuestro caso, ya que el elemento puede no existir incluso si args no tiene el valor null. Puesto que la comprobación del recuento de elementos con args?.Length ya permite comprobar que args no tiene el valor null, nunca necesitará usar el operador condicional null al indexar la recopilación tras comprobar la longitud. En definitiva, evite usar el operador condicional null combinado con el operador de índice si este genera una excepción IndexOutOfRangeException para índices no existentes. Esto puede dar lugar a una falsa impresión de validez del código.

Constructores predeterminados en estructuras

Otra de las características de C# 6.0 que debemos conocer es la compatibilidad con constructores predeterminados (sin parámetros) en un tipo de valor. En versiones anteriores, esto no estaba permitido porque el constructor no se llamaba al inicializar las matrices, al establecer como predeterminado un campo de estructura de tipo o al inicializar una instancia con el operador predeterminado. Sin embargo, en C# 6.0, los constructores predeterminados no están permitidos con la salvedad de que solo se invocan cuando se crea una instancia del tipo de valor con el nuevo operador. Tanto la inicialización de matrices como la asignación explícita del valor predeterminado (o la inicialización implícita de un tipo de campo de estructura) sortearán el constructor predeterminado.

Para comprender cómo se usa el constructor predeterminado, preste atención al ejemplo de la clase ConsoleConfiguration que se muestra en la Ilustración 9. Las estructuras se inicializarán por completo según el constructor y su invocación a través del nuevo operador, tal como se muestra en el método CreateUsingNewIsInitialized. Tal como puede apreciarse en la Ilustración 9, el encadenamiento de constructores es completamente compatible y permite que un constructor llame a otro si se usa la palabra clave “this” después de la declaración del constructor.

Ilustración 9: Declaración de un constructor predeterminado en un tipo de valor

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
public struct ConsoleConfiguration
{
  public ConsoleConfiguration() :
    this(ConsoleColor.Red, ConsoleColor.Yellow, ConsoleColor.White)
  {
    Initialize(this);
  }
  public ConsoleConfiguration(ConsoleColor foregroundColorError,
    ConsoleColor foregroundColorInformation,
    ConsoleColor foregroundColorVerbose)
  {
    // All auto-properties and fields must be set before
    // accessing expression bodied members
    ForegroundColorError = foregroundColorError;
    ForegroundColorInformation = foregroundColorInformation;
    ForegroundColorVerbose = foregroundColorVerbose;
  }
   private static void Initialize(ConsoleConfiguration configuration)
  {
    // Load configuration from App.json.config file ...
  }
  public ConsoleColor ForegroundColorVerbose { get; }
  public ConsoleColor ForegroundColorInformation { get; }
  public ConsoleColor ForegroundColorError { get; }
  // ...
  // Equality implementation excluded for elucidation
}
[TestClass]
public class ConsoleConfigurationTests
{
  [TestMethod]
  public void DefaultObjectIsNotInitialized()
  {
    ConsoleConfiguration configuration = default(ConsoleConfiguration);
    AreEqual<ConsoleColor>(0, configuration.ForegroundColorError);
    ConsoleConfiguration[] configurations = new ConsoleConfiguration[42];
    foreach(ConsoleConfiguration item in configurations)
    {
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorError);
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorInformation);
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorVerbose);
    }
  }
  [TestMethod]
  public void CreateUsingNewIsInitialized()
  {
    ConsoleConfiguration configuration = new ConsoleConfiguration();
    AreEqual<ConsoleColor>(ConsoleColor.Red,
      configuration.ForegroundColorError);
    AreEqual<ConsoleColor>(ConsoleColor.Yellow,
      configuration.ForegroundColorInformation);
    AreEqual<ConsoleColor>(ConsoleColor.White,
      configuration.ForegroundColorVerbose);
  }
}

Existe un punto clave sobre las estructuras que siempre debe tenerse en cuenta: todos los campos de instancia y propiedades automáticas (puesto que tienen campos de respaldo) deben estar completamente inicializados antes de invocar a ningún otro miembro de instancia. Como resultado, en el ejemplo que se muestra en la Ilustración 9, el constructor no podrá llamar al método Initialize hasta que no se haya asignado todos los campos y propiedades automáticas. Afortunadamente, si un constructor encadenado controla la inicialización de todos los requisitos y se invoca mediante “this”, el compilador detectará que no es necesario inicializar nuevamente los datos del cuerpo del constructor no invocado mediante “this”, tal como se muestra en la Ilustración 9.

Mejoras de propiedades automáticas

En la Ilustración 9, las tres propiedades (para las que no hay campos explícitos) están declaradas como propiedades automáticas (sin cuerpo) y solo un captador. Estas propiedades automáticas con solo un captador son una característica de C# 6.0 que permite declarar propiedades de solo lectura respaldadas (internamente) por un campo de solo lectura. Por lo tanto, estas propiedades solo pueden modificarse desde el constructor.

Las propiedades automáticas con solo un captador están disponibles en las declaraciones de estructuras y de clases, aunque son muy importantes para las estructuras debido a la directriz recomendada que sostiene que las estructuras deben ser inmutables. En lugar de las aproximadamente seis líneas que se necesitaban antes de C# 6.0 para declarar una propiedad de solo lectura e inicializarla, ahora solo se necesita una declaración de una sola línea junto con la asignación desde el constructor. Esto convierte a la declaración de estructuras inmutables en no solo el patrón de programación correcto para las estructuras, sino en el patrón más sencillo (un cambio que se agradece si tenemos en cuenta que con la sintaxis de versiones anteriores el esfuerzo para realizar una codificación correcta era mucho mayor).

Otra característica de propiedad automática introducida en C# 6.0 es la compatibilidad con los inicializadores. Por ejemplo, podemos agregar a ConsoleConfiguration una propiedad automática DefaultConfig estática con inicializador:

// Instance property initialization not allowed on structs.
static private Lazy<ConsoleConfiguration> DefaultConfig{ get; } =
  new Lazy<ConsoleConfiguration>(() => new ConsoleConfiguration());

Esta propiedad proporciona un patrón de fábrica de una única instancia para el acceso a la instancia de ConsoleConfigurtion predeterminada. Tenga en cuenta que en lugar de asignar la propiedad automática de solo captador desde el constructor, este ejemplo usa System.Lazy<T> y crea una instancia como inicializador durante la declaración. Como resultado, una vez completado el constructor, la instancia de Lazy<ConsoleConfiguration> será inmutable y la invocación de DefaultConfig siempre devolverá la misma instancia de ConsoleConfiguration.

Tenga en cuenta que los inicializadores de propiedad automática no están permitidos en miembros de instancia de estructuras (aunque seguro que están permitidos en las clases).

Métodos y propiedades automáticas con cuerpo de expresión

Otra de las características introducidas en C# 6.0 son los miembros con cuerpo de expresión. Esta característica existe para propiedades y métodos. Además, permite usar el operador de flecha (=>) para asignar una expresión a una propiedad o método en lugar de un cuerpo de instrucción. Por ejemplo, debido a que la propiedad DefaultConfig del ejemplo anterior es privada y del tipo Lazy<T>, necesitará el método GetDefault para recuperar la instancia predeterminada real de ConsoleConfiguration:

static public ConsoleConfiguration GetDefault() => DefaultConfig.Value;

Sin embargo, tenga en cuenta que en este fragmento de código no hay cuerpo de método de tipo bloque. En su lugar, el método se implementa solo con una expresión (y no con una instrucción) con el operador de flecha lambda como prefijo. El objetivo es ofrecer de manera directa una implementación sencilla de una línea que sea funcional en la firma del método con o sin parámetros:

private static void LogExceptions(ReadOnlyCollection<Exception> innerExceptions) =>
  LogExceptionsAsync(innerExceptions).Wait();

Con respecto a las propiedades, tenga en cuenta que los cuerpos de las expresiones solo funcionan con propiedades de solo lectura (de solo captador). De hecho, la sintaxis es prácticamente idéntica a la de los métodos con cuerpo de expresión, con la diferencia de que no hay paréntesis después del identificador. Si volvemos al ejemplo de Person anterior, podemos implementar las propiedades de solo lectura FirstName y LastName con cuerpos de expresión, tal como se muestra en la Ilustración 10.

Ilustración 10: Propiedades automáticas con cuerpo de expresión

public class Person
{
  public Person(string name)
  {
    Name = name;
  }
  public Person(string firstName, string lastName)
  {
    Name = $"{firstName} {lastName}";
    Age = age;
  }
  // Validation ommitted for elucidation
  public string Name {get; set; }
  public string FirstName => Name.Split(' ')[0];
  public string LastName => Name.Split(' ')[1];
  public override string ToString() => "\{Name}(\{Age}";
}

Además, las propiedades con cuerpo de expresión también pueden usarse en miembros de índice, lo que devolvería un elemento para una recopilación interna, por ejemplo.

Inicializador de diccionario

Las recopilaciones de tipo diccionario son una buena forma de definir pares de valores de nombre. Por desgracia, la sintaxis de la inicialización está poco optimizada:

{ {"First", "Value1"}, {"Second", "Value2"}, {"Third", "Value3"} }

Para mejorar esta situación, C# 6.0 incluye una nueva sintaxis de tipo de asignación de diccionario:

Dictionary<string, Action<ConsoleColor>> colorMap =
  new Dictionary<string, Action<ConsoleColor>>
{
  ["Error"] =               ConsoleColor.Red,
  ["Information"] =        ConsoleColor.Yellow,
  ["Verbose"] =            ConsoleColor.White
};

Para mejorar la sintaxis, el equipo responsable del lenguaje introdujo el operador de asignación que permite asociar una pareja de elementos que componen una pareja de valores de búsqueda (nombre) o asignación. La búsqueda corresponde al valor de índice (y al tipo de datos) declarado para el diccionario.

Mejoras en las excepciones

Como no podía ser menos, en C# 6.0 también se ha mejorado el lenguaje de las excepciones. En primer lugar, ahora se pueden usar cláusulas await tanto en bloques catch como finally, tal como se muestra en la Ilustración 11.

Ilustración 11: Uso de Await en bloques Catch y Finally

public static async Task<int> EncryptFilesAsync(string directoryPath, string searchPattern = "*")
{
  ConsoleColor color = Console.ForegroundColor;
  try
  {
  // ...
  }
  catch (System.ComponentModel.Win32Exception exception)
    if (exception.NativeErrorCode == 0x00042)
  {
    // ...
  }
  catch (AggregateException exception)
  {
    await LogExceptionsAsync(exception.InnerExceptions);
  }
  finally
  {
    Console.ForegroundColor = color;
    await RemoveTemporaryFilesAsync();
  }
}

Desde la introducción de await en C# 5.0, la posibilidad de usar await en bloques catch y finally ha resultado ser mucho más útil de lo esperado. Por ejemplo, el patrón para invocar métodos asincrónicos desde bloques catch o finally es bastante común, sobre todo a la hora de realizar acciones de limpieza o registro durante estos periodos. Esto es posible C# 6.0.

La segunda característica relacionada con las excepciones (que ha estado disponible en Visual Basic desde la versión 1.0) es la compatibilidad con los filtros de excepción que, además de filtrar por un tipo de excepción determinado, permiten especificar una cláusula if para restringir aún más si la excepción la detectará el bloque catch o no. (En ocasiones, esta característica también se ha usado para realizar tareas secundarias como, por ejemplo, el registro de excepciones como excepciones del tipo “flies by” sin realizar ningún procesamiento de excepción.)  Una precaución que debe tenerse con esta característica es la de evitar, en caso de que vaya a localizar la aplicación, expresiones condicionales catch que operen mediante mensajes de excepción, puesto que estas no funcionarán a no ser que realice cambios tras la localización.

Resumen

Como nota final válida para todas las características de C# 6.0, hay que puntualizar que aunque requieren el compilador de C# 6.0 incluido en Visual Studio 2015 o posterior, no necesitan ninguna versión actualizada de Microsoft .NET Framework. Por lo tanto, podrá usar las características de C# 6.0 incluso cuando vaya a realizar la compilación con .NET Framework 4, por ejemplo. La razón por la que esto es posible es que todas las características están implementadas en el compilador y no tienen ninguna dependencia de .NET Framework.

Este breve pero intenso análisis cierra el presente análisis de C# 6.0. Las únicas dos características no abordadas son la posibilidad de definir un método de extensión Add personalizado para ayudar con los inicializadores de recopilaciones y la característica de resolución de sobrecargas. En resumen, C# 6.0 no implica ningún cambio radical para el código, al menos no como lo supusieron los genéricos o los operadores LINQ. No obstante, C# 6.0 sí que simplifica los patrones de codificación. El operador condicional null en el uso de delegados probablemente sea el mejor ejemplo; aunque también lo son muchas otras características, entre las que se incluyen la interpolación de cadenas, el operador nameof y las mejoras de propiedades (sobre todo para las propiedades de solo lectura).

A continuación se proporciona referencias adicionales que podrá consultar para más información:

  • Novedades de C# 6.0, por Mads Togersen (vídeo): bit.ly/CSharp6Mads
  • Blog de C# de Mark Michaelis, que incorpora las actualizaciones de la versión 6.0 realizadas desde la redacción del presente artículo: itl.tc/csharp
  • Debates sobre el lenguaje C# 6.0: roslyn.codeplex.com/discussions

También puede consultar la siguiente versión de mi libro “Essential C# 6.0” (intellitect.com/EssentialCSharp), aunque esta no estará disponible hasta el segundo trimestre de 2015.

Para cuando lea este documento, probablemente los debates sobre las características de C# 6.0 ya estén cerrados. Sin embargo, Microsoft se está renovando. Prueba de ello es su compromiso de inversión en desarrollo entre plataformas aplicando prácticas recomendadas de código abierto que permiten a la comunidad de desarrolladores compartir opiniones relacionadas con la creación de software. Por esta razón, pronto podrá leer debates sobre el diseño de C# 7.0, ya que estos debates tendrán lugar en un foro de código abierto.


Mark Michaelis (itl.tc/Mark) es el fundador de IntelliTect. También trabaja de arquitecto técnico jefe y formador. Desde 1996, ha sido Microsoft MVP para C#, Visual Studio Team System (VSTS) y Windows SDK, y fue nombrado Director regional de Microsoft en 2007. También trabaja en varios equipos de revisión de diseño de software de Microsoft, como C#, la División de sistemas conectados y VSTS. Michaelis imparte conferencias para desarrolladores y ha escrito numerosos artículos y libros. Actualmente trabaja en la próxima edición de "Essential C#" (Addison-Wesley Professional).

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Mads Torgersen