Nicht nullfähige C # 8-Referenzen und das Try-Muster

22

Es ist ein Muster , in C # Klassen veranschaulicht durch , Dictionary.TryGetValueund int.TryParse: ein Verfahren , das gibt einen booleschen angibt Erfolg einer Operation und einem Out - Parameter das tatsächliche Ergebnis enthält; Wenn der Vorgang fehlschlägt, wird der Parameter out auf null gesetzt.

Angenommen, ich verwende C # 8-Referenzen, die nicht nullwertfähig sind, und möchte eine TryParse-Methode für meine eigene Klasse schreiben. Die korrekte Signatur lautet:

public static bool TryParse(string s, out MyClass? result);

Da das Ergebnis im falschen Fall null ist, muss die Variable out als nullbar markiert werden.

Das Try-Muster wird jedoch im Allgemeinen folgendermaßen verwendet:

if (MyClass.TryParse(s, out var result))
{
  // use result here
}

Da ich den Zweig nur betrete, wenn der Vorgang erfolgreich ist, sollte das Ergebnis in diesem Zweig niemals null sein. Aber da ich es als nullbar markiert habe, muss ich es jetzt entweder überprüfen oder !zum Überschreiben verwenden:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // compiler warning, could be null
  Console.WriteLine("Look: {0}", result!.SomeProperty); // need override
}

Das ist hässlich und ein bisschen unergonomisch.

Aufgrund des typischen Verwendungsmusters habe ich eine andere Option: Lüge über den Ergebnistyp:

public static bool TryParse(string s, out MyClass result) // not nullable
{
   // Happy path sets result to non-null and returns true.
   // Error path does this:
   result = null!; // override compiler complaint
   return false;
}

Jetzt wird die typische Verwendung schöner:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // no warning
}

atypische Nutzung wird jedoch nicht entsprechend gewarnt:

else
{
  Console.WriteLine("Fail: {0}", result.SomeProperty);
  // Yes, result is in scope here. No, it will never be non-null.
  // Yes, it will throw. No, the compiler won't warn about it.
}

Jetzt bin ich mir nicht sicher, welchen Weg ich hier einschlagen soll. Gibt es eine offizielle Empfehlung des C # -Sprachteams? Gibt es CoreFX-Code, der bereits in nicht nullfähige Verweise konvertiert wurde, die mir zeigen könnten, wie das geht? (Ich habe nach TryParseMethoden IPAddressgesucht. Ist eine Klasse, die eine hat, aber sie wurde im Master-Zweig von corefx nicht konvertiert.)

Und wie geht generischer Code damit Dictionary.TryGetValueum? (Möglicherweise mit einem speziellen MaybeNullAttribut aus dem, was ich gefunden habe.) Was passiert, wenn ich einen Dictionarymit einem nicht nullwertfähigen Werttyp instanziiere ?

Sebastian Redl
quelle
Ich habe es nicht ausprobiert (weshalb ich dies nicht als Antwort schreibe), aber mit der neuen Mustervergleichsfunktion von switch-Anweisungen besteht eine Möglichkeit darin, einfach die nullfähige Referenz zurückzugeben (kein Try-Muster, return MyClass?), und schalten Sie es mit einem case MyClass myObj:und (optional) ein case null:.
Filip Milovanović
+1 Ich mag diese Frage wirklich und wenn ich damit arbeiten musste, habe ich immer einen zusätzlichen Null-Check anstelle des Overrides verwendet - was sich immer als unnötig und unelegant anfühlte, aber niemals in leistungskritischem Code also lass ich es einfach los. Wäre schön zu sehen, ob es eine sauberere Art gibt, damit umzugehen!
BrianH

Antworten:

10

Das bool / out-var-Muster funktioniert nicht gut mit nullfähigen Referenztypen, wie Sie beschreiben. Verwenden Sie die Funktion, um die Dinge zu vereinfachen, anstatt den Compiler zu bekämpfen. Wenn Sie die verbesserten Mustervergleichsfunktionen von C # 8 verwenden, können Sie eine nullfähige Referenz als "armen Mannes vielleicht Typ" behandeln:

public static MyClass? TryParse(string s) => 



if (TryParse(someString) is {} myClass)
{
    // myClass wasn't null, we are good to use it
}

Auf diese Weise vermeiden Sie das Herumspielen von outParametern und müssen sich nicht mit dem Compiler nullherumschlagen, wenn es um das Mischen mit nicht nullwertfähigen Referenzen geht.

Und wie geht generischer Code damit Dictionary.TryGetValueum?

Zu diesem Zeitpunkt fällt dieser "arme Mann ist vielleicht Typ". Die Herausforderung besteht darin, dass der Compiler bei Verwendung von NRT-Referenztypen (nullable reference types) diese Foo<T>als nicht nullable behandelt. Aber versuchen Sie es zu ändern Foo<T?>und es wird gewünscht, dass die TEinschränkung auf eine Klasse oder Struktur, da nullbare Werttypen eine ganz andere Sache aus Sicht der CLR sind. Hierfür gibt es eine Vielzahl von Umgehungsmöglichkeiten:

  1. Aktivieren Sie nicht die NRT-Funktion,
  2. Beginnen Sie mit der Verwendung von default(zusammen mit !) für outParameter, obwohl sich Ihr Code ohne Nullen anmeldet.
  3. Verwenden Sie einen reellen Maybe<T>Typ als Rückgabewert, der dann never ist nullund das boolund out Tin HasValueund Valueproperties oder so einschließt,
  4. Verwenden Sie ein Tupel:
