Compartir a través de


Tutorial: Expresar la intención de diseño con más claridad con tipos de referencia anulables y no anulables

Tipos de referencia anulables complementan los tipos de referencia de la misma manera que los tipos de valor anulables complementan los tipos de valor. Se declara una variable como un tipo de referencia anulable añadiendo un ? al tipo. Por ejemplo, string? representa un stringque puede ser nulo. Puede usar estos nuevos tipos para expresar con más claridad la intención de diseño: algunas variables siempre deben tener un valor, otros pueden faltar un valor.

En este tutorial, aprenderá a:

  • Incorpore tipos de referencia anulables y no anulables en sus diseños
  • Habilite comprobaciones de tipos de referencia anulables en todo el código.
  • Escriba código en el que el compilador aplique esas decisiones de diseño.
  • Usar la característica de referencia que acepta valores NULL en sus propios diseños

Prerrequisitos

Deberá configurar la máquina para ejecutar .NET, incluido el compilador de C#. El compilador de C# está disponible con de Visual Studio 2022 o SDK de .NET.

En este tutorial se da por supuesto que está familiarizado con C# y .NET, incluido Visual Studio o la CLI de .NET.

Incorpore tipos de referencia anulables en sus diseños

En este tutorial, creará una biblioteca que modela la ejecución de una encuesta. El código utiliza tanto tipos de referencia nulos como no nulos para representar los conceptos del mundo real. Las preguntas de la encuesta nunca pueden ser nulas. Un encuestado podría preferir no responder a una pregunta. Las respuestas podrían ser null en este caso.

El código que escribirá para este ejemplo expresa esa intención y el compilador aplica esa intención.

Creación de la aplicación y habilitación de 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. Esto se debe a que una vez activada la característica, las declaraciones de variables de referencia existentes se convierten en tipos de referencia que no aceptan valores NULL. Aunque esa decisión ayudará a encontrar problemas en los que es posible que el código existente no tenga comprobaciones null adecuadas, puede que no refleje con precisión la intención de diseño original:

<Nullable>enable</Nullable>

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

Diseñar los tipos de 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
  • Clase que modela las respuestas de una persona que realizó la encuesta.

Estos tipos usarán tanto tipos de referencia nulos como no nulos para expresar qué miembros son necesarios y cuáles son 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 nulas: no tiene sentido formular una pregunta vacía.
  • Los encuestados nunca pueden ser nulos. Querrá realizar un seguimiento de las personas a las que se ha contactado, incluso a los encuestados que rechazaron participar.
  • Cualquier respuesta a una pregunta puede ser null. Los encuestados pueden rechazar responder a algunas o todas las preguntas.

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 debe ser no anulable.
  • La colección de encuestados debe aceptar valores NULL.

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

La aplicación que vas a construir realiza los siguientes pasos:

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

Construir la encuesta con tipos de referencia anulables y no anulables

El primer código que escribirá 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: Sí/No respuestas, números 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, 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 se ha inicializado QuestionText, el compilador emite una advertencia de que no se ha inicializado una propiedad que no acepta valores NULL. El diseño requiere que el texto de la pregunta no sea NULL, por lo que también debe agregar un constructor para inicializarlo y el valor QuestionType. La definición de clase finalizada tiene el siguiente aspecto:

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 constructor también es un tipo de referencia que no acepta valores NULL, por lo que el compilador no emite ninguna advertencia.

A continuación, cree una clase public denominada SurveyRun. Esta clase contiene una lista de SurveyQuestion objetos y métodos para agregar preguntas a la encuesta, 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);
    }
}

Como antes, debe inicializar el objeto de lista en un valor distinto de NULL o el compilador emite una advertencia. No hay comprobaciones nulas en la segunda sobrecarga de AddQuestion porque no son necesarias: ha declarado que esa variable no admite valores NULL. Su valor no puede ser null.

Cambie a Program.cs en el editor y reemplace el contenido de Main por 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);

Crear encuestados y obtener respuestas a la encuesta

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

  1. Cree un método que genere objetos encuestados. Estas representan a las personas que han pedido que rellenen la encuesta.
  2. Cree lógica para simular hacer las preguntas a un encuestado y recopilar respuestas o observar que un encuestado no respondió.
  3. Repita el proceso hasta que suficientes encuestados hayan respondido a la encuesta.

Vas a necesitar una clase para representar una respuesta de encuesta, por lo que deberías añadirla ahora. Habilite la compatibilidad con la aceptación de valores NULL. Agregue una propiedad Id y un constructor que lo inicialice, 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 de un participante a las preguntas de la encuesta. Esta responsabilidad tiene algunos pasos:

  1. Solicitar participación en la encuesta. Si la persona no da su consentimiento, devuelva una respuesta ausente (o nula).
  2. Haga cada pregunta y registre la respuesta. También puede faltar cada respuesta (o null).

Agregue el código siguiente 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 cualquier persona que lea el código más adelante. Si alguna vez desreferencia surveyResponses sin comprobar el valor de null primero, recibirá una advertencia del compilador. No recibe una advertencia en el método AnswerSurvey porque el compilador puede determinar que la variable surveyResponses se estableció en un valor distinto de NULL más arriba.

El uso de null para las respuestas que faltan resalta un punto clave para trabajar con tipos de referencia que aceptan valores NULL: el objetivo no es quitar todos los valores de null del programa. En su lugar, el objetivo es asegurarse de 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 null valor es una manera clara de expresar esos valores que faltan. Intentar quitar todos los valores de null solo conduce a definir alguna otra manera de expresar esos valores que faltan sin null.

A continuación, debe escribir el método PerformSurvey en la clase SurveyRun. Agregue el código siguiente en 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);
    }
}

Aquí de nuevo, su elección de un List<SurveyResponse>? anulable indica que la respuesta puede ser NULL. Esto indica que la encuesta aún no se ha dado a ningún encuestado. 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 para 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 hace hincapié en el valor de distinguir entre los tipos de referencia que aceptan valores nulos y los que no los aceptan. Comience por agregar los siguientes dos miembros definidos por 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 acepta valores NULL, las comprobaciones de nulidad son necesarias antes de desreferenciarla. El método Answer devuelve una cadena no nula, por lo que tenemos que cubrir el caso de una respuesta que falta mediante el operador de coalescencia nula.

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 null, pero el valor devuelto no puede ser NULL. Si cambia esa expresión quitando el ?? y la secuencia vacía que sigue, el compilador advierte que el método podría devolver null y que su firma de retorno corresponde a un tipo no anulable.

Por último, agregue el siguiente bucle en 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 de en la carpeta csharp/NullableIntroduction.

Experimente cambiando las declaraciones de tipo entre los tipos de referencia que aceptan valores NULL y los que no aceptan valores NULL. Vea cómo genera diferentes advertencias y asegúrese de no desreferenciar accidentalmente un null.

Pasos siguientes

Aprenda a usar el tipo de referencia que acepta valores NULL al usar Entity Framework: