Ist es ein schlechter Stil, eine Bedingung redundant zu überprüfen?


10

Ich komme oft zu Stellen in meinem Code, an denen ich immer wieder eine bestimmte Bedingung überprüfe.

Ich möchte Ihnen ein kleines Beispiel geben: Angenommen, es gibt eine Textdatei, die Zeilen enthält, die mit "a" beginnen, Zeilen, die mit "b" beginnen, und andere Zeilen, und ich möchte eigentlich nur mit den ersten beiden Arten von Zeilen arbeiten. Mein Code würde ungefähr so ​​aussehen (mit Python, aber als Pseudocode lesen):

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a")):
        # do stuff
    elif (line.startsWith("b")):
        # magic
    else:
        # this else is redundant, I already made sure there is no else-case
        # by using clear_lines()
# ...

Sie können sich vorstellen, dass ich diesen Zustand hier nicht nur überprüfe, sondern vielleicht auch in anderen Funktionen und so weiter.

Betrachten Sie es als Rauschen oder fügt es meinem Code einen Wert hinzu?


5
Im Grunde geht es darum, ob Sie defensiv codieren oder nicht. Sehen Sie, dass dieser Code häufig bearbeitet wird? Ist es wahrscheinlich, dass dies Teil eines Systems sein wird, das äußerst zuverlässig sein muss? Ich sehe keinen großen Schaden darin, einen zu schieben assert(), um beim Testen zu helfen, aber darüber hinaus ist es wahrscheinlich übertrieben. Das heißt, es wird je nach Situation variieren.
Latty

Ihr "else" -Fall ist im Wesentlichen toter / nicht erreichbarer Code. Stellen Sie sicher, dass es keine systemweiten Anforderungen gibt, die dies verbieten.
NWS

@NWS: Wollen Sie damit sagen, dass ich den Fall else behalten soll? Entschuldigung, ich verstehe dich nicht ganz.
März

2
nicht speziell im Zusammenhang mit der Frage - aber ich würde diese 'Behauptung' zu einer Invariante machen -, die eine neue "Line" -Klasse (möglicherweise mit abgeleiteten Klassen für A & B) erfordern würde, anstatt die Linien als Strings zu behandeln und ihnen zu sagen, was sie repräsentieren von außen. Ich würde gerne bei CodeReview
MattDavey

meinst du elif (line.startsWith("b"))? Übrigens können Sie diese umgebenden Klammern unter den Bedingungen sicher entfernen, sie sind in Python nicht idiomatisch.
Tokand

Antworten:


14

Dies ist eine äußerst übliche Praxis, und der Umgang damit erfolgt über Filter höherer Ordnung .

Im Wesentlichen übergeben Sie der Filtermethode eine Funktion zusammen mit der Liste / Sequenz, nach der Sie filtern möchten, und die resultierende Liste / Sequenz enthält nur die gewünschten Elemente.

Ich bin mit der Python-Syntax nicht vertraut (obwohl sie eine solche Funktion enthält, wie im obigen Link gezeigt), aber in c # / f # sieht es so aus:

c #:

var linesWithAB = lines.Where(l => l.StartsWith("a") || l.StartsWith("b"));
foreach (var line in linesWithAB)
{
    /* line is guaranteed to ONLY start with a or b */
}

f # (setzt ienumerable voraus, andernfalls würde List.filter verwendet):

let linesWithAB = lines
    |> Seq.filter (fun l -> l.StartsWith("a") || l.StartsWith("b"))

for line in linesWithAB do
    /* line is guaranteed to ONLY start with a or b */

Um es klar zu sagen: Wenn Sie bewährten Code / Muster verwenden, ist dies ein schlechter Stil. Wenn Sie die Liste im Speicher so ändern, wie Sie es über clear_lines () scheinen, verlieren Sie die Thread-Sicherheit und alle Hoffnungen auf Parallelität, die Sie hätten haben können.


3
Als Hinweis wäre die Python-Syntax hierfür ein Generatorausdruck : (line for line in lines if line.startswith("a") or line.startswith("b")).
Latty

1
+1 für den Hinweis, dass die (unnötige) zwingende Umsetzung von clear_lineswirklich eine schlechte Idee ist. In Python würden Sie wahrscheinlich Generatoren verwenden, um das Laden der gesamten Datei in den Speicher zu vermeiden.
Tokand

Was passiert, wenn die Eingabedatei größer als der verfügbare Speicher ist?
Blrfl

@Blrfl: Nun, wenn der Begriff Generator zwischen c # / f # / python konsistent ist, was übersetzt @tokland und @Lattyware dann in c # / f # Ausbeute und / oder Ausbeute! Aussagen. In meinem f # -Beispiel ist dies etwas offensichtlicher, da Seq.filter nur auf Sammlungen von IEnumerable <T> angewendet werden kann, aber beide Codebeispiele funktionieren, wenn lineses sich um eine generierte Sammlung handelt.
Steven Evers

@mcwise: Wenn Sie sich alle anderen verfügbaren Funktionen ansehen, die auf diese Weise funktionieren, wird es sehr sexy und unglaublich ausdrucksstark, da sie alle miteinander verkettet und komponiert werden können. Schauen Sie sich skip, take, reduce( aggregatein .NET), map( selectin .NET), und es gibt mehr , aber das ist ein wirklich solider Start.
Steven Evers

14

Ich musste kürzlich einen Firmware-Programmierer im Motorola S-Record- Format implementieren , der dem, was Sie beschreiben, sehr ähnlich ist. Da wir etwas Zeitdruck hatten, ignorierte mein erster Entwurf Redundanzen und machte Vereinfachungen basierend auf der Teilmenge, die ich tatsächlich in meiner Anwendung verwenden musste. Es hat meine Tests problemlos bestanden, ist aber schwer gescheitert, sobald es jemand anderes versucht hat. Es gab keine Ahnung, wo das Problem lag. Es kam den ganzen Weg durch, scheiterte aber am Ende.

Ich hatte also keine andere Wahl, als alle redundanten Überprüfungen durchzuführen, um einzugrenzen, wo das Problem lag. Danach habe ich ungefähr zwei Sekunden gebraucht, um das Problem zu finden.

Ich brauchte vielleicht zwei Stunden mehr, um es richtig zu machen, verschwendete aber auch einen Tag der Zeit anderer Leute mit der Fehlerbehebung. Es ist sehr selten, dass einige Prozessorzyklen einen Tag der verschwendeten Fehlerbehebung wert sind.

Wenn Sie jedoch Dateien lesen, ist es häufig von Vorteil, Ihre Software so zu gestalten, dass sie zeilenweise gelesen und verarbeitet wird, anstatt die gesamte Datei in den Speicher einzulesen und im Speicher zu verarbeiten. Auf diese Weise funktioniert es auch bei sehr großen Dateien.


"Es ist sehr selten, dass einige Prozessorzyklen einen Tag der verschwendeten Fehlerbehebung wert sind." Vielen Dank für die Antwort, Sie haben einen guten Punkt.
März

5

Sie können für den elseFall eine Ausnahme auslösen. Auf diese Weise ist es nicht redundant. Ausnahmen sind Dinge, die nicht passieren sollen, aber trotzdem überprüft werden.

clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a)):
        # do stuff
    if (line.startsWith("b")):
        # magic
    else:
        throw BadLineException
# ...

Ich würde argumentieren, dass Letzteres eine schlechte Idee ist, da es weniger explizit ist - wenn Sie später entscheiden, ein hinzuzufügen "c", könnte es weniger klar sein.
Latty

Der erste Vorschlag hat seine Berechtigung ... der zweite (angenommen "b") ist eine schlechte Idee
Andrew

@Lattyware Ich habe die Antwort verbessert. Danke für deine Kommentare.
Tulains Córdova

1
@ Andrew Ich habe die Antwort verbessert. Danke für deine Kommentare.
Tulains Córdova

3

Bei der vertraglichen Gestaltung muss jede Funktion ihre Aufgabe wie in der Dokumentation beschrieben erfüllen. Jede Funktion verfügt also über eine Liste von Vorbedingungen, dh Bedingungen an den Eingängen der Funktion sowie Nachbedingungen, dh Bedingungen der Ausgabe der Funktion.

Die Funktion muss ihren Kunden garantieren, dass, wenn die Eingaben die Vorbedingungen erfüllen, die Ausgabe den in den Nachbedingungen beschriebenen entspricht. Wenn mindestens eine der Voraussetzungen nicht eingehalten wird, kann die Funktion tun, was sie will (Absturz, Ergebnis zurückgeben, ...). Daher sind Vor- und Nachbedingungen eine semantische Beschreibung der Funktion.

Dank des Vertrags ist eine Funktion sicher, dass ihre Clients sie korrekt verwenden, und ein Client ist sicher, dass die Funktion ihre Arbeit korrekt ausführt.

Einige Sprachen bearbeiten Verträge nativ oder über ein spezielles Framework. Für die anderen ist es am besten, die Vor- und Nachbedingungen dank Asserts zu überprüfen, wie @Lattyware sagte. Aber ich würde das nicht als defensive Programmierung bezeichnen, da sich dieses Konzept meiner Meinung nach mehr auf den Schutz vor Eingaben des (menschlichen) Benutzers konzentriert.

Wenn Sie Verträge ausnutzen, können Sie die redundant überprüfte Bedingung vermeiden, da entweder die aufgerufene Funktion einwandfrei funktioniert und Sie keine doppelte Überprüfung benötigen oder die aufgerufene Funktion nicht funktioniert und die aufrufende Funktion sich wie gewünscht verhalten kann.

Der schwierigere Teil besteht dann darin, zu definieren, welche Funktion für was verantwortlich ist, und diese Rollen streng zu dokumentieren.


1

Sie brauchen die clear_lines () am Anfang eigentlich nicht. Wenn die Zeile weder "a" noch "b" ist, werden die Bedingungen einfach nicht ausgelöst. Wenn Sie diese Zeilen entfernen möchten, machen Sie das else zu einer clear_line (). Derzeit führen Sie zwei Durchgänge durch Ihr Dokument durch. Wenn Sie clear_lines () am Anfang überspringen und dies als Teil der foreach-Schleife tun, halbieren Sie Ihre Verarbeitungszeit.

Es ist nicht nur ein schlechter Stil, es ist auch rechnerisch schlecht.


2
Es könnte sein, dass diese Zeilen für etwas anderes verwendet werden und dass sie behandelt werden müssen, bevor sie mit den "a"/ "b"Zeilen behandelt werden. Nicht zu sagen, dass es wahrscheinlich ist (der klare Name impliziert, dass sie verworfen werden), nur dass es eine Möglichkeit gibt, dass es benötigt wird. Wenn dieser Satz von Zeilen in Zukunft wiederholt wiederholt wird, kann es sich auch lohnen, sie vorher zu entfernen, um viele sinnlose Iterationen zu vermeiden.
Latty

0

Wenn Sie tatsächlich etwas tun möchten, wenn Sie eine ungültige Zeichenfolge finden (z. B. Debug-Text ausgeben), würde ich sagen, dass dies absolut in Ordnung ist. Ein paar zusätzliche Zeilen und ein paar Monate später, wenn es aus einem unbekannten Grund nicht mehr funktioniert, können Sie sich die Ausgabe ansehen, um herauszufinden, warum.

Wenn es jedoch sicher ist, es einfach zu ignorieren, oder Sie sicher sind, dass Sie niemals eine ungültige Zeichenfolge erhalten, ist der zusätzliche Zweig nicht erforderlich.

Persönlich bin ich immer dafür, mindestens eine Trace-Ausgabe für unerwartete Zustände einzugeben - es macht das Leben viel einfacher, wenn Sie einen Fehler mit angehängter Ausgabe haben, der Ihnen genau sagt, was schief gelaufen ist.


0

... Angenommen, es gibt eine Textdatei, die Zeilen enthält, die mit "a" beginnen, Zeilen, die mit "b" beginnen, und andere Zeilen, und ich möchte eigentlich nur mit den ersten beiden Arten von Zeilen arbeiten. Mein Code würde ungefähr so ​​aussehen (mit Python, aber als Pseudocode lesen):

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if ...

Ich hasse if...then...elseKonstruktionen. Ich würde das ganze Problem vermeiden:

process_lines_by_first_character (lines,  
                                  'a' => { |line| ... a code ... },
                                  'b' => { |line| ... b code ... } )
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.