A
SideWinder schrieb:
Evtl. MaybeResult und MaybeVoid? Maybe und MaybeVoid?
Maybe<T> hat ja schon anerkanntermaßen eine ähnliche Bedeutung wie Nullable<T> , und die ist anders als Exceptional<T> : ersteres liefert einen Wert oder keinen Wert, letzteres einen Wert oder eine Fehlermeldung. Und außerdem, was heißt dann Maybe und MaybeVoid ? Vielleicht keinen Wert? Und was ist dann andernfalls?
SideWinder schrieb:
Vielleicht sage ich sicherheitshalber noch ausdrücklich, dass ich dieses Vorgehen überhaupt nicht gutheißen kann. Ist ja quasi nur Schnickschnack für ein Tuple<T, Exception>
Nein, ist es nicht. Eine korrekte Entsprechung wäre Union<T, NonNullable<Exception>> , oder meinetwegen Either<T, Exception> . Einem Tupel könntest du ein T und eine Exception mitgeben, und das macht keinen Sinn.
SideWinder schrieb:
und das wiederum nur ein Schnickschnack für HRESULT und das wiederum nur ein Schnickschnack für GetLastError()
Auch nicht. COM benutzt HRESULT und IErrorInfo , um Exceptions abzubilden, und das Win32-API benutzt GetLastError() und (meistens) einen BOOL -Rückgabewert für denselben Zweck. Beides sind technische Details von Low-level-APIs. Und wir sind ja in C#, wo wir den Umgang mit derartigen Schnittstellen möglichst vermeiden, geschweige denn welche davon nachbauen, haben wir doch die Abstraktionsmittel einer ausgewachsenen modernen Sprache zur Verfügung
Der Anwendungsfall, der mich auf diese Geschichte gebracht hat, ist der folgende:
In einer GUI-Anwendung möchte ich die Benutzereingabe auf Gültigkeit prüfen. Über ein Databinding ist z.B. eine Textbox mit einem String-Property verbunden. Der Setter führt natürlich eine Prüfung durch und wirft ggf. eine Exception; aber natürlich will ich nicht, daß eine Exception fliegt, nur weil der Benutzer eine Fehleingabe tätigt. Aber die Fehlermeldung, die der Exception beiliegen würde, möchte ich dem Benutzer z.B. via ErrorProvider anzeigen.
Der erste Versuch war eine Validatorfunktion, die die Exception nicht warf, sondern einfach zurückgab:
Exception ValidateName(string input)
{
if (input.Length > 200) return new ArgumentException("Value may not exceed 200 characters");
...
return null;
}
Daran gefiel mir allerdings der null -Rückgabewert nicht, weil null nichtssagend ist: hat die Funktion jetzt einen Bug, weil sie null zurückgibt, oder bedeutet null , daß es keinen Fehler gab? Selbst wenn ich das für die Funktion klarstellen kann, z.B. indem ich sie in GetNameValidationErrorOrNull() umbenenne, bleibt das Problem bestehen, wenn ich das Funktionsergebnis herumreiche, weil die Bedeutung der null nicht im Typsystem abgebildet wird. (Aus solchen Gründen sollte man auf null -Objektreferenzen möglichst verzichten und jedes null als einen Fehler behandeln.) Also verwendete ich stattdessen ein Maybe<Exception> als Rückgabewert, das zurzeit ValueDiagnostic heißt:
ValueDiagnostic ValidateName(string input)
{
if (input.Length > 200) return new ArgumentException("Value may not exceed 200 characters");
...
return ValueDiagnostic.None;
}
Weil die Funktion nie null zurückgibt, kann ich jetzt denselben Validator bequem im Property-Setter verwenden:
string Name
{
get { ... }
set
{
ValidateName(value).Check(); // wirft die Validierungs-Exception, falls es eine gab
...
}
}
Jedenfalls fiel mir da auf, daß es eigentlich auch ein ValueDiagnostic -Pendant geben müßte für Funktionen, die einen Rückgabewert haben. Und das ist eben die Exceptional -Monade, wie sie z.B. in Haskell verwendet wird, und die ich zur Förderung der Erkenntnis mal in C# nachgebaut hatte.
Und jetzt bin ich eben unsicher, ob ValueDiagnostic wirklich so passend ist. Eigentlich ist diese Bezeichnung zu spezifisch für meinen Einsatzzweck (Eingabewerte prüfen), und insbesondere ValueDiagnostic<T> macht keinen Sinn (wieso soll ein Diagnoseergebnis einen Typparameter haben?). Ebenso finde ich, wie oben dargelegt, Exceptional ohne Typparameter nicht intuitiv. Deshalb überlege ich nun, ob ich das Ding einfach Result nenne. Die Semantik ist jedem vertraut, der mal ein HRESULT gesehen hat, woran der Name eben erinnert; die Erweiterung auf Result<T> würde einleuchten; und nicht zuletzt gibt es schon einen Präzedenzfall mit Task und Task<T> . Ich habe nur Bedenken, weil "Result" so allgemein ist und sich oft genug nur auf den Wert bezieht, etwa in TaskCompletionSource<T>.SetResult() . Aber ResultOrException / ResultOrException<T> ist lang und häßlich
Ob man dann wirklich die kanonischen LINQ-Extension-Methoden definieren will, damit man from ... in ... select ... schreiben kann, ist eine andere Frage. Für Task / Task<T> könnte man das ja auch machen, aber in der Praxis ist es mit async / await viel natürlicher. So wie das Werfen von Exceptions viel natürlicher wäre als der Umgang mit Result / Result<T>