Laden...

C# 8 Nullable reference types - Pro & Con

Erstellt von Palladin007 vor einem Jahr Letzter Beitrag vor einem Jahr 602 Views
Palladin007 Themenstarter:in
2.080 Beiträge seit 2012
vor einem Jahr
C# 8 Nullable reference types - Pro & Con

Guten Abend,

ich möchte hier eine Meinungsrunde anstoßen: Was haltet Ihr von dem in C# 8 eingeführten Feature: Nullable reference types?
Mir geht es dabei um die Langlebigkeit, Stabilität, Wartbarkeit, etc. der Software, auch (oder besonders) bei der Arbeit im Team.
Mir geht es nicht darum, wie einfach/schwierig oder intuitiv es zu nutzen ist, da das denke ich nur eine Frage der Gewohnheit ist.

Das Thema ändert die ja in gewisser Weise die generelle Art zu arbeiten, daher ist es denke ich wichtig, mehr als nur die eigene Meinung zu kennen.
Ich persönlich finde es super (Argumente lasse ich bewusst weg), aber vielleicht liege ich da auch falsch?

Außerdem möchte ich meine Kollegen davon überzeugen, es zu nutzen, oder - sollte ich falsch liegen - ich lasse es doch lieber bleiben.
Ich freue mich auch über Artikel zu dem Thema, die nicht nur erklären, wie man es nutzt, sondern auch warum oder warum nicht.

Also: Wie seht Ihr das?

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

16.842 Beiträge seit 2008
vor einem Jahr

Das Thema ändert die ja in gewisser Weise die generelle Art zu arbeiten, daher ist es denke ich wichtig, mehr als nur die eigene Meinung zu kennen.
Ich persönlich finde es super (Argumente lasse ich bewusst weg), aber vielleicht liege ich da auch falsch?

Außerdem möchte ich meine Kollegen davon überzeugen, es zu nutzen, oder - sollte ich falsch liegen - ich lasse es doch lieber bleiben.

Nullable Reference Types sind prinzipiell nur eine Compiler Hilfe und ändert zur Runtime - im Gegensatz zum heißen Thema "Bang Operator" - ja nichts.
Es ändert daher auch quasi kaum die Arbeit.

Die Kolleg:innen kann man eigentlich super einfach überzeugen: es ist eine geschenkte Compiler Hilfe, stabileren Code zu schreiben.
Natürlich sollte man das so schnell wie möglich nutzen. Sich dagegen auszusprechen zeigt ein grundlegendes Unverständnis der Funktionsweise und Nachteile ("the billion dollar mistake") von OOP.

