チュートリアル: null 許容参照型と null 非許容参照型を使って設計意図を表現する

Tip

null許容参照型は初めてですか? 最初にnull 許容参照型をお読みください。 このチュートリアルでは、null 非許容参照型と null 許容参照型の違いと、コンパイラが null 状態を追跡する方法を理解していることを前提としています。

別の言語から来ていますか? Kotlin の null 許容型、TypeScript の strictNullChecks、または Swift のオプションを使用した場合、概念モデルは直接マップされます。 ここでの演習では、構文を学習せず、 設計意図を表現します。

このチュートリアルでは、アンケートを実行するモデルを作成する小さなライブラリを作成します。 データには、null 許容参照型で区別できる 2 つの異なるパターンがあります。

  • アンケートの質問は常に存在する必要があります。 質問の一覧と各質問のテキストを nullすることはできません。
  • 質問に対する回答が見つからない可能性があります。 回答者は、一部またはすべての質問に答えるのを拒否することができ、モデルはそのことを明示的にする必要があります。

これらの規則は、null 非許容参照型と null 許容参照型を使用して宣言します。 その後、コードの動作がデザインと一致しない場合は常に、コンパイラによって警告が表示されます。

このチュートリアルでは、次の操作を行います。

  • アプリケーションを作成します。
  • アンケートの質問を作成します。
  • 質問のアンケートを作成します。
  • not-null 要件をテストします。
  • 応答の種類をビルドします。
  • 回答者を作成します。
  • アンケートの回答を 1 つ生成します。
  • アンケート回答のセットを作成します。
  • 調査結果を調べます。

3 つのクラスがアンケートをモデル化します。

  • SurveyQuestion: 1 つの質問。 テキストと質問の種類が必要です。
  • SurveyRun: 質問のコレクションと回答者のリスト。
  • SurveyResponse: 1 人の回答者の回答が不足している可能性があります。

各型は、必要な値には null 非許容参照型を使用し、欠損値には null 許容参照型を使用します。

前提条件

このチュートリアルでは、C# と Visual Studio または .NET CLI について理解していることを前提としています。

アプリケーションを作成し、null 許容参照型を有効にする

NullableIntroductionという名前の新しいコンソール アプリケーションを作成します。

dotnet new console -n NullableIntroduction
cd NullableIntroduction

アンケートの質問を作成する

SurveyQuestion.csという名前の新しいファイルをプロジェクトに追加し、その内容を次のコードに置き換えます。 テキストと質問の種類は null 非許容であるため、コンストラクターは両方を初期化する必要があります。

namespace NullableIntroduction;

public enum QuestionType
{
    YesNo,
    Number,
    Text
}

public class SurveyQuestion(QuestionType typeOfQuestion, string text)
{
    public string QuestionText { get; } = text;
    public QuestionType TypeOfQuestion { get; } = typeOfQuestion;
}

コンストラクター パラメーターは null 非許容参照型であるため、いずれかの引数が nullされる可能性がある場合、コンパイラは呼び出し元に警告します。

質問のアンケートを作成する

次に、 SurveyRun.cs という名前の新しいファイルをプロジェクトに追加し、質問の一覧を保持する SurveyRun クラスを定義します。

namespace NullableIntroduction;

public class SurveyRun
{
    private List<SurveyQuestion> surveyQuestions = [];

    public void AddQuestion(QuestionType type, string question) =>
        AddQuestion(new SurveyQuestion(type, question));

    public void AddQuestion(SurveyQuestion surveyQuestion) =>
        surveyQuestions.Add(surveyQuestion);
}

surveyQuestions フィールドは、null を許容しない List<SurveyQuestion> です。 コレクション式を使用して空のリストを初期化します。 どちらの AddQuestion オーバーロードも null 非許容パラメーターを受け入れるため、コンパイラは呼び出し元が nullを渡さないように強制します。

Program.csで、SurveyRunを作成し、次の 3 つの質問を追加します。

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?");

not-null 要件をテストする

