Odwołania, które nie mają wartości zerowej w języku C # 8, oraz wzorzec Try

23

Istnieje wzorzec w klasach C # zilustrowany przez Dictionary.TryGetValuei int.TryParse: metodę, która zwraca wartość logiczną wskazującą powodzenie operacji i parametr wyjściowy zawierający rzeczywisty wynik; jeśli operacja się nie powiedzie, parametr wyjściowy ma wartość null.

Załóżmy, że używam odwołań, które nie są zerowalne w języku C # 8, i chcę napisać metodę TryParse dla mojej własnej klasy. Poprawny podpis brzmi:

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

Ponieważ w fałszywym przypadku wynik jest zerowy, zmienna wyjściowa musi być oznaczona jako pusta.

Jednak wzorzec Try jest ogólnie używany w następujący sposób:

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

Ponieważ wprowadzam gałąź tylko wtedy, gdy operacja się powiedzie, wynik nigdy nie powinien być pusty w tej gałęzi. Ale ponieważ oznaczyłem to jako zerowalne, muszę teraz to sprawdzić lub użyć !do zastąpienia:

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
}

To jest brzydkie i trochę nieergonomiczne.

Ze względu na typowy wzorzec użytkowania mam inną opcję: kłamstwo na temat typu wyniku:

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

Teraz typowe użycie staje się przyjemniejsze:

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

ale nietypowe użycie nie wyświetla ostrzeżenia:

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

Teraz nie jestem pewien, w którą stronę się udać. Czy istnieje oficjalna rekomendacja zespołu języka C #? Czy jest już jakiś kod CoreFX przekonwertowany na odwołania, które nie mają wartości zerowej, które mogłyby mi pokazać, jak to zrobić? (Poszedłem szukać TryParsemetod. IPAddressJest klasą, która ma jedną, ale nie została przekonwertowana w gałęzi master corefx.)

Jak radzi Dictionary.TryGetValuesobie z tym ogólny kod ? (Prawdopodobnie ze specjalnym MaybeNullatrybutem z tego, co znalazłem.) Co się stanie, gdy utworzę instancję Dictionaryz wartością o wartości innej niż null?

Sebastian Redl
źródło
Nie próbowałem tego (dlatego nie piszę tego jako odpowiedzi), ale dzięki nowej funkcji dopasowania wzorca instrukcji switch, domyślam się, że jedną z opcji jest po prostu zwrócenie nullable referencji (bez wzorca Try, return MyClass?) i wykonaj na nim przełącznik za pomocą a case MyClass myObj:i (opcjonalnie) case null:.
Filip Milovanović
+1 Naprawdę podoba mi się to pytanie, a kiedy musiałem nad tym popracować, zawsze użyłem dodatkowego sprawdzenia zerowego zamiast zastąpienia - co zawsze wydawało się trochę niepotrzebne i nieeleganckie, ale nigdy nie było w kodzie krytycznym pod względem wydajności więc po prostu odpuściłem. Byłoby miło zobaczyć, czy istnieje czystszy sposób radzenia sobie z tym!
BrianH

Odpowiedzi:

10

Wzorzec bool / out-var nie działa dobrze z nullable typami referencyjnymi, jak opisano. Zamiast walczyć z kompilatorem, użyj tej funkcji, aby uprościć rzeczy. Dodaj ulepszone funkcje dopasowywania wzorów w C # 8 i możesz potraktować zerowalne odniesienie jako „typ biednego mężczyzny”:

public static MyClass? TryParse(string s) => 



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

W ten sposób unikniesz bałaganu przy outparametrach i nie będziesz musiał walczyć z kompilatorem o mieszanie nullz niedopuszczalnymi wartościami referencyjnymi.

Jak radzi Dictionary.TryGetValuesobie z tym ogólny kod ?

W tym momencie upadł „typ biednego człowieka”. Wyzwanie, przed którym stoisz, polega na tym, że podczas korzystania z typów zerowalnych (NRT) kompilator będzie traktowany Foo<T>jako nie dopuszczający wartości zerowych. Ale spróbuj go zmienić Foo<T?>i będzie chciał, aby Tograniczenia były ograniczone do klasy lub struktury, ponieważ typy wartości dopuszczających wartości zerowe są zupełnie inne niż punkt widzenia CLR. Istnieje wiele obejść tego problemu:

  1. Nie włączaj funkcji NRT,
  2. Zacznij używać default(wraz z !) outparametrów, nawet jeśli Twój kod rejestruje się bez zer,
  3. Użyj prawdziwy Maybe<T>typ jako wartość zwracana, która nigdy potem nulli owija, że booli out Tdo HasValuei Valuewłaściwości lub coś w tym rodzaju,
  4. Użyj krotki:
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
}

Ja osobiście preferuję używanie, Maybe<T>ale wsparcie dekonstrukcji, aby można było dopasować wzór jako krotkę jak w 4 powyżej.

David Arno
źródło
2
TryParse(someString) is {} myClass- Ta składnia wymaga przyzwyczajenia się, ale podoba mi się ten pomysł.
Sebastian Redl
TryParse(someString) is var myClasswygląda mi łatwiej.
Olivier Jacot-Descombes
2
@ OlivierJacot-Descombes To może wyglądać łatwiej ... ale to nie zadziała. Wzór samochodu zawsze pasuje, więc x is var yzawsze będzie prawdziwy, bez względu na xto , czy jest zerowy, czy nie.
David Arno
15

Jeśli przybywających na to trochę późno, jak ja, okazuje się, że zespół NET skierowana go przez kilka atrybutów parametrów jak MaybeNullWhen(returnValue: true)w System.Diagnostics.CodeAnalysisprzestrzeni, które można wykorzystać do wzoru try.

Na przykład:

jak radzi sobie z tym ogólny kod, taki jak Dictionary.TryGetValue?

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

co oznacza, że ​​zostaniesz wykrzyczany, jeśli nie sprawdzisz 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"

Dalsze szczegóły:

Nick Darvey
źródło
3

Nie sądzę, żeby tu był konflikt.

twój sprzeciw wobec

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

jest

Ponieważ wprowadzam gałąź tylko wtedy, gdy operacja się powiedzie, wynik nigdy nie powinien być pusty w tej gałęzi.

Jednak w rzeczywistości nic nie stoi na przeszkodzie przypisaniu wartości null do parametru out w funkcjach TryParse w starym stylu.

na przykład.

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

Ostrzeżenie podane programiście, gdy używają parametru out bez sprawdzania, jest prawidłowe. Powinieneś sprawdzać!

Będzie mnóstwo przypadków, w których będziesz zmuszony zwrócić typy zerowalne, w których główna gałąź kodu zwraca typ nie dopuszczający wartości zerowych. Ostrzeżenie jest tylko po to, aby pomóc ci je wyrazić. to znaczy.

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

Niepozwalający na kod sposób spowoduje wygenerowanie wyjątku tam, gdzie byłby zerowy. Niezależnie od tego, czy parsujesz, dostajesz czy pierwszy

MyClass x = (new List<MyClass>()).First(i=>i==1);
Ewan
źródło
Nie sądzę, że FirstOrDefaultmożna to porównać, ponieważ nieważność wartości zwracanej jest głównym sygnałem. W TryParsemetodach parametr out nie jest pusty, jeśli zwracana wartość jest prawdą, jest częścią kontraktu metody.
Sebastian Redl
nie jest to częścią umowy. jedyne, co jest zapewnione, to że coś jest przypisane do parametru out
Ewan
Tego zachowania oczekuję od TryParsemetody. Jeśli IPAddress.TryParsekiedykolwiek zwrócił prawdę, ale nie przypisał wartości zerowej do parametru out, zgłosiłbym to jako błąd.
Sebastian Redl
Twoje oczekiwania są zrozumiałe, ale nie są egzekwowane przez kompilator. Więc pewnie, specyfikacja IpAddress może powiedzieć, że nigdy nie zwraca true i null, ale mój przykład JsonObject pokazuje przypadek, w którym zwracanie null może być poprawne
Ewan
„Twoje oczekiwania są zrozumiałe, ale nie są egzekwowane przez kompilator”. - Wiem, ale moje pytanie brzmi: jak najlepiej napisać kod, aby wyrazić umowę, o której myślę, a nie jaka jest umowa z innym kodem.
Sebastian Redl