C#

El nuevo y mejorado C# 6.0

Mark Michaelis

Aunque C# 6.0 aún no está completado, actualmente se encuentra en un punto en el que las características están casi finalizadas. Ha habido varios cambios y mejoras implementadas en C# 6.0 en la versión CTP3 de la próxima versión de Visual Studio, de nombre en clave "14", desde el artículo de mayo de 2014, "Anticipo del lenguaje C# 6.0" (msdn.microsoft.com/magazine/dn683793.aspx).

En este artículo, presentaré las nuevas características y comentaré las novedades de las características mencionadas en mayo. También mantendré al día un completo blog, donde describiré las novedades de cada característica de C# 6.0. Puede echarle un vistazo en itl.tc/csharp6. Muchos de estos ejemplos provienen de la próxima edición de mi libro "Essential C# 6.0" (Addison-Wesley Professional).

Operador condicional de NULL

Incluso los desarrolladores de .NET menos experimentados conocen la NullReferenceException. Esta es una excepción que casi siempre indica un error debido a que el desarrollador no realizó suficientes comprobaciones de NULL antes de invocar un miembro en un objeto (NULL). Considere este ejemplo: 

public static string Truncate(string value, int length)
{
  string result = value;
  if (value != null) // Skip empty string check for elucidation
  {
    result = value.Substring(0, Math.Min(value.Length, length));
  }
  return result;
}

Si no fuera por la comprobación de NULL, el método lanzaría una NullReferenceException. Aunque es simple, hacer una comprobación de NULL del parámetro de cadena es algo que requiere muchas palabras. A menudo, ese enfoque con tantas palabras es innecesario, dada la frecuencia de la comparación. C# 6.0 incluye un nuevo operador condicional de NULL que ayuda a escribir este tipo de comprobaciones de forma más concisa:

public static string Truncate(string value, int length)
{          
  return value?.Substring(0, Math.Min(value.Length, length));
}
[TestMethod]
public void Truncate_WithNull_ReturnsNull()
{
  Assert.AreEqual<string>(null, Truncate(null, 42));
}

Como muestra el método Truncate_WithNull_ReturnsNull, si el valor del objeto es realmente NULL, el operador condicional de NULL devolverá NULL. Esto nos puede hacer preguntarnos qué ocurre cuando el operador condicional de NULL aparece dentro de una cadena de llamada, como se muestra en el siguiente ejemplo:

public static string AdjustWidth(string value, int length)
{
  return value?.Substring(0, Math.Min(value.Length, length)).PadRight(length);
}
[TestMethod]
public void AdjustWidth_GivenInigoMontoya42_ReturnsInigoMontoyaExtended()
{
  Assert.AreEqual<int>(42, AdjustWidth("Inigo Montoya", 42).Length);
}

Aunque se llame a una subcadena por medio del operador condicional de NULL y un value?.Substring podría devolver aparentemente NULL, el comportamiento del lenguaje hace exactamente lo que usted desearía. Cortocircuita la llamada a PadRight e inmediatamente devuelve NULL, de forma que se impide el error de programación que de otra forma provocaría una NullReferenceException. Este concepto se conoce como propagación de tipo NULL.

El operador condicional de NULL realiza una comprobación de NULL de forma condicional antes de invocar el método de destino u otro método adicional dentro de la cadena de llamada. Potencialmente, esto podría producir un resultado sorprendente, como en la instrucción text?.Length.GetType.

Si el condicional de NULL devuelve NULL cuando el destino de invocación es NULL, ¿cuál es el tipo de dato resultante de la invocación de un miembro que devuelve un tipo de valor? (considerando que un tipo de valor no puede ser NULL) Por ejemplo, el tipo de dato que devuelve value?. La longitud no puede ser simplemente int. La respuesta, por supuesto, es un tipo de dato que acepta valores NULL (¿int?). De hecho, si se intenta asignar el resultado simplemente a un int, se producirá un error de compilación:

