2016-11-25 20 views
9

W języku C#, chciałbym mieć możliwość obsługi błędów w bardziej "funkcjonalny" sposób, niż zawsze za pomocą domyślnego modelu throw-throwing. W niektórych scenariuszach model rzucania jest świetny, ponieważ pozwala wymusić wykonanie kodu, jeśli wydarzy się coś nieoczekiwanego. Jednak model rzucania ma kilka poważnych wad:Czy istnieje typ .NET, taki jak (wartość OR błąd), dyskryminowany związek?

  1. Rzucanie wyjątków to zasadniczo cały drugorzędny system sterowania aplikacjami, który podważa główny system przepływu sterowania. Gdy wywoływana jest metoda, jej ciało jest wykonywane, a następnie skutecznie kontroluje zwroty do osoby dzwoniącej, niezależnie od tego, czy było to spowodowane oświadczeniem return lub throw. Jeśli kontrola zwrócona do wywołującego z instrukcji throw, sterowanie natychmiast przeniesie się do swojego wywołującego, jeśli nie ma odpowiedniego zakresu w zakresie; jeśli kontrola powróci z instrukcji return, kontrola będzie kontynuowana jak zwykle. Wiem, że wdrożenie jest o wiele bardziej skomplikowane i subtelniejsze, ale uważam, że jest to odpowiednie podsumowanie pojęciowe. Ten podwójny system może być ogólnie mylący, zarówno w czasie projektowania, jak iw czasie działania na różne sposoby.
  2. System rzucania staje się bardziej niezręczny w scenariuszach równoległych lub asynchronicznych, co widać w procedurach błędów wykonanych przez System.Threading.Tasks.Task. Wszelkie wyjątki generowane przez wykonywanie Task są przechowywane w kodzie AggregateException i są dostępne za pośrednictwem właściwości Task.Exception przez kod wywołujący. Tak więc, podczas gdy wykonywanie Task s może zostać przerwane, kod wywołujący musi szukać błędów przechowywanych we właściwościach obiektu, używając normalnego przepływu sterowania C#.
  3. Oprócz komentarzy XML, nie ma metadanych na temat tego, czy metoda może generować wyjątki, czy może ją wyrzucać. Wyjątki zachowują się jako alternatywna forma wyjścia metody, ale są w dużej mierze ignorowane przez system typów. Na przykład metoda może dać albo liczbę całkowitą albo DivideByZeroException, ale podpis tej metody nie sugeruje niczego odnośnie błędów. Odwrotnie, często słyszałam skargi na sprawdzone wyjątki Java, w których każdy specyficzny typ wyjątku, który metoda może wyrzucić, musi zostać dodany do jego podpisu, co może być bardzo rozwlekłe. Dla mnie najprostszym środkowym obszarem byłby typ ogólny, taki jak Nullable<T>, który zawiera wartość lub wyjątek. Metoda taka jak Divide miałaby wówczas taką sygnaturę: Fallible<int> Divide(int x, int y). Wszelkie operacje korzystające z wyniku będą musiały zajmować się przypadkiem błędu. Metody mogą również przyjmować parametry, aby umożliwić łatwiejsze łańcuchowanie.

Oto implementacja Fallible ja naszkicował:

public class Fallible<T> : IEquatable<Fallible<T>> { 

    #region Constructors 

    public Fallible() { 
     //value defaults to default(T) 
     //exception defaults to null 
    } 

    public Fallible(T value) : this() { 
     this.value = value; 
    } 

    public Fallible(Exception error) : this() { 
     if (error == null) throw new ArgumentNullException(nameof(error)); 
     Error = error; 
    } 

    public Fallible(Func<T> getValue) : this() { 
     if (error == null) throw new ArgumentNullException(nameof(getValue)); 
     try { 
      this.value = getValue(); 
     } 
     catch(Exception x) { 
      Error = x; 
     } 
    } 

    #endregion 


    #region Properties 

    public T Value { 
     get { 
      if (!HasValue) throw new InvalidOperationException("Cannot get Value if HasValue is false."); 
      return value; 
     } 
    } 
    private T value; 

    public Exception Error { get; } 

    public bool HasValue => Error == null; 

    #endregion 


    #region Equality 

    public bool Equals(Fallible<T> other) => (other != null) 
     && Equals(Error, other.Error) 
     && Equals(Value, other.Value); 

    public override bool Equals(object obj) => Equals(obj as Fallible<T>); 

    public static bool operator ==(Fallible<T> a, Fallible<T> b) { 
     if (a == null) return b == null; 
     return a.Equals(b); 
    } 

    public static bool operator !=(Fallible<T> a, Fallible<T> b) { 
     if (a == null) return b != null; 
     return !a.Equals(b); 
    } 

    public override int GetHashCode() => 
     HasValue 
      ? Value.GetHashCode() 
      : Error.GetHashCode(); 

    #endregion 


    public override string ToString() => 
     HasValue 
      ? $"Fallible{{{Value}}}" 
      : $"Fallible{{{Error.GetType()}: {Error.Message}}}"; 
} 

i pytania:

  1. Czy istnieje już coś takiego jak Fallible<T> dla .NET? Być może klasa w bibliotece zadań równoległych, rozszerzeniach reaktywnych lub podstawowych bibliotekach F #?
  2. Czy są jakieś poważne braki we wdrażaniu powyżej?
  3. Czy są jakieś problemy koncepcyjne z przepływem sterowania, które może być niewidoczne?
+1

Nie są znane żadne wbudowane w podobne konstrukcje. Pomysł jest interesujący i może mieć ważne przypadki użycia, ale zauważmy, że prawie każde wywołanie takiej metody będzie zwykle poprzedzane przez if-else w celu sprawdzenia błędów, podczas gdy try-catch ma tę zaletę, że jest w stanie ochronić większy blok logiki. Również wywoływanie kaskadowe wywołań metod zwracających 'Fallible' może być trudne, a wyjątki będą się domyślnie bąkać domyślnie bez zaśmiecania kodu, który nie jest zainteresowany ich obsługą. – KMoussa

+0

Implementacja jest dość prosta i wygląda dobrze dla mnie. Możesz użyć tego podejścia, ale inni deweloperzy (użytkownicy Twojej biblioteki lub współpracownicy) mogą nie być z niego zadowoleni (ponieważ jest to sprzeczne z tym, jak projektowany jest framework .NET). Ale jeśli użyjesz go dla siebie - na pewno, dlaczego nie. – Evk

+0

Oto przykład, w którym F # użył właśnie 2-elementowego DU https://msdn.microsoft.com/en-us/visualfsharpdocs/conceptual/async.catch%5B't%5D-method-%5Bfsharp%5D –

Odpowiedz

5

Nie ma nic takiego jak ten wbudowany w BCL, choć FSharp.Core zawiera obsługę opcji, które zapewniają inny mechanizm, dzięki któremu można uniknąć obsługi wyjątków.

Projekt Language Ext zawiera typ Try<T>, który jest bardzo podobny do opisywanego. Ponadto ma przyjazne C# Option<T> z dopasowaniem pseudopoprawek dla operacji na opcjach.

+0

Język Ext jest czymś, czego szukałem, dzięki. – JamesFaix

2

Projekt https://github.com/mcintyre321/OneOf jest biblioteką C# przeznaczoną do naśladowania F # dyskryminowanych związków w sposób ogólny. Możesz użyć tego do zaimplementowania swojego własnego typu Result jako OneOf<ResultData, ErrorData> (gdzie zdefiniowałeś klasy ResultData i ErrorData z określonymi rzeczami, których potrzebuje twój model danych).