Ich frage mich, ob es gegen diesen Stil Vor- und Nachteile gibt:
private void LoadMaterial(string name)
{
if (_Materials.ContainsKey(name))
{
throw new ArgumentException("The material named " + name + " has already been loaded.");
}
_Materials.Add(
name,
Resources.Load(string.Format("Materials/{0}", name)) as Material
);
}
Diese Methode sollte jeweils name
nur einmal ausgeführt werden. _Materials.Add()
wird eine Ausnahme auslösen, wenn es mehrmals für das gleiche aufgerufen wird name
. Ist meine Wache daher völlig überflüssig, oder gibt es einige weniger offensichtliche Vorteile?
Das ist C #, Unity, wenn sich jemand dafür interessiert.
exceptions
asynchron
quelle
quelle
_Materials.Add
eine Ausnahme?string.Format
Über-String-Verkettung zum Erstellen der Ausnahmemeldung. 2) Verwendenas
Sie diese Option nur, wenn Sie davon ausgehen, dass der Cast fehlschlägt, und überprüfen Sie das Ergebnis aufnull
. Wenn Sie immer eine erwartenMaterial
, verwenden Sie eine Besetzung wie(Material)Resources.Load(...)
. Eine Clear Cast-Ausnahme ist einfacher zu debuggen als eine Nullreferenz-Ausnahme, die später auftritt.LoadMaterialIfNotLoaded
oder eineReloadMaterial
(dieser Name könnte eine Verbesserung gebrauchen) Methode nützlich.Antworten:
Der Vorteil ist, dass Ihre "benutzerdefinierte" Ausnahme eine Fehlermeldung enthält, die für jeden, der diese Funktion aufruft, von Bedeutung ist, ohne zu wissen, wie sie implementiert ist (welche in Zukunft Sie sein könnten!).
Zugegeben, in diesem Fall könnten sie wahrscheinlich erraten, was die "Standard" -Ausnahme bedeutete, aber Sie machen trotzdem deutlich, dass sie gegen Ihren Vertrag verstoßen haben, anstatt auf einen seltsamen Fehler in Ihrem Code gestoßen zu sein.
quelle
_Materials.Add
nicht um den Fehler handelt, den Sie dem Anrufer übermitteln möchten, halte ich diese Methode zum erneuten Kennzeichnen des Fehlers für ineffizient. Für jeden erfolgreichen Anruf vonLoadMaterial
(normaler Betrieb) haben Sie den gleichen Gesundheitstest zweimal inLoadMaterial
und dann wieder in durchgeführt_Materials.Add
. Wenn mehrere Ebenen mit demselben Stil umhüllt werden, können Sie sogar den gleichen Test um ein Vielfaches wiederholen._Materials.Add
bedingungslose Eintauchen , fangen Sie einen möglichen Fehler ab und werfen Sie einen anderen in den Handler. Die zusätzliche Arbeit wird jetzt nur in den fehlerhaften Fällen erledigt, dem außergewöhnlichen Ausführungspfad, in dem Sie sich überhaupt nicht um die Effizienz kümmern.Ich stimme der Antwort von Ixrec zu . Möglicherweise möchten Sie jedoch eine dritte Alternative in Betracht ziehen: die Funktion idempotent machen. Mit anderen Worten, kehren Sie früh zurück, anstatt eine zu werfen
ArgumentException
. Dies ist häufig vorzuziehen, wenn Sie andernfalls vorLoadMaterial
jedem Anruf überprüfen müssten, ob es bereits geladen wurde. Je weniger Voraussetzungen Sie haben, desto geringer ist die kognitive Belastung des Programmiergeräts.Das Auslösen einer Ausnahme ist vorzuziehen, wenn es sich wirklich um einen Programmiererfehler handelt, bei dem es zur Kompilierungszeit offensichtlich und bekannt sein sollte, ob das Material bereits geladen wurde, ohne dass dies vor dem Aufruf zur Laufzeit überprüft werden muss.
quelle
LoadMaterial
folgende Einschränkungen: "Nach dem Aufruf wird das Material immer geladen und die Anzahl der geladenen Materialien hat sich nicht verringert." Eine Ausnahme für eine dritte Einschränkung auszulösen "Das Material kann nicht zweimal hinzugefügt werden" erscheint mir albern und nicht intuitiv. Indem Sie die Funktion idempotent machen, wird die Abhängigkeit des Codes von der Ausführungsreihenfolge und dem Anwendungsstatus verringert. Es scheint ein doppelter Gewinn für mich.Die grundlegende Frage, die Sie sich stellen müssen, lautet: Wie soll die Schnittstelle für Ihre Funktion aussehen? Ist der Zustand insbesondere
_Materials.ContainsKey(name)
eine Voraussetzung für die Funktion?Wenn dies keine Voraussetzung ist, muss die Funktion ein genau definiertes Ergebnis für alle möglichen Werte von liefern
name
. In diesem Fall wird die Ausnahme, die ausgelöst wird, wenn siename
nicht Teil von ist,_Materials
Teil der Schnittstelle der Funktion. Das bedeutet, dass es Teil der Schnittstellendokumentation sein muss. Wenn Sie sich jemals dazu entschließen, diese Ausnahme in Zukunft zu ändern, ist dies eine brechende Schnittstellenänderung .Die interessantere Frage ist, was passiert, wenn dies eine Voraussetzung ist. In diesem Fall wird die Vorbedingung selbst Teil der Funktionsschnittstelle, aber die Art und Weise, wie sich eine Funktion verhält, wenn diese Bedingung verletzt wird, ist nicht unbedingt Teil der Schnittstelle.
In diesem Fall handelt es sich bei dem von Ihnen veröffentlichten Ansatz, nach Verstößen gegen die Voraussetzungen zu suchen und den Fehler zu melden, um eine sogenannte defensive Programmierung . Defensive Programmierung ist insofern von Vorteil, als sie den Benutzer frühzeitig benachrichtigt, wenn er einen Fehler gemacht und die Funktion mit einem falschen Argument aufgerufen hat. Das ist insofern schlimm, als es den Wartungsaufwand erheblich erhöht, da der Benutzercode davon abhängen kann, dass die Funktion auf bestimmte Weise mit Verstößen gegen die Voraussetzungen umgeht. Insbesondere wenn Sie feststellen, dass die Laufzeitüberprüfung in Zukunft jemals zu einem Leistungsengpass wird (unwahrscheinlich für einen so einfachen Fall, aber für komplexere Voraussetzungen durchaus üblich), können Sie sie möglicherweise nicht mehr entfernen.
Es stellt sich heraus, dass diese Nachteile sehr bedeutend sein können, was der defensiven Programmierung in bestimmten Kreisen einen schlechten Ruf verlieh. Das ursprüngliche Ziel ist jedoch weiterhin gültig: Wir möchten, dass Benutzer unserer Funktion frühzeitig bemerken, wenn sie einen Fehler gemacht haben.
Daher schlagen heutzutage viele Entwickler einen etwas anderen Ansatz für diese Art von Problemen vor. Anstatt eine Ausnahme auszulösen, verwenden sie einen assert-ähnlichen Mechanismus zum Überprüfen der Voraussetzungen. Das heißt, in Debugbuilds werden möglicherweise Vorbedingungen überprüft, um Benutzern das frühzeitige Erkennen von Fehlern zu erleichtern, sie sind jedoch nicht Teil der Funktionsschnittstelle. Der Unterschied mag auf den ersten Blick subtil erscheinen, kann aber in der Praxis einen großen Unterschied ausmachen.
Technisch gesehen ist der Aufruf einer Funktion mit verletzten Voraussetzungen undefiniertes Verhalten. Die Implementierung kann jedoch entscheiden, diese Fälle zu erkennen und den Benutzer in diesem Fall unverzüglich zu benachrichtigen. Ausnahmen sind leider kein gutes Werkzeug, um dies zu implementieren, da der Benutzercode auf sie reagieren und sich daher möglicherweise auf ihre Anwesenheit verlassen kann.
Eine ausführliche Erläuterung der Probleme mit dem klassischen defensiven Ansatz sowie eine mögliche Implementierung für Voraussetzungsprüfungen im Assert -Stil finden Sie in John Lakos Vortrag Defensive Programming Done Right from CppCon 2014 ( Folien , Video ).
quelle
Hier gibt es bereits einige Antworten, aber hier ist eine Antwort, die Unity3D berücksichtigt (die Antwort ist sehr spezifisch für Unity3D, ich würde das meiste in den meisten Zusammenhängen ganz anders machen):
Im Allgemeinen verwendet Unity3D traditionell keine Ausnahmen. Wenn Sie in Unity3D eine Ausnahme auslösen, ist dies nicht vergleichbar mit einer durchschnittlichen .NET-Anwendung. Das Programm wird nicht gestoppt. Sie können den Editor jedoch so konfigurieren, dass er pausiert. Es wird nur protokolliert. Dies kann das Spiel leicht in einen ungültigen Zustand versetzen und einen Kaskadeneffekt erzeugen, der es schwierig macht, Fehler aufzuspüren. Daher würde ich im Falle von Unity sagen, dass
Add
es besonders unerwünscht ist, eine Ausnahme auslösen zu lassen.Bei der Prüfung der Ausnahmegeschwindigkeit kommt es jedoch in einigen Fällen nicht zu einer vorzeitigen Optimierung, da Mono auf einigen Plattformen in Unity funktioniert. Tatsächlich unterstützt Unity3D unter iOS einige erweiterte Skriptoptimierungen, und deaktivierte Ausnahmen * sind ein Nebeneffekt einer davon. Dies ist wirklich zu berücksichtigen, da sich diese Optimierungen für viele Benutzer als sehr wertvoll erwiesen haben und einen realistischen Fall für die Einschränkung der Verwendung von Ausnahmen in Unity3D darstellen. (* verwaltete Ausnahmen von der Engine, nicht Ihr Code)
Ich würde sagen, dass Sie in Unity einen spezielleren Ansatz wählen möchten. Ironischerweise zeigt eine sehr heruntergestimmte Antwort zum Zeitpunkt des Schreibens dieses Artikels , wie ich so etwas speziell im Kontext von Unity3D implementieren könnte (ansonsten ist so etwas wirklich inakzeptabel und selbst in Unity ist es ziemlich inelegant).
Ein anderer Ansatz, den ich in Betracht ziehen würde, besteht darin, dem Anrufer keinen Fehler anzuzeigen, sondern die
Debug.LogXX
Funktionen zu verwenden. Auf diese Weise erhalten Sie dasselbe Verhalten wie beim Auslösen einer nicht behandelten Ausnahme (aufgrund der Art und Weise, wie Unity3D sie behandelt), ohne das Risiko einzugehen, dass etwas auf der ganzen Linie in einen seltsamen Zustand versetzt wird. Überlegen Sie auch, ob dies wirklich ein Fehler ist (Ist der Versuch, dasselbe Material zweimal einzulegen, in Ihrem Fall unbedingt ein Fehler? Oder ist dies möglicherweise ein Fall, in dem diesDebug.LogWarning
zutreffender ist).Und in Bezug auf die Verwendung von
Debug.LogXX
Funktionen anstelle von Ausnahmen müssen Sie immer noch überlegen, was passiert, wenn eine Ausnahme von etwas ausgelöst wird, das einen Wert zurückgibt (wie z. B. GetMaterial). Ich tendiere dazu, dies zu erreichen, indem ich Null zusammen mit der Protokollierung des Fehlers übergebe ( wieder nur in Unity ). Ich verwende dann Nullprüfungen in meinen MonoBehaviors, um sicherzustellen, dass eine Abhängigkeit wie ein Material kein Nullwert ist, und deaktiviere das MonoBehavior, falls dies der Fall ist. Ein Beispiel für ein einfaches Verhalten, für das einige Abhängigkeiten erforderlich sind, ist etwa Folgendes:SceneData.GetValue<>
ähnelt Ihrem Beispiel darin, dass es eine Funktion in einem Wörterbuch aufruft, die eine Ausnahme auslöst. Anstatt jedoch eine Ausnahme auszulösenDebug.LogError
, wird eine Stapelablaufverfolgung wie bei einer normalen Ausnahme erstellt und null zurückgegeben. Die folgenden Überprüfungen * deaktivieren das Verhalten, anstatt zuzulassen, dass es weiterhin in einem ungültigen Zustand existiert.* Die Schecks sehen so aus, weil ich einen kleinen Helfer verwende, der eine formatierte Nachricht druckt, wenn er das Spielobjekt deaktiviert **. Einfache Null-Checks mit
if
Arbeit hier (** Die Checks des Helfers werden nur in Debug-Builds kompiliert (wie bei Asserts). Die Verwendung von Lambdas und solchen Ausdrücken in Unity kann die Leistung beeinträchtigen.)quelle
Ich mag die beiden Hauptantworten, möchte aber vorschlagen, dass Ihre Funktionsnamen verbessert werden könnten. Ich bin an Java gewöhnt, also YMMV, aber eine "Add" -Methode sollte, IMO, keine Ausnahme auslösen, wenn das Element bereits vorhanden ist. Es sollte das Element erneut hinzufügen oder nichts tun, wenn das Ziel ein Set ist. Da dies nicht das Verhalten von Materials.Add ist, sollte das in TryPut oder AddOnce oder AddOrThrow oder ähnliches umbenannt werden.
Ebenso sollte Ihr LoadMaterial in LoadIfAbsent oder Put oder TryLoad oder LoadOrThrow umbenannt werden (abhängig davon, ob Sie die Antwort 1 oder 2 verwenden) oder in eine solche.
Befolgen Sie die Namenskonventionen von C # Unity.
Dies ist besonders nützlich, wenn Sie über andere AddFoo- und LoadBar-Funktionen verfügen, bei denen es zulässig ist, dasselbe Objekt zweimal zu laden. Ohne eindeutige Namen werden die Entwickler frustriert sein.
quelle
Alle Antworten enthalten wertvolle Ideen, die ich gerne kombinieren möchte:
Legen Sie die beabsichtigte und erwartete Semantik der
LoadMaterial()
Operation fest. Zumindest die folgenden Optionen existieren:Voraussetzung für
name
∉ GeladeneMaterialien : →Wenn die Vorbedingung verletzt wird, ist die Wirkung von
LoadMaterial()
nicht spezifiziert (wie in der Antwort von ComicSansMS ). Dies ermöglicht größtmögliche Freiheit bei der Implementierung und zukünftigen Änderungen vonLoadMaterial()
. Oder,Effekte des Aufrufs
LoadMaterial(name)
mitname
∈ LoadedMaterials werden angegeben; entweder:die Spezifikation besagt, dass das Ergebnis idempotent ist (wie in der Antwort von Karl Bielefeldt ),
Wenn Sie sich für die Semantik entschieden haben, müssen Sie eine Implementierung auswählen. Folgende Optionen und Überlegungen wurden vorgeschlagen:
Werfen Sie eine benutzerdefinierte Ausnahme (wie vorgeschlagen durch Ixrec ) →
Um die Kosten einer wiederholten Überprüfung von
name
∉ LoadedMaterials zu vermeiden, können Sie den Anweisungen von Marc van Leeuwen folgen :Lassen Sie Dictionay.Add die Ausnahme auslösen →
Obwohl die meisten Wähler eher mit Ixrec übereinstimmen
Ein weiterer Grund, sich für diese Implementierung zu entscheiden, sind:
ArgumentException
Ausnahmen behandeln können, undWenn diese beiden Gründe wichtig sind, können Sie Ihre benutzerdefinierte Ausnahme auch aus
ArgumentException
dem Original ableiten und als verkettete Ausnahme verwenden.Machen
LoadMaterial()
Idempotent als anwser von Karl Bielefeldt und upvoted am häufigsten (75 Mal).Die Implementierungsoptionen für dieses Verhalten:
Überprüfen Sie mit Dictionary.ContainsKey ()
Rufen Sie immer Dictionary.Add () auf, um den
ArgumentException
Auslöser abzufangen, wenn der einzufügende Schlüssel bereits vorhanden ist, und ignorieren Sie diese Ausnahme. Dokumentieren Sie, dass Sie beabsichtigen, die Ausnahme zu ignorieren und warum.LoadMaterials()
(fast) immer einmal für jedes aufgerufen wirdname
, vermeidet dies die wiederholte Überprüfung der Kostenname
∉ LoadedMaterials vgl. Marc van Leeuwen . Jedoch,LoadedMaterials()
oft mehrere Male für dasselbe aufgerufen wirdname
, entstehen die (teuren) Kosten für dasArgumentException
Abwickeln des Stapels.Ich dachte, dass es eine
TryAdd
zu TryGet () analoge Methode gibt, die es Ihnen ermöglicht hätte, das teure Auslösen von Ausnahmen und das Abwickeln von Stapeln fehlgeschlagener Aufrufe von Dictionary.Add zu vermeiden.Aber diese
TryAdd
Methode scheint nicht zu existieren.quelle
Der Code zum Auslösen von Ausnahmen ist redundant.
Wenn Sie anrufen:
mit dem gleichen Schlüssel wirft es a
Die Meldung lautet: "Ein Objekt mit demselben Schlüssel wurde bereits hinzugefügt."
Die
ContainsKey
Prüfung ist redundant, da ohne sie im Wesentlichen das gleiche Verhalten erreicht wird. Das einzige Element, das sich unterscheidet, ist die tatsächliche Nachricht in der Ausnahme. Wenn eine benutzerdefinierte Fehlermeldung einen echten Vorteil bietet, hat der Guard-Code einige Vorteile.Andernfalls würde ich in diesem Fall die Wache wahrscheinlich umgestalten.
quelle
Ich denke, dies ist eher eine Frage zum API-Design als eine Frage zur Kodierungskonvention.
Was ist das erwartete Ergebnis (der Vertrag) des Anrufs:
Wenn der Aufrufer erwarten kann / kann, dass nach dem Aufrufen dieser Methode das Material "Holz" geladen wird, sehe ich keinen Grund, eine Ausnahme auszulösen, wenn dieses Material bereits geladen ist.
Wenn beim Laden des Materials ein Fehler auftritt, z. B. die Datenbankverbindung nicht geöffnet werden konnte oder sich kein Material "Holz" im Repository befindet, ist es richtig, eine Ausnahme auszulösen, um den Anrufer über dieses Problem zu benachrichtigen.
quelle
Warum nicht die Methode so ändern, dass sie "wahr" zurückgibt, wenn das Materielle hinzugefügt wird, und "falsch", wenn es nicht so ist?
quelle