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?
- 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
lubthrow
. Jeśli kontrola zwrócona do wywołującego z instrukcjithrow
, sterowanie natychmiast przeniesie się do swojego wywołującego, jeśli nie ma odpowiedniego zakresu w zakresie; jeśli kontrola powróci z instrukcjireturn
, 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. - 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 wykonywanieTask
są przechowywane w kodzieAggregateException
i są dostępne za pośrednictwem właściwościTask.Exception
przez kod wywołujący. Tak więc, podczas gdy wykonywanieTask
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#. - 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 jakNullable<T>
, który zawiera wartość lub wyjątek. Metoda taka jakDivide
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:
- 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 #? - Czy są jakieś poważne braki we wdrażaniu powyżej?
- Czy są jakieś problemy koncepcyjne z przepływem sterowania, które może być niewidoczne?
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
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
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 –