Tutorial: Expresar la intención del diseño con mayor claridad con tipos de referencia que aceptan valores NULL y que no aceptan valores NULL

Los tipos de referencia que admiten un valor NULL complementan a los tipos de referencia del mismo modo que los tipos de valor que admiten valores NULL complementan a los tipos de valor. Declarará una variable para que sea un tipo de referencia que acepta valores NULL anexando un elemento ? al tipo. Por ejemplo, string? representa un elemento string que acepta valores NULL. Puede utilizar estos nuevos tipos para expresar más claramente la intención del diseño: algunas variables siempre deben tener un valor, y a otras les puede faltar un valor.

En este tutorial aprenderá lo siguiente:

  • Incorporar los tipos de referencia que aceptan valores NULL y que no aceptan valores NULL en los diseños
  • Habilitar las comprobaciones de tipos de referencia que aceptan valores NULL en todo el código
  • Escribir código en la parte en la que el compilador aplica esas decisiones de diseño
  • Usar la característica de referencia que acepta valores NULL en sus propios diseños

Requisitos previos

Deberá configurar el equipo para que ejecute .NET, incluido el compilador de C#. El compilador de C# está disponible con Visual Studio 2022 o el SDK de .NET.

En este tutorial se da por supuesto que conoce bien C# y. NET, incluidos Visual Studio o la CLI de .NET.

Incorporación de los tipos de referencia que aceptan valores NULL en los diseños

En este tutorial, va a crear una biblioteca que modela la ejecución de una encuesta. El código usa tipos de referencia que aceptan valores NULL y tipos de referencia que no aceptan valores NULL para representar los conceptos del mundo real. Las preguntas de la encuesta nunca pueden aceptar valores NULL. Un encuestado podría preferir no responder a una pregunta. En este caso, las respuestas podrían ser null.

El código que escriba para este ejemplo expresa dicha intención y el compilador la exige.

Creación de la aplicación y habilitación de los tipos de referencia que aceptan valores NULL

Cree una aplicación de consola en Visual Studio o desde la línea de comandos mediante dotnet new console. Asigne a la aplicación el nombre NullableIntroduction. Una vez que se haya creado la aplicación, se deberá especificar que todo el proyecto se compila en un contexto de anotaciones que admite un valor NULL habilitado. Abra el archivo .csproj y agregue un elemento Nullable al elemento PropertyGroup. Establezca su valor en enable. En proyectos anteriores a C# 11, debe optar por recibir la característica de tipos de referencia que admiten un valor NULL. El motivo es que, una vez que la característica está activada, las declaraciones de variables de referencia existentes se convierten en tipos de referencia que no aceptan valores NULL. Aunque esa decisión lo ayudará a detectar problemas donde el código existente puede no tener comprobaciones de valores NULL adecuadas, es posible que no se refleje con precisión la intención del diseño original:

<Nullable>enable</Nullable>

Antes de .NET 6, los nuevos proyectos no incluyen el elemento Nullable. A partir de .NET 6, los proyectos nuevos incluyen el elemento <Nullable>enable</Nullable> en el archivo del proyecto.

Diseño de los tipos para la aplicación

Esta aplicación de encuesta requiere la creación de una serie de clases:

  • Una clase que modela la lista de preguntas
  • Una clase que modela una lista de personas contactadas para la encuesta
  • Una clase que modela las respuestas de una persona que realizó la encuesta

Estos tipos usarán ambas los tipos de referencia que aceptan valores NULL y los que no aceptan valores NULL para expresar qué miembros que son necesarios y cuáles opcionales. Los tipos de referencia que aceptan valores NULL comunican claramente esa intención de diseño:

  • Las preguntas que forman parte de la encuesta nunca pueden ser NULL: no tiene sentido formular una pregunta vacía.
  • Los encuestados nunca pueden aceptar valores NULL. Quiere realizar un seguimiento de las personas contactadas, incluso de los encuestados que han rechazado participar.
  • Cualquier respuesta a una pregunta puede tener valores NULL. Los encuestados pueden negarse a responder a algunas preguntas o a todas.

Si ha programado en C#, puede estar tan acostumbrado a los tipos de referencia que permiten valores null que puede haber perdido otras oportunidades para declarar instancias que no admiten un valor NULL:

  • La colección de preguntas no debe aceptar valores NULL.
  • La colección de encuestados debe aceptar valores NULL.

A medida que escriba el código, verá que un tipo de referencia que no admite un valor NULL, como el predeterminado para las referencias, evita errores comunes que podrían generar NullReferenceException. Una lección de este tutorial es que tomó decisiones sobre qué variables podrían ser o no null. El lenguaje no proporcionó una sintaxis para expresar esas decisiones. Ahora sí.

La aplicación que compilará realiza los pasos siguientes:

  1. Crea una encuesta y agrega preguntas a dicha encuesta.
  2. Crea un conjunto de encuestados pseudoaleatorios para la encuesta.
  3. Se pone en contacto con los encuestados hasta que el tamaño de la encuesta completada alcanza el número objetivo.
  4. Escribe estadísticas importantes en las respuestas de la encuesta.

Compilación de la encuesta con tipos de referencia que aceptan y no aceptan valores NULL

El primer código que escriba crea la encuesta. Deberá escribir clases para modelar una pregunta de encuesta y una ejecución de encuesta. La encuesta tiene tres tipos de preguntas, que se distinguen por el formato de la respuesta: respuestas afirmativas/negativas, respuestas numéricas y respuestas de texto. Cree una clase public SurveyQuestion:

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

El compilador interpreta cada declaración de variable de tipo de referencia como un tipo de referencia que no admite un valor NULL para el código en un contexto de anotaciones que admite un valor NULL habilitado. Puede ver la primera advertencia agregando propiedades para el texto de la pregunta y el tipo de pregunta, tal y como se muestra en el código siguiente:

namespace NullableIntroduction
{
    public enum QuestionType
    {
        YesNo,
        Number,
        Text
    }

    public class SurveyQuestion
    {
        public string QuestionText { get; }
        public QuestionType TypeOfQuestion { get; }
    }
}

Dado que no ha inicializado QuestionText, el compilador emite una advertencia de que aún no se ha inicializado una propiedad que no acepta valores NULL. Su diseño requiere que el texto de la pregunta no acepte valores NULL, por lo que debe agregar un constructor para inicializar ese elemento y el valor QuestionType también. La definición de clase finalizada es similar al código siguiente:

namespace NullableIntroduction;

public enum QuestionType
{
    YesNo,
    Number,
    Text
}

public class SurveyQuestion
{
    public string QuestionText { get; }
    public QuestionType TypeOfQuestion { get; }

    public SurveyQuestion(QuestionType typeOfQuestion, string text) =>
        (TypeOfQuestion, QuestionText) = (typeOfQuestion, text);
}

Al agregar el constructor, se quita la advertencia. El argumento del constructor también es un tipo de referencia que no acepta valores NULL, por lo que el compilador no emite advertencias.

A continuación, cree una clase public denominada "SurveyRun". Esta clase contiene una lista de objetos SurveyQuestion y métodos para agregar preguntas a la encuesta, tal como se muestra en el código siguiente:

using System.Collections.Generic;

namespace NullableIntroduction
{
    public class SurveyRun
    {
        private List<SurveyQuestion> surveyQuestions = new List<SurveyQuestion>();

        public void AddQuestion(QuestionType type, string question) =>
            AddQuestion(new SurveyQuestion(type, question));
        public void AddQuestion(SurveyQuestion surveyQuestion) => surveyQuestions.Add(surveyQuestion);
    }
}

Al igual que antes, debe inicializar el objeto de lista en un valor distinto a NULL o el compilador emitirá una advertencia. No hay ninguna comprobación de valores que aceptan valores NULL en la segunda sobrecarga de AddQuestion porque no son necesarias: ha declarado esa variable para que no acepte valores NULL. Su valor no puede ser null.

Cambie a Program.cs en el editor y reemplace el contenido de Main con las siguientes líneas de código:

var surveyRun = new SurveyRun();
surveyRun.AddQuestion(QuestionType.YesNo, "Has your code ever thrown a NullReferenceException?");
surveyRun.AddQuestion(new SurveyQuestion(QuestionType.Number, "How many times (to the nearest 100) has that happened?"));
surveyRun.AddQuestion(QuestionType.Text, "What is your favorite color?");

Dado que todo el proyecto está habilitado para un contexto de anotaciones que admite un valor NULL, al pasar null a cualquier método que espera un tipo de referencia que no admite un valor NULL, recibirá una advertencia. Pruébelo agregando la siguiente línea a Main:

surveyRun.AddQuestion(QuestionType.Text, default);

Creación de los encuestados y obtención de respuestas a la encuesta

A continuación, escriba el código que genera respuestas a la encuesta. Este proceso implica realizar varias pequeñas tareas:

  1. Crear un método que genere objetos de encuestados. Estos representan a las personas a las que se les ha pedido que completen la encuesta.
  2. Crear una lógica para simular la realización de preguntas a un encuestado y la recopilación de respuestas o de la ausencia de respuesta de un encuestado.
  3. Repetir todo esto hasta que hayan respondido a la encuesta los suficientes encuestados.

Necesitará una clase para representar una respuesta de encuesta, así que agréguela en este momento. Habilite la compatibilidad con la aceptación de valores NULL. Agregue una propiedad Id y un constructor que la inicialice, tal como se muestra en el código siguiente:

namespace NullableIntroduction
{
    public class SurveyResponse
    {
        public int Id { get; }

        public SurveyResponse(int id) => Id = id;
    }
}

A continuación, agregue un método static para crear nuevos participantes mediante la generación de un identificador aleatorio:

private static readonly Random randomGenerator = new Random();
public static SurveyResponse GetRandomId() => new SurveyResponse(randomGenerator.Next());

La responsabilidad principal de esta clase es generar las respuestas para que un participante de las preguntas de la encuesta. Esta responsabilidad implica una serie de pasos:

  1. Solicitar la participación en la encuesta. Si la persona no da su consentimiento, devolver una respuesta con valores ausentes (o NULL).
  2. Realizar cada pregunta y registrar la respuesta. Las respuestas también pueden tener valores ausentes (o NULL).

Agregue el siguiente código a la clase SurveyResponse:

private Dictionary<int, string>? surveyResponses;
public bool AnswerSurvey(IEnumerable<SurveyQuestion> questions)
{
    if (ConsentToSurvey())
    {
        surveyResponses = new Dictionary<int, string>();
        int index = 0;
        foreach (var question in questions)
        {
            var answer = GenerateAnswer(question);
            if (answer != null)
            {
                surveyResponses.Add(index, answer);
            }
            index++;
        }
    }
    return surveyResponses != null;
}

private bool ConsentToSurvey() => randomGenerator.Next(0, 2) == 1;

private string? GenerateAnswer(SurveyQuestion question)
{
    switch (question.TypeOfQuestion)
    {
        case QuestionType.YesNo:
            int n = randomGenerator.Next(-1, 2);
            return (n == -1) ? default : (n == 0) ? "No" : "Yes";
        case QuestionType.Number:
            n = randomGenerator.Next(-30, 101);
            return (n < 0) ? default : n.ToString();
        case QuestionType.Text:
        default:
            switch (randomGenerator.Next(0, 5))
            {
                case 0:
                    return default;
                case 1:
                    return "Red";
                case 2:
                    return "Green";
                case 3:
                    return "Blue";
            }
            return "Red. No, Green. Wait.. Blue... AAARGGGGGHHH!";
    }
}

El almacenamiento de las respuestas de la encuesta es una cadena Dictionary<int, string>?, que indica que puede aceptar valores NULL. Está usando la nueva característica de lenguaje para declarar la intención de diseño tanto para el compilador como para cualquiera que lea el código más adelante. Si alguna vez desreferencia surveyResponses sin comprobar el valor null en primer lugar, obtendrá una advertencia del compilador. No recibirá una advertencia en el método AnswerSurvey porque el compilador puede determinar que la variable surveyResponses se estableció en un valor distinto de NULL.

Al usar null en las respuestas que faltan se resalta un punto clave para trabajar con tipos de referencia que aceptan valores NULL: el objetivo no es quitar todos los valores null del programa. En cambio, de lo que se trata es de garantizar que el código que escribe expresa la intención del diseño. Los valores que faltan son un concepto necesario para expresar en el código. El valor null es una forma clara de expresar los valores que faltan. El intento de quitar todos los valores null solo lleva a definir alguna otra forma de expresar esos valores que faltan sin null.

A continuación, deberá escribir el método PerformSurvey en la clase SurveyRun. Agregue el código siguiente a la clase SurveyRun:

private List<SurveyResponse>? respondents;
public void PerformSurvey(int numberOfRespondents)
{
    int respondentsConsenting = 0;
    respondents = new List<SurveyResponse>();
    while (respondentsConsenting < numberOfRespondents)
    {
        var respondent = SurveyResponse.GetRandomId();
        if (respondent.AnswerSurvey(surveyQuestions))
            respondentsConsenting++;
        respondents.Add(respondent);
    }
}

De nuevo, la elección de que un elemento List<SurveyResponse>? acepte valores NULL indica que la respuesta puede tener valores NULL. Esto indica que la encuesta no se ha asignado a los encuestados todavía. Tenga en cuenta que los encuestados se agregan hasta que hayan dado su consentimiento.

El último paso para ejecutar la encuesta es agregar una llamada al realizar la encuesta al final del método Main:

surveyRun.PerformSurvey(50);

Examen de las respuestas de la encuesta

El último paso es mostrar los resultados de la encuesta. Agregará código a muchas de las clases que ha escrito. Este código muestra el valor de distinguir entre tipos de referencia que aceptan valores NULL y que no aceptan valores NULL. Empiece agregando los siguientes dos miembros con forma de expresión a la clase SurveyResponse:

public bool AnsweredSurvey => surveyResponses != null;
public string Answer(int index) => surveyResponses?.GetValueOrDefault(index) ?? "No answer";

Dado que surveyResponses es un tipo de referencia que admite un valor NULL, las comprobaciones de valores NULL son necesarias antes de desreferenciarlo. El método Answer devuelve una cadena que no admite un valor NULL, por lo que tenemos que cubrir el caso de que falte una respuesta mediante el operador de fusión de NULL.

A continuación, agregue estos tres miembros con forma de expresión a la clase SurveyRun:

public IEnumerable<SurveyResponse> AllParticipants => (respondents ?? Enumerable.Empty<SurveyResponse>());
public ICollection<SurveyQuestion> Questions => surveyQuestions;
public SurveyQuestion GetQuestion(int index) => surveyQuestions[index];

El miembro AllParticipants debe tener en cuenta que la variable respondents podría ser aceptar valores NULL, pero el valor devuelto no puede ser NULL. Si cambia esa expresión quitando ?? y la secuencia vacía de a continuación, el compilador advertirá de que el método podría devolver null y su firma de devolución devuelve un tipo que no acepta valores NULL.

Finalmente, agregue el siguiente bucle a la parte inferior del método Main:

foreach (var participant in surveyRun.AllParticipants)
{
    Console.WriteLine($"Participant: {participant.Id}:");
    if (participant.AnsweredSurvey)
    {
        for (int i = 0; i < surveyRun.Questions.Count; i++)
        {
            var answer = participant.Answer(i);
            Console.WriteLine($"\t{surveyRun.GetQuestion(i).QuestionText} : {answer}");
        }
    }
    else
    {
        Console.WriteLine("\tNo responses");
    }
}

No necesita ninguna comprobación null en este código porque ha diseñado las interfaces subyacentes para que devuelvan todos los tipos de referencia que no aceptan valores NULL.

Obtención del código

Puede obtener el código del tutorial terminado en nuestro repositorio de ejemplos en la carpeta csharp/NullableIntroduction.

Experimente cambiando las declaraciones de tipos entre tipos de referencia que aceptan valores NULL y que no aceptan valores NULL. Vea cómo así se generan advertencias diferentes para garantizar que no se desreferencia accidentalmente un valor null.

Pasos siguientes

Obtenga información sobre cómo usar tipos de referencia que aceptan valores NULL al utilizar Entity Framework: