Expresión de la intención

Completado

En la unidad anterior, ha aprendido cómo el compilador de C# puede realizar análisis estáticos para ayudar a protegerse contra NullReferenceException. También ha aprendido a habilitar un contexto que acepta valores NULL. En esta unidad, aprenderá más sobre cómo expresar explícitamente la intención en un contexto que acepta valores NULL.

Declaración de variables

Con un contexto que acepta valores NULL habilitado, tiene más visibilidad sobre cómo el compilador ve el código. Puede actuar sobre las advertencias generadas a partir de un contexto que acepta valores NULL y, al hacerlo, se definen explícitamente sus intenciones. Por ejemplo, vamos a seguir examinando el código FooBar y a examinar la declaración y la asignación:

// Define as nullable
FooBar? fooBar = null;

Tenga en cuenta el ? agregado a FooBar. Esto indica al compilador que se pretende explícitamente que fooBar acepte valores NULL. Si no pretende que fooBar admita valores NULL, pero quiere evitar la advertencia, tenga en cuenta lo siguiente:

// Define as non-nullable, but tell compiler to ignore warning
// Same as FooBar fooBar = default!;
FooBar fooBar = null!;

En este ejemplo se agrega el operador que admite valores NULL (!) a null, que indica al compilador que va a inicializar explícitamente esta variable como NULL. El compilador no emitirá advertencias sobre esta referencia como NULL.

Un procedimiento recomendado es asignar valores no null a las variables que no aceptan valores NULL cuando se declaran, si es posible:

// Define as non-nullable, assign using 'new' keyword
FooBar fooBar = new(Id: 1, Name: "Foo");

Operadores

Como se describe en la unidad anterior, C# define varios operadores para expresar su intención en relación a tipos de referencia que aceptan valores NULL.

Operador que acepta valores NULL (!)

En la sección anterior se presentó el operador que acepta valores NULL (!). Indica al compilador que ignore la advertencia CS8600. Esta es una manera de transmitir al compilador que sabe lo que está haciendo, pero incluye la advertencia de que realmente debe saber lo que está haciendo.

Al inicializar tipos que no aceptan valores NULL mientras se habilita un contexto que acepta valores NULL, es posible que tenga que pedir la aceptación explícitamente al compilador. Por ejemplo, considere el siguiente código:

#nullable enable

using System.Collections.Generic;

var fooList = new List<FooBar>
{
    new(Id: 1, Name: "Foo"),
    new(Id: 2, Name: "Bar")
};

FooBar fooBar = fooList.Find(f => f.Name == "Bar");

// The FooBar type definition for example.
record FooBar(int Id, string Name);

En el ejemplo anterior, FooBar fooBar = fooList.Find(f => f.Name == "Bar"); genera una advertencia CS8600 porque Find podría devolver null. Este posible null se asignaría a fooBar, que no acepta valores NULL en este contexto. Sin embargo, en este ejemplo inventado, sabemos que Find nunca devolverá null como se ha escrito. Puede expresar esta intención al compilador con el operador que acepta valores NULL:

FooBar fooBar = fooList.Find(f => f.Name == "Bar")!;

Observe ! al final de fooList.Find(f => f.Name == "Bar"). Esto indica al compilador que sabe que el objeto devuelto por el método Find podría ser null, y habría ningún problema.

El operador que admite valores NULL también se puede aplicar a un objeto insertado antes de una evaluación de propiedad o de una llamada de método. Considere otro ejemplo inventado:

List<FooBar>? fooList = FooListFactory.GetFooList();

// Declare variable and assign it as null.
FooBar fooBar = fooList.Find(f => f.Name == "Bar")!; // generates warning

static class FooListFactory
{
    public static List<FooBar>? GetFooList() =>
        new List<FooBar>
        {
            new(Id: 1, Name: "Foo"),
            new(Id: 2, Name: "Bar")
        };
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

En el ejemplo anterior:

  • GetFooList es un método estático que devuelve un tipo que acepta valores NULL, List<FooBar>?.
  • A fooList se le asigna el valor devuelto por GetFooList.
  • El compilador genera una advertencia en fooList.Find(f => f.Name == "Bar"); porque el valor asignado a fooList podría ser null.
  • Suponiendo que fooList no es null, Find podría devolver null, pero sabemos que no, por lo que se aplica el operador que acepta valores NULL.

Puede aplicar el operador que acepta valores NULL a fooList para deshabilitar la advertencia:

FooBar fooBar = fooList!.Find(f => f.Name == "Bar")!;

Nota:

Debe usar el operador que admite valores NULL con criterio. Usarlo simplemente para descartar una advertencia significa que le está indicando al compilador que no le ayude a detectar posibles contratiempos de valores NULL. Úselo con moderación y solo cuando esté seguro.

Para obtener más información, consulte ! (permite valores NULL) (referencia de C#).

Operador de fusión de NULL (??)

Al trabajar con tipos que aceptan valores NULL, es posible que tenga que evaluar si actualmente son null y realizar determinadas acciones. Por ejemplo, cuando a un tipo que acepta valores NULL se le ha asignado null o no está inicializado, es posible que tenga que asignarle un valor distinto de NULL. Aquí es donde resulta útil el operador de fusión de NULL (??).

Considere el ejemplo siguiente:

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    salesTax ??= DefaultStateSalesTax.Value;

    // Safely use salesTax object.
}

En el código de C# anterior:

  • El parámetro salesTax se define como un elemento IStateSalesTax que acepta valores NULL.
  • Dentro del cuerpo del método, se asigna salesTax condicionalmente mediante el operador de fusión de NULL.
    • Esto garantiza que si salesTax se ha pasado como null, entonces tendrá un valor.

Sugerencia

Esto es funcionalmente equivalente al siguiente código de C#:

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    if (salesTax is null)
    {
        salesTax = DefaultStateSalesTax.Value;
    }

    // Safely use salesTax object.
}

Este es un ejemplo de otra expresión común de C# donde el operador de fusión de NULL puede ser útil:

public sealed class Wrapper<T> where T : new()
{
    private T _source;

    // If given a source, wrap it. Otherwise, wrap a new source:
    public Wrapper(T source = null) => _source = source ?? new T();
}

El código de C# anterior:

  • Define una clase contenedora genérica, donde el parámetro de tipo genérico está restringido a new().
  • El constructor acepta un parámetro T source con el valor predeterminado null.
  • El elemento _source encapsulado se inicializa condicionalmente en new T().

Para obtener más información, consulte Operadores ?? y ??= (referencia de C#).

Operador condicional NULL (?.)

Al trabajar con tipos que aceptan valores NULL, es posible que tenga que realizar acciones condicionalmente en función del estado de un objeto null. Por ejemplo, en la unidad anterior, el registro FooBar se usó para mostrar NullReferenceException mediante la desreferenciación de null. Esto se produjo cuando se llamó a ToString. Considere este mismo ejemplo, pero ahora aplicando el operador condicional NULL:

using System;

// Declare variable and assign it as null.
FooBar fooBar = null;

// Conditionally dereference variable.
var str = fooBar?.ToString();
Console.Write(str);

// The FooBar type definition.
record FooBar(int Id, string Name);

El código de C# anterior:

  • Desreferencia condicionalmente fooBar, asignando el resultado de ToString a la variable str.
    • La variable str es de tipo string? (cadena que acepta valores NULL).
  • Escribe el valor de str en la salida estándar, que no es nada.
  • La llamada a Console.Write(null) es válida, por lo que no hay ninguna advertencia.
  • Recibiría una advertencia si llamara a Console.Write(str.Length), porque podría desreferenciar NULL.

Sugerencia

Esto es funcionalmente equivalente al siguiente código de C#:

using System;

// Declare variable and assign it as null.
FooBar fooBar = null;

// Conditionally dereference variable.
string str = (fooBar is not null) ? fooBar.ToString() : default;
Console.Write(str);

// The FooBar type definition.
record FooBar(int Id, string Name);

Puede combinar el operador para expresar mejor su intención. Por ejemplo, podría encadenar los operadores ?. y ??:

FooBar fooBar = null;
var str = fooBar?.ToString() ?? "unknown";
Console.Write(str); // output: unknown

Para obtener más información, consulte Operadores ?. y ?[] (condicional NULL).

Resumen

En esta unidad, ha aprendido a expresar la intención de nulabilidad en el código. En la siguiente unidad, aplicará lo que ha aprendido a un proyecto existente.

Comprobación de conocimientos

1.

¿Cuál es el valor default del tipo de referencia string?

2.

¿Cuál es el comportamiento esperado de la desreferenciación de null?

3.

¿Qué ocurre cuando se ejecuta este código throw null; de C#?

4.

¿Qué instrucción es más precisa con respecto a los tipos de referencia que aceptan valores NULL?