コンパイラが null 非許容パラメーターを適用する方法を確認するには、次の行を追加して再構築してみてください。

surveyRun.AddQuestion(QuestionType.Text, default);

コンパイラが警告 CS8625 を発行するのは、 default が参照型の null と評価され、 AddQuestion が null 非許容 stringを想定しているためです。 続行する前に行を削除します。

応答の種類をビルドする

回答者はアンケートの実施を辞退でき、参加した場合でも個々の質問をスキップできます。 両方の形式の "不足" は有効な結果であり、型システムによって表示されるようにする必要があります。 両方のフォームを nullで表現します。

SurveyResponse.csという名前の新しいファイルをプロジェクトに追加し、SurveyResponse クラスを定義します。 常に必要なをキャプチャするには、Id (型自体で宣言されたパラメーター、本文全体で使用できます) を使用します。

namespace NullableIntroduction;

public class SurveyResponse(int id)
{
    public int Id { get; } = id;
}

回答者を作成する

ランダム ID を持つ回答者を作成する 静的ファクトリ メソッド (コンストラクターを直接呼び出す代わりに、型の新しいインスタンスを作成して返す static メソッド) を追加します。

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

アンケート回答を 1 つ生成する

次に、回答者にアンケートを依頼する方法を追加します。 回答を null 許容ディクショナリに格納して、回答者が拒否する可能性があることを型自体が伝えるようにします。

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!";
    }
}

surveyResponses フィールドはDictionary<int, string>?。 最初に nullを確認せずにフィールドを逆参照すると、コンパイラによって警告が発行されます。 AnswerSurvey内では、コンパイラはsurveyResponses式の直後にnew追跡するため、ループ本体に追加のチェックは必要ありません。

アンケート回答のセットを作成する

参加するのに十分な同意が得られるまで回答者のリストを作成するメソッドを SurveyRun に追加します。

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

respondents フィールドは List<SurveyResponse>? で、アンケートが実行されるまでは null です。

PerformSurvey から Main を呼び出します。

surveyRun.PerformSurvey(50);

調査結果を調べる

結果を報告するには、 SurveyResponseSurveyRunからいくつかのヘルパーを公開します。 SurveyResponseで、null 許容ディクショナリを処理する式形式のメンバー (=> で定義されたメンバーと、{ ... } ブロックの代わりに 1 つの式) を追加します。

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

AnsweredSurvey は、フィールドを nullに対してチェックします。 Answer では、?. null 条件演算子(左辺がnullの場合に例外をスローする代わりにnullと評価される)を使用して安全に逆参照を行い、?? null 合体演算子(左辺がnullの場合に右オペランドに置き換える)を使用して、null ではない代替値を提供します。 メソッドの戻り値の型は null 非許容 stringであるため、呼び出し元は null チェックを必要としません。

SurveyRun に、参加者一覧と質問一覧を公開するための式形式メンバーを追加します。

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

AllParticipants は、respondentsnull である可能性があるにもかかわらず、null 非許容のシーケンスを返します。 ??演算子は、フィールドがまだ設定されていない場合Enumerable.Empty<SurveyResponse>()に置き換えます。 ??句を削除すると、null 非許容の戻り値の型にもかかわらず、メソッドがnullを返す可能性があることをコンパイラが警告します。

最後に、 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");
    }
}

participantsurveyRun.Questions、またはsurveyRun.GetQuestion(i)に null チェックは必要ありません。 型は、これらの値を null 非許容として宣言するため、コンパイラはそれらをループ全体で null 以外 として扱います。

アプリケーションを実行します。

dotnet run

回答者はランダムに生成されるため、実行ごとに出力が異なりますが、すべての行が参加者の回答または拒否したメモを報告します。

まとめ

完成したサンプルは、dotnet/samples リポジトリの csharp/NullableIntroduction フォルダーにあります。 null 許容型と null 非許容型を切り替えて試してみてください。 デザインで欠損値が許容される ? を削除すると、欠損値が重要なすべての場所を指すコンパイラ警告が生成されます。