public static (bool success, T result) TryParse<T>(string s) => 


if (TryParse<MyClass>(someString) is (true, var result))
{
    // result is valid here, as success is true
}

Ich persönlich bevorzuge Maybe<T>es, ein Dekonstrukt zu verwenden, aber zu haben, damit es als Tupel wie in 4 oben musterangepasst werden kann.

David Arno
quelle
2
TryParse(someString) is {} myClass- Diese Syntax ist gewöhnungsbedürftig, aber die Idee gefällt mir.
Sebastian Redl
TryParse(someString) is var myClasssieht für mich einfacher aus.
Olivier Jacot-Descombes
2
@ OlivierJacot-Descombes es mag einfacher aussehen ... aber es wird nicht funktionieren. Das Automuster stimmt immer überein und x is var yist immer wahr, unabhängig davon, ob xes null ist oder nicht.
David Arno
15

Wenn Sie, wie ich, etwas spät dazu kommen, hat sich herausgestellt, dass das .NET-Team dies durch eine Reihe von Parameterattributen wie MaybeNullWhen(returnValue: true)in dem System.Diagnostics.CodeAnalysisBereich, den Sie für das Testmuster verwenden können, angesprochen hat .

Beispielsweise:

Wie geht generischer Code wie Dictionary.TryGetValue damit um?

bool TryGetValue(TKey key, [MaybeNullWhen(returnValue: false)] out TValue value);

was bedeutet, dass Sie angeschrien werden, wenn Sie nicht nach einem suchen true

// This is okay:
if(myDictionary.TryGetValue("cheese", out var result))
{
  var more = result * 42;
}

// But this is not:
_ = myDictionary.TryGetValue("cheese", out var result);
var more = result * 42;
// "CS8602: Dereference of a potentially null reference"

Weitere Details:

Nick Darvey
quelle
3

Ich glaube nicht, dass es hier einen Konflikt gibt.

Ihr Einwand gegen

public static bool TryParse(string s, out MyClass? result);

ist

Da ich den Zweig nur betrete, wenn der Vorgang erfolgreich ist, sollte das Ergebnis in diesem Zweig niemals null sein.

Tatsächlich steht der Zuweisung von null zum out-Parameter in den TryParse-Funktionen des alten Stils jedoch nichts entgegen.

z.B.

MyJsonObject.TryParse("null", out obj) //sets obj to a null MyJsonObject and returns true

Die Warnung, die dem Programmierer gegeben wird, wenn er den out-Parameter ohne Überprüfung verwendet, ist korrekt. Sie sollten überprüfen!

Es wird viele Fälle geben, in denen Sie gezwungen sind, nullfähige Typen zurückzugeben, während der Hauptzweig des Codes einen nicht nullfähigen Typ zurückgibt. Die Warnung soll Ihnen nur dabei helfen, diese explizit zu machen. dh

MyClass? x = (new List<MyClass>()).FirstOrDefault(i=>i==1);

Die nicht nullfähige Codierung löst eine Ausnahme aus, bei der eine Null aufgetreten wäre. Egal ob Sie analysieren, lesen oder anfangen

MyClass x = (new List<MyClass>()).First(i=>i==1);
Ewan
quelle
Ich glaube nicht FirstOrDefaultverglichen werden, da die nullness seines Rückgabewertes ist das Hauptsignal. In den TryParseMethoden ist der Parameter out, der nicht null ist, wenn der Rückgabewert true ist, Teil des Methodenvertrags.
Sebastian Redl
es ist nicht Bestandteil des Vertrages. Das einzige, was sichergestellt ist, ist, dass dem out-Parameter etwas zugewiesen ist
Ewan,
Das ist das Verhalten, das ich von einer TryParseMethode erwarte . Wenn IPAddress.TryParsejemals true zurückgegeben wurde, dem out-Parameter jedoch kein Wert ungleich null zugewiesen wurde, würde ich dies als Fehler melden.
Sebastian Redl
Ihre Erwartung ist verständlich, wird aber vom Compiler nicht durchgesetzt. So sicher, die Spezifikation für IpAddress könnte sagen, dass nie true und null zurückgegeben werden, aber mein JsonObject-Beispiel zeigt einen Fall, in dem die Rückgabe von null korrekt sein könnte
Ewan
"Ihre Erwartung ist verständlich, wird aber vom Compiler nicht durchgesetzt." - Ich weiß, aber meine Frage ist, wie ich meinen Code am besten schreiben kann, um den Vertrag auszudrücken, den ich mir vorgenommen habe, und nicht, wie der Vertrag eines anderen Codes lautet.
Sebastian Redl