Der Beitrag von Mats (Introducing Nullable Reference Types in C#) erklärt auch, warum NRT passiv implementiert wurden, und wieso null immernoch als Value möglich ist (TLDR; Backward Compatibility).

Palladin007 Themenstarter:in
2.080 Beiträge seit 2012
vor einem Jahr

warum NRT passiv implementiert wurden, und wieso null immernoch als Value möglich ist

Neben der Backward Compatibility ist null mMn. auch noch ein durchaus vernünftiger "Wert", WENN alle Beteiligten wissen, dass null erlaubt ist - naja und wenn es inhaltlich sinnvoll ist, aber das sagt sich so leicht.

Das ist auch das für mich wichtigste Argument:
Das Feature ist ein (wenn auch technisch nicht ausschlaggebender) Teil der Member-Definition.
Jeder weiß, was null sein darf und was nicht, man muss nicht erst den Code absuchen.

Die Kolleg:innen kann man eigentlich super einfach überzeugen: es ist eine geschenkte Compiler Hilfe, stabileren Code zu schreiben.

Hat leider nicht geholfen 😠

Gegenargument:
Man sollte einfach pauschal immer davon ausgehen, dass es null sein kann und entsprechend darauf prüfen.
Wenn ein Entwickler vom Compiler darauf hingewiesen wird, auf null zu prüfen, dann setzt der ggf. ein sinnloses Default-Verhalten (oder Werte) um, die dann erst irgendwann später als Logik-Fehler auffallen. Im schlimmsten Fall landet so ein unsinniger Default-Wert in der DB und sorgt für Stress, den da wieder raus zu kriegen.
So betrachtet ist es besser, dass es in eine NullReferenceException rennt, sofort auffällt und dann auch leicht(er) zu finden ist, als ein Logik-Fehler.
(Inhaltliches Zitat: Kollege)

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

16.842 Beiträge seit 2008
vor einem Jahr

Man sollte einfach pauschal immer davon ausgehen, dass es null sein kann und entsprechend darauf prüfen.

Das is so nen Totschlagargument der defensiven Programmierung, wenn man die letzten 30 Jahre Software Entwicklung verpasst hat.

Klar, man kann immer alles auf null prüfen.
Ist das sinnvoll? Nein. Ist das realistisch? Nein.
Und eben darum gibt es solche Dinge wie NRT.

NRT sind Teil von defensiver Programmierung in moderner Art und Weise.

Im schlimmsten Fall landet so ein unsinniger Default-Wert in der DB und sorgt für Stress, den da wieder raus zu kriegen.

Wenn dadurch ein "unsinniger Default Wert" entsteht, dann ist der Fehler viel weiter vorne in der Software Architektur / Verantwortung.
NullReferenceExceptions hilft Dir hier gar nicht, ausser dass ein Risiko entsteht. NullReferenceExceptions sind immer Entwicklungsfehler.

6.911 Beiträge seit 2009
vor einem Jahr

Hallo Palladin007,

in einer streng typisierten Programmiersprache gibt es keinen Grund NRTs nicht zu verwenden.

NRTs sind Teil vom Typsystem (während der Entwurfszeit) bzw. eine Erweiterung / Ergänzung dessen.
Etwas überspitzt: diese nicht zu verwenden wäre wie alle Argumente mit dynamic* antatt konkreter Typen zu schreiben. Oder eine untypisierte zu verwenden...

Wer also Typsicherheit haben will, sollte auch NRTs verwenden. Am besten mit <TreatWarningsAsErrors>true</TreatWarningsAsErrors> in den Projekteigenschaften.

* Laufzeitverhalten außer Acht gelassen, denn dynamic ist langsam

So betrachtet ist es besser, dass es in eine NullReferenceException rennt, sofort auffällt und dann auch leicht(er) zu finden ist, als ein Logik-Fehler.

Hier liegt mMn bei vielen ein Verständnisfehler vor.
NRT sind eine Annotation, die zur Entwurfszeit via Compiler eine Hilfe gibt.
Das Laufzeitverhalten wird dadurch nicht beeinflusst. D.h. zur Laufzeit sind (bei öffentlichen / public APIs) daher sehr wohl Null-Prüfungen durchzuführen.
Siehe z.B. auch eine Diskussion im dotnet-docs Repo (und die Kommentare darunter) dazu.

Der große Vorteil von NRTs ist, dass der Compiler durch statische Analyse prüfen kann ob null zulässig ist od. nicht.

Da eben public APIs dennoch auf Null geprüft werden sollen, findet der große Vorteil bei internen APIs Anwendung. "Intern" ist hier nicht nur auf die internal, private, etc. (also alles außer public) begrenzt, sondern kann auch eine public-Methode sein, die nur innerhalb einer Anwendung / einer Organisation verwendet wird.
Anders gesagt: wenn nicht public im Sinne von "Verwendung für alle" sein soll, so können die Null-Checks auch entfallen falls die statische Prüfung vom Compiler ausreicht -- ergänzt (natürlich) mit Unit-Tests.

Auch public APIs profitieren von NRTs, da so via Methoden-Signatur ersichtlich ist ob null erlaubt ist od. ob bei (falscher) Übergabe von null eine NRE (besser: ArgumentNullException von der Validierung) geschmissen wird. D.h.


public int Foo(object? obj)
{
    // null kann für obj übergeben werden, die Implementierung hier muss damit umgehen können

    return obj is null 
        ? 0
        : obj.GetHashCode();
}

public int Bar(object obj)
{
    // null ist nicht gestattet, daher sollte das Argument validiert werden
    ArgumentNullException.ThrowIfNull(obj);

    return obj.GetHashCode();
}

internal int Baz(object obj)
{
    // null ist nicht gestatt, es ist ein kein öffentliches API, daher reicht eine Debug-Prüfung aus
    Debug.Assert(obj is not null);

    return obj.GetHashCode();
}

Auch sind die weiteren Annotation wie [NotNullWhen], etc. sehr hilfreich v.a. im Zusammenhang mit Try-Patterns, da so der Compiler im Zuge des "control flows" weiß wann der Wert null ist od. nicht.
Das hilft einfach Fehler zu vermeiden. Und zwar nicht erst bei Tests od. noch später, sondern während des Schreibens vom Code. Und es ist hinlänglich bekannt, je früher ein Fehler erkannt / behoben wird, desto weniger Kosten wurden dadurch verursacht.

Man sollte einfach pauschal immer davon ausgehen, dass es null sein kann und entsprechend darauf prüfen.

Ergänzend zu Abts Kommentar dazu, dem ich zustimme, noch folgendes Analogon: bei Werttypen kann auch einfach default verwendet werden, aber prüft jemand ob der Wert default ist od. nicht?
Ja, der Vergleich hinkt und es gibt auch Nullable-ValueTypes, aber der Kern der Aussage ist: durch Annotationen und Asserts lassen sich zur Entwurfszeit viele Fehler vermeiden ohne dass zur Laufzeit alles geprüft werden muss / braucht. Unit-Tests erledigen den Rest.

Wenn ein Entwickler vom Compiler darauf hingewiesen wird, auf null zu prüfen, dann setzt der ggf. ein sinnloses Default-Verhalten (oder Werte) um, die dann erst irgendwann später als Logik-Fehler auffallen.

Dann liegt das Problem aber zwischen Bildschirm und Bürostuhl 😉

... die dann erst irgendwann später als Logik-Fehler auffallen.

Das sollte doch bei Unit-Tests schon auffallen. Spätestens bei Integrations-Tests müsste das bemerkt werken -- idealerweise durch CI.
Wobei der Compiler nicht darauf hinweist "auf null zu prüfen", sondern der Compiler weist darauf hin, dass der Wert unerlaubterweise null ist.

NullReferenceExceptions sind immer Entwicklungsfehler.

👍 -- auch wenn "fail early" gut ist, sind NREs einfach Bugs die durch Tests detektiert werden sollten bevor ein Produkt veröffentlicht wird.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Palladin007 Themenstarter:in
2.080 Beiträge seit 2012
vor einem Jahr

Ich merke, wir sind uns einig 😁
Das heißt zumindest, dass meine Ansicht nicht so falsch sein kann( - man sollte bei so Grundsatzdiskussionen ja bereit sein, den Fehler bei sich selbst zu suchen.

Und TreatWarningsAsErrors=true versuche ich auch noch durchzusetzen, es gibt ja leider immer noch Leute, die Warnungen ignorieren.

Und es ist hinlänglich bekannt, je früher ein Fehler erkannt / behoben wird, desto weniger Kosten wurden dadurch verursacht.

Das ist ein guter Punkt, daran hatte ich noch nicht gedacht - danke dafür.

Hier liegt mMn bei vielen ein Verständnisfehler vor.

Und dem werde ich auch mal nachgehen, da das Thema vorher scheinbar niemandem bekannt war ((was ich auch irgendwie bedenklich finde)

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

16.842 Beiträge seit 2008
vor einem Jahr

es gibt ja leider immer noch Leute, die Warnungen ignorieren.

Gibt solche Warnungen und solche.
Null-Reference Issue Warnings zu ignorieren hat aber eine direkte Auswirkung auf die Stabilität der Anwendung, und daher ein High Risk Warning.

Palladin007 Themenstarter:in
2.080 Beiträge seit 2012
vor einem Jahr

Ich persönlich würde immer mit <TreatWarningsAsErrors>true</TreatWarningsAsErrors> arbeiten.
Wenn manche Warnungen gar nicht beachtet werden sollen, kann man sie immer noch in der EditorConfig abschalten - dank Git kann man dann aber auch hervorragend nachlesen, wer da was nicht sehen will.
Oder man deaktiviert die Warnungen einzeln im Code - samt Begründung natürlich.

So mache ich das (wenn ich denn darf) auch: Warnung = Fehler und wenn der Code Absicht ist, wird die Warnung je Situation unterdrückt.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.