Oktatóanyag: Tervezési szándék kifejezése null értékű és nem null értékű hivatkozástípusokkal

Jótanács

Még nem ismeri a null értékű referenciatípusokat? Először olvassa el a null értékű hivatkozástípusokat . Ez az oktatóanyag feltételezi, hogy tisztában van a nem null értékű és a null értékű hivatkozástípusok közötti különbségekkel, valamint azzal, hogy a fordító hogyan követi nyomon a null állapotot.

Más nyelvről jön? Ha a Kotlin null értékű típusait, TypeScript vagy strictNullChecksSwift választható elemeit használta, a koncepciómodell közvetlenül leképezhető. A gyakorlat itt a tervezési szándék kifejezéséről szól, nem a szintaxis elsajátításáról.

Ebben az oktatóanyagban egy kis könyvtárat hoz létre, amely egy felmérés lebonyolítását modellezi. Az adatok két különböző mintával rendelkeznek, amelyek null értékű hivatkozástípusokkal különböztethetők meg:

  • Egy felmérési kérdésnek mindig jelen kell lennie. A kérdések listája és az egyes kérdések szövege soha nem lehet null.
  • Előfordulhat , hogy hiányzik egy kérdésre adott válasz . A válaszadók elutasíthatnak néhány vagy az összes kérdés megválaszolását, és a modellnek explicitnek kell lennie.

Ezeket a szabályokat nem nullázható és nullázható hivatkozástípusokkal deklarálja. A fordító ezután figyelmeztet, ha a kód viselkedése nem egyezik a tervével.

Ebben az útmutatóban Ön:

  • Hozza létre az alkalmazást.
  • Készítse el a felmérésre vonatkozó kérdéseket.
  • Állítson össze egy kérdőívet.
  • Tesztelje a nem null érték követelményét.
  • Választípusok létrehozása.
  • Válaszadók létrehozása.
  • Hozzon létre egy felmérési választ.
  • Felmérési válaszok készletének létrehozása.
  • Vizsgálja meg a felmérés eredményeit.

A felmérést három osztály modellezheti:

  • SurveyQuestion: egy kérdés. A szöveg és a kérdés típusa kötelező.
  • SurveyRun: a kérdések gyűjteménye, valamint a válaszadók listája.
  • SurveyResponse: egy válaszadó válasza, amely esetleg hiányzik.

Minden típus nem null értékű referenciatípusokat használ a szükséges értékekhez, a hiányzó értékekhez pedig null értékű referenciatípusokat.

Prerequisites

Ez az oktatóanyag feltételezi, hogy ismeri a C#-t, és vagy Visual Studio vagy a .NET parancssori felületet.

Az alkalmazás létrehozása és null értékű hivatkozástípusok engedélyezése

Hozzon létre egy új konzolalkalmazást a következő néven NullableIntroduction:

dotnet new console -n NullableIntroduction
cd NullableIntroduction

Felmérési kérdések összeállítása

Adjon hozzá egy új, a projekthez elnevezett SurveyQuestion.cs fájlt, és cserélje le annak tartalmát a következő kódra. A szöveg és a kérdés típusa nem null értékű, ezért a konstruktornak mindkettőt inicializálnia kell:

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

A konstruktor paraméterei nem null értékű referenciatípusok, ezért a fordító figyelmezteti a hívót, ha bármelyik argumentum lehet null.

Kérdések felmérésének összeállítása

Ezután adjon hozzá egy új fájlt a projekthez, SurveyRun.cs és definiáljon egy osztályt SurveyRun a kérdések listájának tárolásához:

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);
}

A surveyQuestions mező egy olyan List<SurveyQuestion>, amely nem lehet null értékű. Gyűjteménykifejezéssel inicializál egy üres listát. Mindkét AddQuestion túlterhelés nem nullázható paramétereket fogad el, ezért a fordító kikényszeríti, hogy a hívók ne adhassanak át null értéket.

A(z) Program.cs elemben hozzon létre egy SurveyRun elemet, és adjon hozzá három kérdést:

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

A not-null követelmény tesztelése

Ha meg szeretné tudni, hogy a fordító hogyan kényszeríti ki a nem null értékű paramétereket, próbálkozzon a következő sor hozzáadásával és újraépítésével:

surveyRun.AddQuestion(QuestionType.Text, default);

A fordító a CS8625 figyelmeztetést adja ki, mert a(z) default referenciatípus esetén null értékre kiértékelődik, a(z) AddQuestion pedig nem nullázható string értéket vár. A folytatás előtt távolítsa el a vonalat.

Választípusok létrehozása

A válaszadók elutasíthatják a felmérést, és még akkor is, ha részt vesznek, kihagyhatják az egyes kérdéseket. A "hiányzó" mindkét formája érvényes eredmény, és a típusrendszernek láthatóvá kell tennie őket. Mindkét formát a null használatával fejezi ki.

Adjon hozzá egy új, a projekthez elnevezett SurveyResponse.cs fájlt, és definiáljon egy osztályt SurveyResponse . Használjon elsődleges konstruktort (a típuson deklarált paramétereket, amelyek a teljes törzsben elérhetők) a mindig szükséges Idkonstruktor rögzítéséhez:

namespace NullableIntroduction;

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

Válaszadók létrehozása

Adjon hozzá egy statikus gyári metódust (egy metódust static , amely létrehoz és visszaad egy új típusú példányt, amely a konstruktor közvetlen meghívásának alternatíva), amely véletlenszerű azonosítóval hozza létre a válaszadókat:

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

Egyetlen felmérési válasz létrehozása

Ezután adja hozzá azt a metódust, amely felteszi a kérdőívet a válaszadónak. A válaszokat egy null értékű szótárban tárolja, így maga a típus közli, hogy a válaszadó elutasíthatja:

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

A surveyResponses mező Dictionary<int, string>?. Ha a mezőre anélkül hivatkozik, hogy előbb ellenőrizné, hogy null, a fordító figyelmeztetést ad. A AnswerSurvey belsejében a fordító nyomon követi, hogy a surveyResponsesnem null közvetlenül a new kifejezés után, ezért a ciklusmag nem igényel további ellenőrzést.

Felmérési válaszok készletének létrehozása

Adjon hozzá egy metódust SurveyRun , amely összeállítja a válaszadók listáját, amíg elegendő hozzájárulást nem kap a részvételhez:

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);
    }
}

A respondents mező List<SurveyResponse>?null, amíg a felmérés le nem fut.

Hívás PerformSurvey innen Main:

surveyRun.PerformSurvey(50);

A felmérés eredményeinek vizsgálata

Az eredmények jelentéséhez tegyen elérhetővé néhány segédfüggvényt a SurveyResponse és a SurveyRun elemből. A(z) SurveyResponse elemhez adjon hozzá kifejezéstörzsű tagokat (olyan tagokat, amelyek egy { ... } blokk helyett => és egyetlen kifejezés használatával vannak definiálva), amelyek a null értékű szótárt kezelik:

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

AnsweredSurvey ellenőrzi a mezőt a(z) null alapján. Answer a ?. nullfeltételes operátort használja (amely null értékre értékelődik ki, ha a bal oldal null, kivételdobás helyett) a biztonságos dereferáláshoz, valamint a ?? null-egyesítő operátort (amely a jobb oldali operandust használja, ha a bal oldali null) egy nem null értékű tartalékérték biztosítására. A metódus visszatérési típusa nem null értékű string, ezért a hívóknak nincs szükségük null értékű ellenőrzésekre.

A(z) SurveyRun elemhez adjon hozzá olyan kifejezéstörzsű tagokat, amelyek elérhetővé teszik a résztvevők és a kérdések listáját:

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

AllParticipants nem nullázható sorozatot ad vissza, annak ellenére, hogy a respondents lehet null. Az ?? operátor a(z) Enumerable.Empty<SurveyResponse>() értéket helyettesíti be, ha a mező még nincs kitöltve. Ha eltávolítja a ?? záradékot, a fordító arra figyelmeztet, hogy a metódus a nem nullázható visszatérési típusa ellenére is null értéket adhat vissza.

Végül írja meg a jelentést a Main alján:

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

Figyelje meg, hogy nincs szükség null értékű ellenőrzésre az participant, surveyRun.Questionsvagy surveyRun.GetQuestion(i). A típusok nem null értékűként deklarálják ezeket az értékeket, így a fordító a ciklus során nem null értékűként kezeli őket.

Futtassa az alkalmazást:

dotnet run

A kimenet minden futtatáskor eltérő, mert a válaszadókat véletlenszerűen generálják, de minden sor vagy egy résztvevő válaszait tartalmazza, vagy azt jelzi, hogy nem kívánt válaszolni.

Conclusion

A kész minta a dotnet/samples adattár csharp/NullableIntroduction mappájában található. Kísérletezzen a null értékű és a nem null értékű típusok módosításával. Ha eltávolít egy ? elemet egy olyan kialakításban, amely lehetővé teszi a hiányzó értékeket, a fordítóprogram figyelmeztetéseket ad, amelyek minden olyan helyre rámutatnak, ahol a hiányzó érték számít.