Non-nullable Reference Types und Nullable-Kontexte in C# 8.0

Kreuzzug gegen Null-Referenz-Exception in C# 8.0
Keine Kommentare

Referenztypen können seit C# 1.0 den Nullwert annehmen. In C# 8.0 gibt es nun auch Referenztypen, die nicht mehr Null werden können. Außerdem kann der Compiler den Softwareentwickler warnen, wenn er mit seiner Programmierung Null-Referenz-Laufzeitfehler riskiert.

Es kann sich kaum ein C#-Programmierer auf dieser Welt davon freisprechen, dass er in seinem Programmcode nicht schon einmal einen Null-Reference-Laufzeitfehler („Object reference not set to an instance of an object“) produziert hat. Meist treten die Fehler auf, wenn man Objektreferenzen von Unterroutinen oder Steuerelementen empfängt und vor dem Aufruf einer Operation nicht prüft, ob die empfangene Objektreferenz null ist. Sehr beliebt ist der Fehler bei Zeichenketten, denn string ist in C# ein Nullable Reference Type. Wenn eine Stringvariable s den Wert null hat, reicht ein Aufruf wie s.Trim() zur Null Reference Exception. Null-Reference-Laufzeitfehler treten auch häufig in Visual Studio selbst auf, weil auch dort die Entwickler nicht immer den Null-Fall abgefangen haben (Abb. 1).

Abb. 1: Null-Referenz-Fehler in Visual Studio 2019 nach dem Versuch, einen Menüpunkt anzuwählen: Da haben die Entwickler bei Microsoft nicht aufgepasst

Abb. 1: Null-Referenz-Fehler in Visual Studio 2019 nach dem Versuch, einen Menüpunkt anzuwählen: Da haben die Entwickler bei Microsoft nicht aufgepasst

Nullable-Kontexte

Das C#-Entwicklungsteam möchte nun in der achten Version der Programmiersprache C# den Softwareentwicklern Hilfsinstrumente bieten, um diese Null-Referenz-Fehler zu vermeiden. Diese Instrumente sind optional, d. h. im Standard nicht aktiv. Es war in der Entwicklungsphase im Gespräch, diese Instrumente im Standard zu aktivieren. Das wurde aber verworfen, weil es zu viele Warnungen in bestehendem Programmcode gibt.

  • Das erste Instrument ist der Nullable Warning Context. In diesem Kontext warnt der Compiler vor dem Auftreten von Null-Reference-Laufzeitfehlern bei allen Zugriffen auf Objektreferenzen, bei denen möglich und nicht seitens des Entwicklers sichergestellt ist, dass sie nicht null enthalten. Der Compiler ist nur zufrieden, wenn man den Nullfall behandelt.
  • Das zweite Instrument ist der Nullable Annotation Context. In diesem Kontext sind Referenztypen (string, eigene Klassen) im Standard nicht mehr nullable, d. h. nicht mehr fähig, den Nullwert anzunehmen). Wenn Nullwerte explizit gewünscht sind, ist dies mit dem Fragezeichen bei der Typdeklaration anzuzeigen, z. B. string? und Klasse?. Es ist aber nicht erlaubt, hier Nullable<string> und Nullable<Klasse> statt des Fragezeichens zu verwenden wie bei den Nullable Value Types, die Microsoft in C# 2.0 im Jahr 2005 eingeführt hatte.
  • Es gibt noch einen dritten Kontext, der einfach Nullable Context heißt. Er ist die Zusammenfassung der Funktionen aus Nullable Warning Context und Nullable Annotation Context und ist das, was man als Softwareentwickler für seinen gesamten Programmcode anstreben sollte. In ihm warnt der Compiler vor Nullfehlern und die Referenztypen sind im Standard non-nullable.

Einen Kontext kann der Softwareentwickler auf Ebene eines C#-Projekts in der Projektdatei .csproj aktivieren mit dem Tag <Nullable> oder in einzelnen .cs-Programmcodedateien mit #nullable enable. Da man einen Kontext auch wieder deaktivieren kann mit #nullable disable, ist es möglich, einen Kontext auch auf einzelne Programmcodeblöcke zu beschränken.

C# 8.0 Spickzettel

Kostenlos: C# 8.0 – neue Sprachfeatures auf einen Blick

Der C#-8.0-Spickzettel fasst die neuen Features der Sprache zusammen mit Blick auf das aktuelle .NET Core 3.0 bzw. .NET Standard 2.1. Jetzt herunterladen und schneller & effektiver programmieren!

 

Tabelle 1 vergleicht alle drei Kontextarten hinsichtlich der Aktivierung, Deaktivierung und Funktionen. Listing 1 zeigt an Beispielen die Auswirkungen der drei Kontextarten.

Tabelle 1: Varianten des Null-Kontexts in C# 8.0

Tabelle 1: Varianten des Null-Kontexts in C# 8.0

// Normaler Kontext
string name1 = null;
Experte e1 = null;
int id1 = 1;
int? plz1 = null;
 
// Nullable Context einschalten
#nullable enable 
string name2 = null; // Non-Nullable Reference Type -> Warnung!
string? name3 = null; // Nullable Reference Type
Experte e2 = null; // Non-Nullable Reference Type -> Warnung!
Experte? e3 = null; // Nullable Reference Type
int id2 = 1; // keine Auswirkung auf Value Types!
int? plz2 = null; // keine Auswirkung auf Value Types!
Console.WriteLine(name2.Trim()); // Warnung: Dereference of a possibly null reference
Console.WriteLine(name3.Trim()); // Warnung: Dereference of a possibly null reference
Console.WriteLine(plz2.ToString()); // keine Warnung

// Nullable Context wieder ausschalten
#nullable disable
name2 = null; // keine Warnung
string? name4 = null; // Warnung bei ?

// nur Nullable Annotations Context einschalten
#nullable enable annotations
string name5 = null; // Nullable Reference Type, keine Warnung!
string? name6 = null; // Nullable Reference Type
Console.WriteLine(name5.Trim()); // keine Warnung
Console.WriteLine(name6.Trim()); // keine Warnung
#nullable disable annotations

// nur Nullable Warning Context einschalten
#nullable enable warnings
string name7 = null; // Nullable Reference Type, keine Warnung!
string? name8 = null; // Warnung bei ?, Nullable Reference Type nicht erlaubt
Console.WriteLine(name7.Trim()); // Warnung: Dereference of a possibly null reference
Console.WriteLine(name8.Trim()); // Warnung: Dereference of a possibly null reference
#nullable disable warnings

Die neuen Kontexte in der Praxis

Zum Praxistest kommt das Programm in Listing 2 zum Einsatz. Der C#-Compiler (auch in Version 8.0) übersetzt den Programmcode fehlerfrei und ohne Warnungen. Einwandfrei funktionieren kann der Programmcode freilich nicht: Bei der Ausführung sieht man direkt zweimal den Laufzeitfehler „NullReferenceException: Object reference not set to an instance of an object.“ (Abb. 2). Hier müsste man null-Prüfungen oder eine Toleranz gegenüber null einbauen.

using ITVisions;
using System;

namespace CS80
{
  class NullableRefTypes
  {
    public static void Run()
    {
      try
      {
        string Name = null;
        Print("Guten Tag, " + Name);
        Console.WriteLine($"Ihr Name ist {Name.Length} Zeichen lang!");
      }
      catch (System.Exception ex)
      {
        CUI.PrintError("ERROR: " + ex.Message);
      }

      CUI.MainHeadline(nameof(NullableRefTypes) + ": 2. Person");
      try
      {
        Person p1 = new Person() { ID = 123, Surname = "Schwichtenberg" };
        PrintPerson(p1);
        Person p2 = null;
        PrintPerson(p2);

        p1.Firstname = null;
        string name = p1.Firstname.ToUpper();
        Console.WriteLine(name);
      }
      catch (System.Exception ex)
      {
        CUI.PrintError("ERROR: " + ex.Message);
      }
    }

    static void Print(string s)
    {
      Console.WriteLine(s.Trim());
    }

    static void PrintPerson(Person p)
    {
      Console.WriteLine($"{p.ID}: {p.ToString()}");
    }

  class Person
  {
    public int ID { get; set; }
    public string Firstname { get; set; } 
    public string Surname { get; set; }

    public Person()   {   }

    public Person(int ID) : this()
    {
      this.ID = ID;
    }

    public override string ToString()
    {
      return this.Firstname.ToUpper() + " " + this.Surname.ToUpper();
    }
  }
}
Abb. 2: Die Ausgabe des Programms in Listing 1 zeigt zwei Null-Referenz-Fehler, die der Softwareentwickler verschuldet hat

Abb. 2: Die Ausgabe des Programms in Listing 1 zeigt zwei Null-Referenz-Fehler, die der Softwareentwickler verschuldet hat

Mit der Aktivierung des Nullable-Kontexts via #nullable enable zu Beginn von Listing 1 kommt es zu neun Warnungen (Abb. 3). Da es nur Warnungen sind, kompiliert das Programm weiterhin und es kommt immer noch zu den Laufzeitfehlern. Durch einen Eintrag in der Projektdatei kann der Entwickler aber ausgewählte Warnungen zu Fehlern hochstufen, z. B. <WarningsAsErrors>CS8600;CS8602;CS8603;CS8604;CS8625</WarningsAsErrors> und damit verhindern, dass das Programm kompiliert.

Abb. 3: Warnungen bei aktivierter Null-Reference-Prüfung

Abb. 3: Warnungen bei aktivierter Null-Reference-Prüfung

Listing 3 zeigt das verbesserte Programm, das nun alle strengeren Null-Reference-Prüfung besteht. Änderungen in dem Listing sind:

  • Variablen für Referenztypen, die null erlauben sollen, wurden explizit mit einem Fragezeichen versehen, also zu Nullable Reference Types gemacht, z. B. string? und Person?
  • Es wurden Null-tolerierende Operatoren eingebaut, z. B. ?? und ?.
  • Es wurden Null-Prüfungen eingebaut, z. B. if (p == null) { … }
  • Es wurden Initialisierungen ergänzt, z. B. Firstname = „“; Surname = „“;
  • Es wurde der neue sogenannte Null Forgiveness Operator eingebaut: this.Firstname!.ToUpper() + “ “ + this.Surname!.ToUpper();
using ITVisions;
using System;
#nullable enable 

namespace CS80
{
  class NullableRefTypesMitPrüfungen
  {
    public static void Run()
    {
      try
      {
        string? Name = null;
        Print("Guten Tag, " + Name);
        Console.WriteLine($"Ihr Name ist {Name?.Length ?? 0} Zeichen lang!");
      }
      catch (System.Exception ex)
      {
        CUI.PrintError("ERROR: " + ex.Message);
      }

      CUI.MainHeadline(nameof(NullableRefTypes) + ": 2. Person");
      try
      {
        Person p1 = new Person() { ID = 123, Surname = "Schwichtenberg" };
        PrintPerson(p1);
        Person? p2 = null;
        PrintPerson(p2);

        p1.Firstname = null;
        string name = p1.Firstname!.ToUpper();
        Console.WriteLine(name);
      }
      catch (System.Exception ex)
      {
        CUI.PrintError("ERROR: " + ex.Message);
      }
    }

    static void Print(string s)
    {
      Console.WriteLine(s.Trim());
    }

    static void PrintPerson(Person? p)
    {
      if (p == null) { Console.WriteLine("Person ist leer!"); return; }
      // oder: null coalescing assignment ("compound assigment")
      //p ??= new Person() { ID = -1 };
      Console.WriteLine($"{p.ID}: {p.ToString()}");
    }

    class Person
    {
      public int ID { get; set; }
      public string? Firstname { get; set; }
      public string? Surname { get; set; }

      public Person()
      {
        Firstname = ""; Surname = "";
      }

      public Person(int ID) : this()
      {
        this.ID = ID;
      }

      public override string ToString()
      {
        // Null Forgiveness-Operator als Beispiel
        return this.Firstname!.ToUpper() + " " + this.Surname!.ToUpper();
        // besser wäre eine Null-tolerierende Lösung:
        return this.Firstname?.ToUpper() + " " + this.Surname?.ToUpper();
      }
    }
  }
}

Operator ??=

In Listing 3 sieht man in der Routine PrintPerson() einen neu in C# 8.0 eingeführten Operator ??= zur Behandlung des Nullfalls. Microsoft nennt ihn „Null Coalescing Assignment“ (??=). Mit diesem Zuweisungsoperator kann der C#-Softwareentwickler eine Zuweisung ausführen, wenn eine Variable den Wert null hat. Statt

p = p ?? new Person() { ID = 123, Name = "Dr. Holger Schwichtenberg" };

oder

if (p == null) p = new Person() { ID = 123, Name = "Dr. Holger Schwichtenberg" };

kann der Softwareentwickler nun auch prägnanter schreiben:

p ??= new Person() { ID = 123, Name = "Dr. Holger Schwichtenberg" };

Fazit

Die in C# 8.0 eingeführte Null-Reference-Prüfung ist ein gutes Konzept, um Null-Reference-Fehler zu verhindern. Allerdings muss man bedenken, dass die Aktivierung der neuen Kontexte für bestehenden Programmcode ein nicht zu unterschätzender Aufwand ist, denn die meisten Entwickler wird der Compiler mit sehr vielen Warnungen konfrontieren. Man sollte die neuen Kontexte daher erst einmal an einzelnen C#-Projekten oder Programmteilen erproben.

Windows Developer

Windows DeveloperDieser Artikel ist im Windows Developer erschienen. Windows Developer informiert umfassend und herstellerneutral über neue Trends und Möglichkeiten der Software- und Systementwicklung rund um Microsoft-Technologien.

Natürlich können Sie den Windows Developer über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. Außerdem ist der Windows Developer weiterhin als Print-Magazin im Abonnement erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -