Die folgenden Codebeispiele bieten Kontext zu meiner Frage.
Die Raumklasse wird mit einem Delegaten initialisiert. In der ersten Implementierung der Room-Klasse gibt es keine Schutzmaßnahmen gegen Delegierte, die Ausnahmen auslösen. Solche Ausnahmen sprudeln in die Eigenschaft North, in der der Delegat ausgewertet wird (Hinweis: Die Methode Main () zeigt, wie eine Room-Instanz im Clientcode verwendet wird):
public sealed class Room
{
private readonly Func<Room> north;
public Room(Func<Room> north)
{
this.north = north;
}
public Room North
{
get
{
return this.north();
}
}
public static void Main(string[] args)
{
Func<Room> evilDelegate = () => { throw new Exception(); };
var kitchen = new Room(north: evilDelegate);
var room = kitchen.North; //<----this will throw
}
}
Da ich lieber bei der Objekterstellung als beim Lesen der Eigenschaft North versagen möchte, ändere ich den Konstruktor in private und führe eine statische Factory-Methode mit dem Namen Create () ein. Diese Methode fängt die vom Delegaten ausgelöste Ausnahme ab und löst eine Wrapper-Ausnahme mit einer aussagekräftigen Ausnahmemeldung aus:
public sealed class Room
{
private readonly Func<Room> north;
private Room(Func<Room> north)
{
this.north = north;
}
public Room North
{
get
{
return this.north();
}
}
public static Room Create(Func<Room> north)
{
try
{
north?.Invoke();
}
catch (Exception e)
{
throw new Exception(
message: "Initialized with an evil delegate!", innerException: e);
}
return new Room(north);
}
public static void Main(string[] args)
{
Func<Room> evilDelegate = () => { throw new Exception(); };
var kitchen = Room.Create(north: evilDelegate); //<----this will throw
var room = kitchen.North;
}
}
Verunreinigt der Try-Catch-Block die Create () -Methode?
quelle
Create
ist, ist er auch unrein, weil er sie aufruft.Create
Funktion schützt Sie nicht vor einer Ausnahme beim Abrufen der Eigenschaft. Wenn Ihr Delegierter wirft, ist es im wirklichen Leben sehr wahrscheinlich, dass er nur unter bestimmten Bedingungen wirft. Es besteht die Möglichkeit, dass die Bedingungen für das Werfen während des Baus nicht gegeben sind, aber sie sind vorhanden, wenn das Eigentum erhalten wird.Antworten:
Ja. Das ist praktisch eine unreine Funktion. Es entsteht ein Nebeneffekt: Die Programmausführung wird an einer anderen Stelle fortgesetzt als an der Stelle, an die die Funktion voraussichtlich zurückkehren wird.
Um es zu einer reinen Funktion zu machen,
return
ein tatsächliches Objekt, das den erwarteten Wert der Funktion und einen Wert, der einen möglichen Fehlerzustand anzeigt, wie einMaybe
Objekt oder ein Objekt der Arbeitseinheit , kapselt .quelle
Na ja ... und nein.
Eine reine Funktion muss über referenzielle Transparenz verfügen. Das heißt, Sie sollten in der Lage sein, jeden möglichen Aufruf einer reinen Funktion durch den zurückgegebenen Wert zu ersetzen, ohne das Verhalten des Programms zu ändern. * Es ist garantiert, dass Ihre Funktion immer nach bestimmten Argumenten wirft. Daher gibt es keinen Rückgabewert, durch den der Funktionsaufruf ersetzt werden kann. Lassen Sie uns ihn stattdessen ignorieren . Betrachten Sie diesen Code:
Wenn
func
es rein ist, könnte ein Optimierer entscheiden, dassfunc(arg)
es durch sein Ergebnis ersetzt werden könnte. Es weiß noch nicht, was das Ergebnis ist, aber es kann feststellen, dass es nicht verwendet wird - also kann es einfach ableiten, dass diese Aussage keine Wirkung hat und sie entfernen.Aber wenn
func(arg)
zufällig geworfen wird, bewirkt diese Anweisung etwas - sie löst eine Ausnahme aus! Der Optimierer kann es also nicht entfernen - es spielt keine Rolle, ob die Funktion aufgerufen wird oder nicht.Aber...
In der Praxis spielt dies nur eine sehr geringe Rolle. Eine Ausnahme - zumindest in C # - ist etwas Außergewöhnliches. Sie sollten es nicht als Teil Ihres regulären Kontrollflusses verwenden - Sie sollten versuchen, es abzufangen, und wenn Sie etwas abfangen, können Sie den Fehler beheben, um das, was Sie getan haben, entweder rückgängig zu machen oder es irgendwie noch zu erreichen. Wenn Ihr Programm nicht richtig funktioniert, weil ein Code, der fehlgeschlagen wäre, wegoptimiert wurde, verwenden Sie Ausnahmen falsch (es sei denn, es handelt sich um Testcode, und wenn Sie für Tests Ausnahmen erstellen, sollten Sie nicht optimieren).
Davon abgesehen ...
Wirf keine Ausnahmen von reinen Funktionen mit der Absicht, dass sie abgefangen werden - es gibt einen guten Grund, warum funktionale Sprachen lieber Monaden als Stack-Unwinding-Ausnahmen verwenden.
Wenn C # eine
Error
Klasse wie Java (und viele andere Sprachen) hätte, hätte ich vorgeschlagen, eineError
anstelle einer zu werfenException
. Es zeigt an, dass der Benutzer der Funktion etwas falsch gemacht hat (eine Funktion übergeben hat, die ausgelöst wird), und solche Dinge sind in reinen Funktionen zulässig. C # hat jedoch keineError
Klasse, und die Ausnahmebedingungen für Verwendungsfehler scheinen sich daraus abzuleitenException
. Mein Vorschlag ist, ein zu werfenArgumentException
, um zu verdeutlichen, dass die Funktion mit einem falschen Argument aufgerufen wurde.* Rechnerisch gesehen. Eine Fibonacci-Funktion, die mit einer naiven Rekursion implementiert wird, wird für große Zahlen viel Zeit in Anspruch nehmen und möglicherweise die Ressourcen der Maschine erschöpfen, da diese bei unbegrenzter Zeit und unbegrenztem Speicher immer den gleichen Wert zurückgeben und keine Nebenwirkungen haben (außer der Zuweisung) Erinnerung und Veränderung dieser Erinnerung) - es wird immer noch als rein betrachtet.
quelle
(value, exception) = func(arg)
und die Möglichkeit des Auslösens einer Ausnahme entfernen (in einigen Sprachen und für einige Arten von Ausnahmen müssen Sie sie tatsächlich mit der Definition auflisten, genauso wie Argumente oder Rückgabewerte). Jetzt wäre es genauso rein wie zuvor (vorausgesetzt, es gibt immer eine Ausnahme aus, wenn das gleiche Argument vorliegt). Das Auslösen einer Ausnahme ist keine Nebenwirkung, wenn Sie diese Interpretation verwenden.func
der Block, den ich gegeben habe, optimiert werden können, da anstelle des Abwickelns des Stapels die geworfene Ausnahme codiert worden wäreignoreThis
, die wir ignorieren. Im Gegensatz zu Ausnahmen, die den Stapel auflösen, verstößt eine im Rückgabetyp codierte Ausnahme nicht gegen die Reinheit - und aus diesem Grund bevorzugen es viele funktionale Sprachen, sie zu verwenden.func(arg)
würde das Tupel zurückgeben(<junk>, TheException())
undignoreThis
wird das Tupel auch enthalten(<junk>, TheException())
, aber da wirignoreThis
die Ausnahme nie verwenden , hat dies keine Auswirkung auf irgendetwas.Eine Überlegung ist, dass der Try-Catch-Block nicht das Problem ist. (Gestützt auf meine Kommentare zu der Frage oben).
Das Hauptproblem besteht darin, dass die North-Eigenschaft ein E / A-Aufruf ist .
Zu diesem Zeitpunkt in der Codeausführung muss das Programm die vom Clientcode bereitgestellten E / A überprüfen. (Es ist nicht relevant, dass der Input in Form eines Delegierten vorliegt oder dass der Input nominell bereits übergeben wurde.)
Sobald Sie die Kontrolle über die Eingabe verlieren, können Sie nicht mehr sicherstellen, dass die Funktion rein ist. (Besonders wenn die Funktion werfen kann).
Mir ist nicht klar, warum Sie den Anruf für Move [Check] Room nicht überprüfen möchten? Nach meinem Kommentar zur Frage:
Wie Bart van Ingen Schenau oben sagte,
Im Allgemeinen verzögert jede Art von verzögertem Laden implizit die Fehler bis zu diesem Zeitpunkt.
Ich würde vorschlagen, eine Move [Check] Room-Methode zu verwenden. Auf diese Weise können Sie die unreinen E / A-Aspekte an einer Stelle trennen.
Ähnlich wie bei Robert Harvey :
Es wäre Sache des Codeschreibers, zu bestimmen, wie mit der (möglichen) Ausnahme von der Eingabe umgegangen werden soll. Dann kann die Methode ein Room-Objekt oder ein Null-Room-Objekt zurückgeben oder möglicherweise die Ausnahme ausblasen.
Auf diesen Punkt kommt es an:
quelle
Hmm ... ich habe mich nicht richtig gefühlt. Ich denke, Sie wollen sicherstellen, dass andere Menschen ihre Arbeit richtig machen, ohne zu wissen, was sie tun.
Da die Funktion vom Verbraucher übergeben wird, könnte dies von Ihnen oder von einer anderen Person geschrieben werden.
Was passiert, wenn die Funktionsübergabe zum Zeitpunkt der Erstellung des Raumobjekts nicht gültig ist?
Aus dem Code konnte ich nicht sicher sein, was der Norden tut.
Sagen wir, wenn die Funktion darin besteht, das Zimmer zu buchen, und es die Zeit und den Zeitraum der Buchung benötigt, dann würden Sie eine Ausnahme bekommen, wenn Sie die Informationen nicht bereit haben.
Was ist, wenn es sich um eine Langzeitfunktion handelt? Sie möchten Ihr Programm nicht blockieren, während Sie ein Raumobjekt erstellen. Was ist, wenn Sie 1000 Raumobjekte erstellen müssen?
Wenn Sie die Person wären, die den Consumer schreibt, der diese Klasse konsumiert, würden Sie sicherstellen, dass die übergebene Funktion richtig geschrieben ist, nicht wahr?
Wenn es eine andere Person gibt, die den Konsumenten schreibt, kennt er / sie möglicherweise nicht die "spezielle" Bedingung zum Erstellen eines Raumobjekts, die zur Ausführung führen kann, und kratzt sich am Kopf, um herauszufinden, warum.
Eine andere Sache ist, dass es nicht immer gut ist, eine neue Ausnahme zu machen. Grund dafür ist, dass es nicht die Informationen der ursprünglichen Ausnahme enthält. Sie wissen, dass Sie einen Fehler erhalten (eine freundliche Meldung für eine allgemeine Ausnahme), aber Sie würden nicht wissen, was der Fehler ist (Nullreferenz, Index außerhalb der Grenzen, Teilung durch Null usw.). Diese Informationen sind sehr wichtig, wenn wir Nachforschungen anstellen müssen.
Sie sollten die Ausnahme nur dann behandeln, wenn Sie wissen, was und wie Sie damit umgehen sollen.
Ich denke, Ihr ursprünglicher Code ist gut genug. Das Einzige, was Sie tun können, ist, dass Sie die Ausnahme auf der "Hauptseite" behandeln möchten (oder auf einer beliebigen Ebene, die weiß, wie man damit umgeht).
quelle
Ich werde mit den anderen Antworten nicht einverstanden sein und Nein sagen . Ausnahmen führen nicht dazu, dass eine ansonsten reine Funktion unrein wird.
Jede Verwendung von Ausnahmen könnte umgeschrieben werden, um explizite Überprüfungen auf Fehlerergebnisse durchzuführen. Darüber hinaus könnten Ausnahmen als syntaktischer Zucker betrachtet werden. Praktischer syntaktischer Zucker macht reinen Code nicht unrein.
quelle
f
undg
sind beide reine Funktionen, dannf(x) + g(x)
sollte das gleiche Ergebnis unabhängig von der Reihenfolge, die Sie auswertenf(x)
undg(x)
. Aber wennf(x)
wirftF
undg(x)
wirftG
, dann wäre die Ausnahme, die von diesem Ausdruck geworfen wird,F
wennf(x)
zuerst ausgewertet wird undG
wenng(x)
zuerst ausgewertet wird - so sollten sich reine Funktionen nicht verhalten! Wenn die Ausnahme im Rückgabetyp codiert ist, würde das Ergebnis ungefähr so aussehenError(F) + Error(G)
unabhängig von der Auswertungsreihenfolge angezeigt.