Tipos de referencia que aceptan valores NULL (referencia de C#)

Nota:

En este artículo se tratan los tipos de referencia que aceptan valores NULL. También puede declarar tipos de valor que aceptan valores NULL.

Los tipos de referencia que admiten valores NULL están disponibles en un código que ha participado en un contexto compatible con los valores NULL. Los tipos de referencia que aceptan valores NULL, las advertencias de análisis estático NULL y el operador null-forgiving son características de lenguaje opcionales. Todas están desactivadas de forma predeterminada. Un contexto que admite valores NULL se controla en el nivel de proyecto mediante la configuración de compilación o en el código que usa pragmas.

Importante

Todas las plantillas de proyecto a partir de .NET 6 (C# 10) habilitan el contexto que admite valores NULL para el proyecto. Los proyectos creados con plantillas anteriores no incluyen este elemento y estas características están desactivadas a menos que las habilite en el archivo del proyecto o use pragmas.

En un contexto compatible con valores NULL:

  • Una variable de un tipo de referencia T se debe inicializar con un valor distinto de NULL y nunca puede tener asignado un valor que sea null.
  • Una variable de un tipo de referencia T? se puede inicializar con null o asignarle null, pero debe comprobarse con null antes de desreferenciarla.
  • Una variable m de tipo T? se considera que no es NULL cuando se aplica el operador null-forgiving, como en m!.

Las distinciones entre un tipo de referencia que no acepta valores NULL T y un tipo de referencia que acepta valores NULL T? se aplican mediante la interpretación del compilador de las reglas anteriores. Una variable de tipo T y una variable de tipo T? se representan mediante el mismo tipo .NET. En el ejemplo siguiente se declara una cadena que no acepta valores NULL y una cadena que acepta valores NULL y luego se usa el operador null-forgiving para asignar un valor a una cadena que no acepta valores NULL:

string notNull = "Hello";
string? nullable = default;
notNull = nullable!; // null forgiveness

Las variables notNullnullable se ambas representan con el tipo String. Dado que los tipos que no aceptan valores NULL y que aceptan valores NULL se almacenan como el mismo tipo, hay varias ubicaciones en las que no se permite el uso de un tipo de referencia que acepte valores NULL. En general, un tipo de referencia que acepta valores NULL no se puede usar como clase base o interfaz implementada. No se puede usar un tipo de referencia que acepte valores NULL en ninguna expresión de creación de objetos o de expresión de prueba de tipos. Un tipo de referencia que acepta valores NULL no puede ser el tipo de una expresión de acceso a miembros. En los siguientes ejemplos se muestran estas construcciones:

public MyClass : System.Object? // not allowed
{
}

var nullEmpty = System.String?.Empty; // Not allowed
var maybeObject = new object?(); // Not allowed
try
{
    if (thing is string? nullableString) // not allowed
        Console.WriteLine(nullableString);
} catch (Exception? e) // Not Allowed
{
    Console.WriteLine("error");
}

Referencias que aceptan valores NULL y análisis estático

Los ejemplos de la sección anterior muestran la naturaleza de los tipos de referencia que aceptan valores NULL. Los tipos de referencia que aceptan valores NULL no son nuevos tipos de clase, sino anotaciones en tipos de referencia existentes. El compilador utiliza esas anotaciones para ayudarle a encontrar posibles errores de referencia nula en el código. No hay ninguna diferencia en tiempo de ejecución entre un tipo de referencia que no acepta valores NULL y un tipo de referencia que acepta valores NULL. El compilador no agrega ninguna comprobación de tiempo de ejecución para los tipos de referencia que no aceptan valores NULL. Las ventajas radican en el análisis en tiempo de compilación. El compilador genera advertencias que le ayudan a encontrar y corregir posibles errores nulos en el código. Declare su intención y el compilador le advierte cuando el código infringe dicha intención.

En un contexto habilitado para valores NULL, el compilador realiza un análisis estático de las variables de cualquier tipo de referencia, tanto si admiten valores NULL como si no aceptan valores NULL. El compilador realiza un seguimiento del estado null-state de cada variable de referencia como not-null o maybe-null. El estado predeterminado de una referencia que no acepta valores NULL es not-null. El estado predeterminado de una referencia que acepta valores NULL es maybe-null.

Los tipos de referencia que no aceptan valores NULL siempre deben ser seguros para desreferenciar porque su estado null-state es not-null. Para aplicar esa regla, el compilador emite advertencias si un tipo de referencia que no acepta valores NULL no se inicializa en un valor no NULL. Las variables locales deben asignarse allí donde se declaran. A cada campo se le debe asignar un valor not-null en un inicializador de campo o en cada constructor. El compilador emite advertencias cuando una referencia que no acepta valores NULL se asigna a una referencia cuyo estado es maybe-null. Por lo general, una referencia que no acepta valores NULL es not-null y no se emite ninguna advertencia cuando se desreferencian esas variables.

Nota:

Si asigna una expresión maybe-null a un tipo de referencia que no admita valores NULL, el compilador genera una advertencia. A continuación, el compilador genera advertencias para esa variable hasta que se asigna a una expresión not-null.

Los tipos de referencia que aceptan valores NULL se pueden inicializar o asignar a null. Por lo tanto, el análisis estático debe determinar que una variable es not-null antes de desreferenciarla. Si se determina que una referencia que acepta valores NULL es maybe-null, la asignación a una variable de referencia que no acepta valores NULL genera una advertencia del compilador. La consulta siguiente muestra ejemplos de estas advertencias:

public class ProductDescription
{
    private string shortDescription;
    private string? detailedDescription;

    public ProductDescription() // Warning! shortDescription not initialized.
    {
    }

    public ProductDescription(string productDescription) =>
        this.shortDescription = productDescription;

    public void SetDescriptions(string productDescription, string? details=null)
    {
        shortDescription = productDescription;
        detailedDescription = details;
    }

    public string GetDescription()
    {
        if (detailedDescription.Length == 0) // Warning! dereference possible null
        {
            return shortDescription;
        }
        else
        {
            return $"{shortDescription}\n{detailedDescription}";
        }
    }

    public string FullDescription()
    {
        if (detailedDescription == null)
        {
            return shortDescription;
        }
        else if (detailedDescription.Length > 0) // OK, detailedDescription can't be null.
        {
            return $"{shortDescription}\n{detailedDescription}";
        }
        return shortDescription;
    }
}

En el fragmento de código siguiente se muestra dónde el compilador emite advertencias al utilizar esta clase:

string shortDescription = default; // Warning! non-nullable set to null;
var product = new ProductDescription(shortDescription); // Warning! static analysis knows shortDescription maybe null.

string description = "widget";
var item = new ProductDescription(description);

item.SetDescriptions(description, "These widgets will do everything.");

En los ejemplos anteriores se muestra cómo el análisis estático del compilador determina el estado null-state de las variables de referencia. El compilador aplica las reglas de lenguaje a las asignaciones y comprobaciones de valores NULL para informar de su análisis. El compilador no puede hacer suposiciones sobre la semántica de métodos o propiedades. Si llama a métodos que realizan comprobaciones de valores NULL, el compilador no puede saber que estos métodos afectan al estado null-state de una variable. Hay atributos que puede agregar a las API para informar al compilador sobre la semántica de los argumentos y los valores devueltos. Estos atributos se han aplicado a muchas API comunes de las bibliotecas de .NET Core. Por ejemplo, IsNullOrEmpty se ha actualizado y el compilador interpreta correctamente ese método como una comprobación de valores NULL. Para obtener más información sobre los atributos que se aplican al análisis estático de estado null-state, consulte el artículo sobre Atributos que aceptan valores NULL.

Establecimiento del contexto que acepta valores NULL

Hay dos formas de controlar el contexto que acepta valores NULL. En el nivel de proyecto, puede agregar la configuración del proyecto <Nullable>enable</Nullable>. En un archivo C# de código fuente único, puede agregar el pragma #nullable enable para habilitar el contexto que acepta valores NULL. Consulte el artículo sobre cómo establecer una estrategia que acepte valores NULL. Antes de .NET 6, los nuevos proyectos usan el elemento predeterminado <Nullable>disable</Nullable>. A partir de .NET 6, los proyectos nuevos incluyen el elemento <Nullable>enable</Nullable> en el archivo del proyecto.

Especificación del lenguaje C#

Para más información, consulte las propuestas siguientes de la especificación del lenguaje C#:

Consulte también