Übung: Anwenden von Nullsicherheitsstrategien

Abgeschlossen

In der vorherigen Lerneinheit haben Sie erfahren, wie Sie Ihre Absicht für die NULL-Zulässigkeit im Code ausdrücken. In dieser Lerneinheit wenden Sie das Gelernte auf ein vorhandenes C#-Projekt an.

Hinweis

In diesem Modul wird die .NET-CLI (Befehlszeilenschnittstelle) und Visual Studio Code für die lokale Entwicklung verwendet. Nach Abschluss dieses Moduls können Sie die Konzepte mit Visual Studio (Windows), Visual Studio für Mac (macOS) oder bei der Weiterentwicklung mit Visual Studio Code (Windows, Linux und macOS) anwenden.

In diesem Modul wird das .NET 6.0 SDK verwendet. Stellen Sie sicher, dass .NET 6.0 installiert ist, indem Sie in Ihrem bevorzugten Terminal den folgenden Befehl ausführen:

dotnet --list-sdks

Daraufhin wird eine Ausgabe angezeigt, die in etwa wie folgt aussieht:

3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]

Stellen Sie sicher, dass eine Version aufgeführt wird, die mit 6 beginnt. Wenn keine solche Version aufgeführt wird oder der Befehl nicht gefunden wurde, installieren Sie das neueste .NET 6.0 SDK.

Abrufen und Untersuchen des Beispielcodes

  1. Klonen Sie in einem Befehlsterminal das GitHub-Beispielrepository, und wechseln Sie zum geklonten Verzeichnis.

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Öffnen Sie das Projektverzeichnis in Visual Studio Code.

    code .
    
  3. Führen Sie das Beispielprojekt mit dem Befehl dotnet run aus.

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    

    Dies führt dazu, dass eine NullReferenceException ausgelöst wird.

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
       at Program.<Main>$(String[] args) in .\src\ContosoPizza.Service\Program.cs:line 13
    

    Die Stapelüberwachung gibt an, dass die Ausnahme in Zeile 13 in .\src\ContosoPizza.Service\Program.cs aufgetreten ist. In Zeile 13 wird die Add-Methode für die pizza.Cheeses-Eigenschaft aufgerufen. Da pizza.Cheesesnull ist, wird eine NullReferenceException ausgelöst.

    using ContosoPizza.Models;
    
    // Create a pizza
    Pizza pizza = new("Meat Lover's Special")
    {
        Size = PizzaSize.Medium,
        Crust = PizzaCrust.DeepDish,
        Sauce = PizzaSauce.Marinara,
        Price = 17.99m,
    };
    
    // Add cheeses
    pizza.Cheeses.Add(PizzaCheese.Mozzarella);
    pizza.Cheeses.Add(PizzaCheese.Parmesan);
    
    // Add toppings
    pizza.Toppings.Add(PizzaTopping.Sausage);
    pizza.Toppings.Add(PizzaTopping.Pepperoni);
    pizza.Toppings.Add(PizzaTopping.Bacon);
    pizza.Toppings.Add(PizzaTopping.Ham);
    pizza.Toppings.Add(PizzaTopping.Meatballs);
    
    Console.WriteLine(pizza);
    
    /*
        Expected output:
    
        The "Meat Lover's Special" is a deep dish pizza with marinara sauce.
        It's covered with a blend of mozzarella and parmesan cheese.
        It's layered with sausage, pepperoni, bacon, ham and meatballs.
        This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49!
    */
    

Aktivieren des Nullable-Kontexts