int length = text?.Length; // Compile Error: Cannot implicitly convert type 'int?' to 'int'

El condicional de NULL tiene dos sintaxis posibles. La primera es el signo de interrogación que precede al operador punto (?.). La segunda es la utilización del signo de interrogación con el operador índice. Por ejemplo, dada una colección, en lugar de realizar una comprobación de NULL de forma explícita antes de indizar dentro de la colección, es posible hacerlo por medio del operador condicional de NULL:

public static IEnumerable<T> GetValueTypeItems<T>(
  IList<T> collection, params int[] indexes)
  where T : struct
{
  foreach (int index in indexes)
  {
    T? item = collection?[index];
    if (item != null) yield return (T)item;
  }
}

Este ejemplo usa la forma de índice del condicional de NULL del operador ?[…], lo que consigue que la indización solo se produzca si la colección no es NULL. Con esta forma del operador condicional de NULL, la instrucción T? item = collection?[index] es equivalente en comportamiento a:

T? item = (collection != null) ? collection[index] : null.

Tenga presente que el operador condicional de NULL solo puede recuperar ítems. No servirá para asignar un ítem. De todas formas, ¿qué significaría eso, dada una colección NULL?

Tenga en cuenta la ambigüedad implícita al usar ?[…] en un tipo de referencia. Debido a que los tipos de referencia pueden ser NULL, un resultado NULL del operador ?[…] presenta una ambigüedad respecto a si la colección es NULL o es el elemento el que es NULL.

Una aplicación especialmente útil del operador condicional de NULL resuelve una idiosincrasia de C# que ha existido desde C# 1.0: la comprobación de NULL antes de invocar un delegado. Veamos el código en C# 2.0 de la Figura 1.

Figura 1. Comprobación de NULL antes de invocar un delegado

class Theremostat
{
  event EventHandler<float> OnTemperatureChanged;
  private int _Temperature;
  public int Temperature
  {
    get
    {
      return _Temperature;
    }
    set
    {
      // If there are any subscribers, then
      // notify them of changes in temperature
      EventHandler<float> localOnChanged =
        OnTemperatureChanged;
      if (localOnChanged != null)
      {
        _Temperature = value;
        // Call subscribers
        localOnChanged(this, value);
      }
    }
  }
}

Si se aprovecha el operador condicional de NULL, la implementación de conjuntos completa se reduce a simplemente:

OnTemperatureChanged?.Invoke(this, value)

Todo lo que necesita ahora es una llamada a Invoke precedida de un operador condicional de NULL. No necesitará asignar la instancia de delegado a una variable local para conseguir seguridad para subprocesos o incluso para comprobar explícitamente si el valor es NULL antes de invocar el delegado.

Los desarrolladores de C# se han estado preguntando si esto se iba a implementar en las últimas cuatro versiones. Por fin va a suceder. Esta característica por sí sola va a cambiar la forma en que invoca delegados.

Otro patrón común donde el operador condicional de NULL podría ser frecuente es en conjunto con el operador de combinación. En lugar de realizar una comprobación de NULL en linesOfCode antes de invocar Length, puede escribir un algoritmo de recuento de elementos como se explica a continuación:

List<string> linesOfCode = ParseSourceCodeFile("Program.cs");
return linesOfCode?.Count ?? 0;

En este caso, se normalizan cualquier colección vacía (sin artículos) y una colección NULL para devolver el mismo recuento. En resumen, el operador condicional de NULL:

  • Devolverá NULL si el operando es NULL
  • Cortocircuitará las invocaciones adicionales en la cadena de llamada si el operando es NULL
  • Devolverá un tipo que acepta valores NULL (System.Nullable<T>) si el miembro de destino devuelve un tipo de valor.
  • Admite invocación de delegados con seguridad para subprocesos.
  • Está disponible tanto como operador de miembro (?.) como operador de índice (?[…])

Inicializadores de propiedades automáticas

A cualquier desarrollador de .NET que haya implementado correctamente una estructura le ha molestado la cantidad de sintaxis que necesita para hacerla de tipo inmutable (como señalan los estándares de .NET que debería hacerse). Se plantea el hecho de que una propiedad de solo lectura debería tener:

  1. Un campo de respaldo definido como de solo lectura
  2. La inicialización del campo de respaldo desde dentro del constructor
  3. La implementación explícita de la propiedad (en lugar de la utilización de una propiedad automática)
  4. La implementación de un captador explícito que devuelva el campo de respaldo 

Todo esto solamente para implementar de manera "adecuada" una propiedad inmutable. Este comportamiento se repite después para todas las propiedades del tipo. De forma que hacer las cosas bien requiere un esfuerzo notablemente superior que el enfoque provisional. C# 6.0 llega al rescate con una nueva característica conocida como inicializadores de propiedades automáticas (CTP3 es también compatible con las expresiones de inicialización). El inicializador de propiedades automáticas permite la asignación de propiedades directamente dentro de su declaración. Para propiedades de solo lectura, se ocupa de todos los procedimientos que se requieren para garantizar que la propiedad es inmutable. Considere, por ejemplo, la clase FingerPrint de este ejemplo:

public class FingerPrint
{
  public DateTime TimeStamp { get; } = DateTime.UtcNow;
  public string User { get; } =
    System.Security.Principal.WindowsPrincipal.Current.Identity.Name;
  public string Process { get; } =
    System.Diagnostics.Process.GetCurrentProcess().ProcessName;
}

Como muestra el código, los inicializadores de propiedades permiten la asignación de un valor inicial a la propiedad como parte de la declaración de propiedades. La propiedad puede ser de solo lectura (solo un captador) o de lectura/escritura (un establecedor y un capturador). Cuando sea de solo lectura, el campo de respaldo subyacente se declara automáticamente con el modificador de solo lectura. Esto garantiza que sea inmutable a continuación de la inicialización.

Los inicializadores pueden ser cualquier expresión. Por ejemplo, al hacer uso del operador condicional, puede establecer un valor de inicialización por defecto:

public string Config { get; } = string.IsNullOrWhiteSpace(
  string connectionString =
    (string)Properties.Settings.Default.Context?["connectionString"])?
  connectionString : "<none>";

En este ejemplo, observe el uso de la expresión de declaración (véase itl.tc/?p=4040), como se comentó en el artículo anterior. Si necesita más de una expresión, podría refactorizar la inicialización en un método estático e invocarlo.

Expresiones nameof

Otra novedad introducida en la versión CTP3 es la compatibilidad con expresiones nameof. Hay varias ocasiones en las que necesitará usar "cadenas mágicas" dentro de su código. Dichas "cadenas mágicas" son cadenas de C# normales que se asignan a elementos del programa dentro de su código. Por ejemplo, cuando se lanza una ArgumentNullException, se usaría una cadena con el nombre del parámetro que no era válido. Lamentablemente, estas cadenas mágicas no disponen de validación en tiempo de compilación y cualquier cambio de elementos del programa (como un cambio de nombre del parámetro) no actualizaría automáticamente la cadena mágica, con la correspondiente incoherencia que nunca cogería el compilador.

En otras ocasiones, como cuando se generan eventos OnPropertyChanged, puede evitar la cadena mágica mediante ejercicios de árboles de expresión que extraigan el nombre. Tal vez es un poco más molesto dada la sencillez de la operación, que consiste simplemente en identificar el nombre del elemento del programa. En ambos casos, la solución no era la idónea.

Para ocuparse de esta idiosincrasia, C# 6.0 proporciona acceso a un nombre de "elemento del programa", ya sea un nombre de clase, de método, de parámetro o de un atributo específico (quizás usando reflexión). Por ejemplo, el código de la Figura 2 usa la expresión nameof para extraer el nombre del parámetro.

Figura 2. Extracción del nombre del parámetro con una expresión nameof

void ThrowArgumentNullExceptionUsingNameOf(string param1)
{
  throw new ArgumentNullException(nameof(param1));
}
[TestMethod]
public void NameOf_UsingNameofExpressionInArgumentNullException()
{
  try
  {
    ThrowArgumentNullExceptionUsingNameOf("data");
    Assert.Fail("This code should not be reached");
  }
  catch (ArgumentNullException exception)
  {
    Assert.AreEqual<string>("param1", exception.ParamName);
}

Como muestra el método de prueba, la propiedad ParamName de ArgumentNullException tiene el valor param1: un conjunto de valores que usan la expresión nameof(param1) en el método. La expresión nameof no está limitada a parámetros. Puede usarla para recuperar cualquier elemento de programación, como se muestra en la Figura 3.

Figura 3. Recuperación de otros elementos del programa

namespace CSharp6.Tests
{
  [TestClass]
  public class NameofTests
  {
    [TestMethod]
    public void Nameof_ExtractsName()
    {
      Assert.AreEqual<string>("NameofTests", nameof(NameofTests));
      Assert.AreEqual<string>("TestMethodAttribute",
        nameof(TestMethodAttribute));
      Assert.AreEqual<string>("TestMethodAttribute",
        nameof(
         Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute));
      Assert.AreEqual<string>("Nameof_ExtractsName",
        string.Format("{0}", nameof(Nameof_ExtractsName)));
      Assert.AreEqual<string>("Nameof_ExtractsName",
        string.Format("{0}", nameof(
        CSharp6.Tests.NameofTests.Nameof_ExtractsName)));
    }
  }
}

La expresión nameof solo recupera el identificador final, incluso si usa más nombres con puntos explícitos. También, en el caso de los atributos, no afecta al sufijo "Attribute". En su lugar, se requiere para la compilación. Da una buena posibilidad de limpiar código desordenado.

Constructores principales

Los inicializadores de propiedades automáticas son especialmente útiles en conjunto con constructores principales. Los constructores principales le proporcionan una forma de reducir los procedimientos en patrones de objetos comunes. Esta característica se ha mejorado de forma significativa desde mayo. Las actualizaciones incluyen:

  1. Un cuerpo de implementación opcional para el constructor principal: esto permite cosas como la validación e inicialización de parámetros de los constructores principales, que antes no era posible.
  2. Eliminación de los parámetros campo: declaración de campos mediante los parámetros del constructor principal. (No continuar con la forma en que esta característica estaba definida ha sido la decisión correcta, dado que ahora no obliga a seguir las convenciones de nomenclaturas específicas que hacían que C# fuera ambiguo).
  3. Compatibilidad con funciones y propiedades con cuerpo de expresión (se comentan más adelante en este artículo).

Con el predominio de los servicios web, aplicaciones de múltiples niveles, servicios de datos, API web, JSON y tecnologías similares, una forma de clase común es el objeto de transferencia de datos (DTO). Generalmente, el DTO no tiene mucho comportamiento de implementación, pero se centra en la simplicidad del almacenamiento de datos. Este enfoque en la simplicidad hace que los constructores principales resulten muy atractivos. Considere, por ejemplo, la estructura de datos inmutable Pair que se muestra en este ejemplo:

struct Pair<T>(T first, T second)
{
  public T First { get; } = first;
  public T Second { get; } = second;
  // Equality operator ...
}

La definición del constructor, Pair(string first, string second), se incorpora en la declaración de la clase. Esto especifica que los parámetros del constructor son first y second (cada uno de tipo T). A esos parámetros también se hace referencia en los inicializadores de propiedades y se asignan a sus propiedades correspondientes. Cuando observa la simplicidad de esta definición de clase, su compatibilidad con la inmutabilidad y el constructor requerido (un inicializador para todos los campos o propiedades), verá la forma en que le ayuda a codificar correctamente. Esto lleva a una mejora significativa en un patrón común que antes requería niveles de detalle innecesarios.

Los cuerpos de los constructores principales especifican el comportamiento del constructor principal. Esto le ayuda a implementar una funcionalidad equivalente en constructores principales y en constructores en general. Por ejemplo, el siguiente paso para mejorar la confiabilidad de la estructura de datos Pair<T> podría ser el de otorgarle validación de propiedades. Tal validación podría asegurar que un valor de NULL para Pair.First no sería válido. CTP3 ahora incluye un cuerpo de constructor principal: un cuerpo de constructor sin la declaración, como se muestra en la Figura 4.

Figura 4. Implementación de un cuerpo de constructor principal

struct Pair<T>(T first, T second)
{
  {
    if (first == null) throw new ArgumentNullException("first");
    First = first; // NOTE: Not working in CTP3
  }     
  public T First { get; }; // NOTE: Results in compile error for CTP3
  public T Second { get; } = second;
  public int CompareTo(T first, T second)
  {
    return first.CompareTo(First) + second.CompareTo(Second);
  }
// Equality operator ...
}

Para que se vea más claro, he colocado el cuerpo del constructor principal en el primer miembro de la clase. No obstante, no se trata de un requisito de C#. El cuerpo del constructor principal puede aparecer en cualquier orden en relación con los otros miembros de la clase.

Aunque no es funcional en CTP3, otra característica de las propiedades de solo lectura es que pueden asignarse directamente desde dentro del constructor (por ejemplo, First = first). Eso no está limitado a los constructores principales, sino que también está disponible para cualquier miembro constructor.

Una consecuencia interesante de la compatibilidad con los inicializadores de propiedades automáticas es que elimina muchos de los casos que se encuentran en versiones más iniciales en las que se necesitaban declaraciones de campos explícitas. El caso evidente que no elimina es una situación en la que se requiere la validación del establecedor. Por otra parte, la necesidad de declarar campos de solo lectura entra prácticamente en desuso. Ahora, cuando se declara un campo de solo lectura, puede declarar una propiedad automática de solo lectura posiblemente como privada, si se requiere tal nivel de encapsulación.

El método CompareTo tiene los parámetros first y second, que aparentemente se superponen a los nombres de los parámetros del constructor principal. Dado que los nombres de los constructores principales están en el ámbito dentro de los inicializadores de propiedad automática, first y second pueden parecer ambiguos. Afortunadamente, no es el caso. Las reglas de ámbito alcanzan una dimensión diferente, que no se ha visto antes en C#. 

Antes de C# 6.0, el ámbito siempre lo identificaba la colocación de las declaraciones de las variables dentro del código. Los parámetros se enlazan dentro del método que ayudan a declarar, los campos se enlazan dentro de la clase y las variables declaradas dentro de una instrucción if las enlaza el cuerpo de la condición de la instrucción if.

Por el contrario, los parámetros del constructor principal se enlazan por tiempo. Los parámetros del constructor principal solo están "vivos" mientras el constructor principal se está ejecutando. Este plazo de tiempo es obvio en el caso del cuerpo del constructor principal. Tal vez es menos obvio en el caso del inicializador de propiedades automáticas. 

En cualquier caso, tal y como ocurre con los inicializadores de campos traducidos en instrucciones que se ejecutan como parte de la inicialización de una clase en C# 1.0, los inicializadores de propiedades automáticas se implementan de la misma manera. En otras palabras, el ámbito de un parámetro de un constructor principal está ligado a la vida del inicializador de clase y al cuerpo del constructor principal. Cualquier referencia a los parámetros del constructor principal fuera de un inicializador de propiedades automáticas o del cuerpo de un constructor principal producirá un error de compilación.

Hay varios conceptos adicionales relacionados con los constructores principales que deben recordarse. Solo el primer constructor principal puede invocar el constructor base. Puede hacer esto usando la palabra clave base (contextual) a continuación de la declaración del constructor principal:

class UsbConnectionException(
  string message, Exception innerException, HidDeviceInfo hidDeviceInfo):
    Exception  (message, innerException)
{
  public HidDeviceInfo HidDeviceInfo { get;  } = hidDeviceInfo;
}

Si especifica constructores adicionales, la cadena de llamada del constructor debe invocar el constructor principal el último. Esto implica que un constructor principal no puede tener un inicializador this. Todos los demás constructores deben tenerlos, suponiendo que el constructor principal no es también el constructor por defecto:

public class Patent(string title, string yearOfPublication)
{
  public Patent(string title, string yearOfPublication,
    IEnumerable<string> inventors)
    ...this(title, yearOfPublication)
  {
    Inventors.AddRange(inventors);
  }
}

Espero que estos ejemplos ayuden a demostrar que los constructores principales aportan simplicidad a C#. Son una oportunidad más para hacer cosas simples de forma sencilla, en lugar de complicarlas. En algunos casos está justificado que las clases tengan constructores múltiples y cadenas de llamada que hacen el código difícil de leer. Si se encuentra un caso en el que la sintaxis del constructor principal hace que su código parezca más complejo en lugar de simplificarlo, no utilice los constructores principales. Si no le gusta alguna característica de las mejoras que aporta C# 6.0 o si hace su código más difícil de leer, simplemente no la use.

Funciones y propiedades con cuerpo de expresión

Las funciones con cuerpo de expresión son otra simplificación de la sintaxis de C# 6.0. Se trata de funciones sin cuerpo de instrucción. En su lugar, simplemente se implementan con una expresión que sigue a la declaración de la función.

Por ejemplo, se podría agregar un reemplazo de ToString a la clase Pair<T>:

public override string ToString() => string.Format("{0}, {1}", First, Second);

No hay nada especialmente radical en torno a las funciones con cuerpo de expresión. Tal y como sucede con la mayoría de características de C# 6.0, están ideadas para simplificar la sintaxis en los casos en los que la implementación es simple. Por supuesto, el tipo de devolución de la expresión debe coincidir con el tipo de devolución identificado en la declaración de la función. En este caso, ToString devuelve una cadena, de la misma forma que lo hace la expresión de implementación de la función. Los métodos que devuelven void o Task deberían implementarse con expresiones que tampoco devuelvan nada.

La simplificación con cuerpo de expresión no se limita a las funciones. También es posible implementar propiedades de solo lectura (solo captadores) utilizando expresiones (propiedades con cuerpo de expresión). Por ejemplo, puede agregar un miembro Text a la clase FingerPrint:

public string Text =>
  string.Format("{0}: {1} - {2} ({3})", TimeStamp, Process, Config, User);

Otras características

Hay muchas varias que ya no están planificadas para C# 6.0:

  • El operador de propiedades indizadas ($) ya no está disponible y no se espera para C# 6.0.
  • La sintaxis de miembro de índice no funciona en CTP3, aunque se espera que vuelva en una versión posterior de C# 6.0:
var cppHelloWorldProgram = new Dictionary<int, string>
{
[10] = "main() {",
[20] = "    printf(\"hello, world\")",
[30] = "}"
};
  • Los argumentos de campo en los constructores principales ya no forman parte de C# 6.0.
  • No es seguro que ni el literal numérico binario ni el separador numérico (‘_’) dentro de un literal numérico lleguen a estar implementados para el comienzo de la fabricación.

Hay varias características que no se comentan aquí, puesto que ya se cubrieron en el artículo de mayo; pero las instrucciones using estáticas (véase itl.tc/?p=4038), las expresiones de declaración (véase itl.tc/?p=4040) y las mejoras en el manejo de excepciones (véase itl.tc/?p=4042) son características que han permanecido estables.

Resumen

Claramente, los desarrolladores están entusiasmados con C# y quieren asegurarse de que mantiene su excelencia. El equipo del lenguaje está tomando muy en serio todos sus comentarios y modificando el lenguaje a medida que se procesa lo que los usuarios piensan. No dude en visitar roslyn.codeplex.com y comentarle al equipo sus opiniones. De igual manera, no olvide visitar itl.tc/csharp6 para estar al día en C# 6.0 hasta que se publique.


Mark Michaelis 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