Sie werden jetzt einen Nullwerte zulassenden Kontext aktivieren und dessen Auswirkungen auf den Build untersuchen.

  1. Fügen Sie in src/ContosoPizza.Service/ContosoPizza.Service.csproj die hervorgehobene Zeile hinzu, und speichern Sie Ihre Änderungen:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\ContosoPizza.Models\ContosoPizza.Models.csproj" />
      </ItemGroup>
    
    </Project>
    

    Die vorherige Änderung aktiviert den Nullable-Kontext für das gesamte ContosoPizza.Service-Projekt.

  2. Fügen Sie in src/ContosoPizza.Models/ContosoPizza.Models.csproj die hervorgehobene Zeile hinzu, und speichern Sie Ihre Änderungen:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
    </Project>
    

    Die vorherige Änderung aktiviert den Nullable-Kontext für das gesamte ContosoPizza.Models-Projekt.

  3. Erstellen Sie die Beispielprojektmappe mit dem dotnet build-Befehl.

    dotnet build
    

    Der Buildvorgang wird erfolgreich mit zwei Warnungen abgeschlossen.

    dotnet build
    Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET
    Copyright (C) Microsoft Corporation. All rights reserved.
    
      Determining projects to restore...
      Restored .\src\ContosoPizza.Service\ContosoPizza.Service.csproj (in 477 ms).
      Restored .\src\ContosoPizza.Models\ContosoPizza.Models.csproj (in 475 ms).
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
      ContosoPizza.Models -> .\src\ContosoPizza.Models\bin\Debug\net6.0\ContosoPizza.Models.dll
      ContosoPizza.Service -> .\src\ContosoPizza.Service\bin\Debug\net6.0\ContosoPizza.Service.dll
    
    Build succeeded.
    
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
        2 Warning(s)
        0 Error(s)
    
    Time Elapsed 00:00:07.48
    
  4. Erstellen Sie die Beispielprojektmappe erneut mit dem dotnet build-Befehl.

    dotnet build
    

    Dieses Mal wird der Build ohne Fehler oder Warnungen abgeschlossen. Der vorherige Build wurde erfolgreich mit Warnungen abgeschlossen. Da sich der Quellcode nicht geändert hat, führt der Buildprozess den Compiler nicht erneut aus. Da der Build den Compiler nicht ausführt, gibt es keine Warnungen.

    Tipp

    Sie können eine Neuerstellung aller Assemblys in einem Projekt erzwingen, indem Sie den dotnet clean-Befehl vor dotnet build verwenden.

  5. Fügen Sie in den CSPROJ-Dateien die hervorgehobenen Zeilen hinzu, und speichern Sie Ihre Änderungen.

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\ContosoPizza.Models\ContosoPizza.Models.csproj" />
      </ItemGroup>
    
    </Project>
    
    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>
    
    </Project>
    

    Die vorherigen Änderungen weisen den Compiler an, den Build fehlschlagen zu lassen, wenn eine Warnung auftritt.

    Tipp

    Die Verwendung von <TreatWarningsAsErrors> ist optional. Wir empfehlen sie jedoch, da dadurch sichergestellt wird, dass Sie keine Warnungen übersehen.

  6. Erstellen Sie die Beispielprojektmappe mit dem dotnet build-Befehl.

    dotnet build
    

    Der Build schlägt mit zwei Fehlern fehl.

    dotnet build
    Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET
    Copyright (C) Microsoft Corporation. All rights reserved.
    
      Determining projects to restore...
      All projects are up-to-date for restore.
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    
    Build FAILED.
    
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
        0 Warning(s)
        2 Error(s)
    
    Time Elapsed 00:00:02.95
    

    Wenn Warnungen als Fehler behandelt werden, wird die App nicht mehr erstellt. Dies ist in dieser Situation tatsächlich erwünscht, da die Anzahl der Fehler klein ist und wir sie schnell beheben können. Die beiden Fehler (CS8618) informieren Sie darüber, dass Eigenschaften als Non-Nullable-Eigenschaften deklariert sind, die noch nicht initialisiert wurden.

Beheben der Fehler

Es gibt viele Taktiken, um die Warnungen/Fehler im Zusammenhang mit der NULL-Zulässigkeit zu beheben. Beispiele hierfür sind:

  • Vorschreiben einer Non-Nullable-Sammlung von Käsesorten und Belägen als Konstruktorparametern.
  • Abfangen der Eigenschaft get/set und Hinzufügen einer null-Überprüfung.
  • Ausdrücken der Absicht, dass die Eigenschaften Nullwerte zulassen sollen
  • Initialisieren der Sammlung mit einem (leeren) Standardwert inline mithilfe von Eigenschafteninitialisierern
  • Zuweisen eines (leeren) Standardwerts zur Eigenschaft im Konstruktor
  1. Zum Beheben des Fehlers bei der Pizza.Cheeses-Eigenschaft ändern Sie die Eigenschaftsdefinition in Pizza.cs, um eine null-Prüfung hinzuzufügen. Ohne Käse ist es ja eigentlich keine Pizza, oder?

    namespace ContosoPizza.Models;
    
    public sealed record class Pizza([Required] string Name)
    {
        private ICollection<PizzaCheese>? _cheeses;
    
        public int Id { get; set; }
    
        [Range(0, 9999.99)]
        public decimal Price { get; set; }
    
        public PizzaSize Size { get; set; }
        public PizzaCrust Crust { get; set; }
        public PizzaSauce Sauce { get; set; }
    
        public ICollection<PizzaCheese> Cheeses
        {
            get => (_cheeses ??= new List<PizzaCheese>());
            set => _cheeses = value ?? throw new ArgumentNullException(nameof(value));
        }
    
        public ICollection<PizzaTopping>? Toppings { get; set; }
    
        public override string ToString() => this.ToDescriptiveString();
    }
    

    Für den Code oben gilt:

    • Ein neues Unterstützungsfeld mit dem Namen _cheeses wird hinzugefügt, um die Eigenschaftenaccessoren get und set abzufangen. Es wird als Nullwerte zulassend (?) deklariert und bleibt uninitialisiert.
    • Der get-Accessor wird einem Ausdruck zugeordnet, der den Null-Sammeloperator (??) verwendet. Dieser Ausdruck gibt das _cheeses-Feld zurück, vorausgesetzt, es ist nicht null. Wenn es null ist, weist er new List<PizzaCheese>()_cheeses zu, bevor er _cheeses zurückgibt.
    • Der set-Accessor wird ebenfalls einem Ausdruck zugeordnet und verwendet den Null-Sammeloperator. Wenn ein Consumer einen null-Wert zuweist, wird die ArgumentNullException ausgelöst.
  2. Da nicht alle Pizzen Beläge aufweisen, kann null ein gültiger Wert für die Pizza.Toppings-Eigenschaft sein. In diesem Fall ist es sinnvoll, sie als Nullwerte zulassend zu formulieren.

    1. Ändern Sie die Eigenschaftendefinition in Pizza.cs so, dass Nullwerte für Toppings zulässig sind.

      namespace ContosoPizza.Models;
      
      public sealed record class Pizza([Required] string Name)
      {
          private ICollection<PizzaCheese>? _cheeses;
      
          public int Id { get; set; }
      
          [Range(0, 9999.99)]
          public decimal Price { get; set; }
      
          public PizzaSize Size { get; set; }
          public PizzaCrust Crust { get; set; }
          public PizzaSauce Sauce { get; set; }
      
          public ICollection<PizzaCheese> Cheeses
          {
              get => (_cheeses ??= new List<PizzaCheese>());
              set => _cheeses = value ?? throw new ArgumentNullException(nameof(value));
          }
      
          public ICollection<PizzaTopping>? Toppings { get; set; }
      
          public override string ToString() => this.ToDescriptiveString();
      }
      

      Die Toppings-Eigenschaft wird jetzt als Nullwerte zulassend angegeben.

    2. Fügen Sie ContosoPizza.Service\Program.cs die hervorgehobene Zeile hinzu:

      using ContosoPizza.Models;
      
      // Create a pizza
      Pizza pizza = new("Meat Lover's Special")
      {
          Size = PizzaSize.Medium,
          Crust = PizzaCrust.DeepDish,
          Sauce = PizzaSauce.Marinara,
          Price = 17.99m,
      };
      
      // Add cheeses
      pizza.Cheeses.Add(PizzaCheese.Mozzarella);
      pizza.Cheeses.Add(PizzaCheese.Parmesan);
      
      // Add toppings
      pizza.Toppings ??= new List<PizzaTopping>();
      pizza.Toppings.Add(PizzaTopping.Sausage);
      pizza.Toppings.Add(PizzaTopping.Pepperoni);
      pizza.Toppings.Add(PizzaTopping.Bacon);
      pizza.Toppings.Add(PizzaTopping.Ham);
      pizza.Toppings.Add(PizzaTopping.Meatballs);
      
      Console.WriteLine(pizza);
      
      /*
          Expected output:
      
          The "Meat Lover's Special" is a deep dish pizza with marinara sauce.
          It's covered with a blend of mozzarella and parmesan cheese.
          It's layered with sausage, pepperoni, bacon, ham and meatballs.
          This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49!
      */
      

    Im vorangehenden Code wird der Null-Sammeloperator verwendet, um new List<PizzaTopping>();Toppings zuzuweisen, wenn sie null ist.

Ausführen der abgeschlossenen Projektmappe

  1. Speichern Sie alle Änderungen, und erstellen Sie dann die Projektmappe.

    dotnet build
    

    Der Buildvorgang wird ohne Fehler oder Warnungen abgeschlossen.

  2. Führen Sie die App aus.

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    

    Die App wird bis zum Abschluss (ohne Fehler) ausgeführt und zeigt die folgende Ausgabe an:

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    The "Meat Lover's Special" is a deep dish pizza with marinara sauce.
    It's covered with a blend of mozzarella and parmesan cheese.
    It's layered with sausage, pepperoni, bacon, ham and meatballs.
    This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49!
    

Zusammenfassung

In dieser Einheit haben Sie einen Nullable-Kontext verwendet, um mögliche Vorkommen von NullReferenceException in Ihrem Code zu identifizieren und zu verhindern.