Benannte Parameter erleichtern das Lesen und erschweren das Schreiben von Code
Wenn ich einen Code lese, können benannte Parameter einen Kontext einführen, der das Verständnis des Codes erleichtert. Betrachten wir zum Beispiel diesen Konstruktor: Color(1, 102, 205, 170)
. Was um alles in der Welt bedeutet das? In der Tat Color(alpha: 1, red: 102, green: 205, blue: 170)
wäre es viel einfacher zu lesen. Aber leider sagt der Compiler "nein" - es will Color(a: 1, r: 102, g: 205, b: 170)
. Wenn Sie Code mit benannten Parametern schreiben, verbringen Sie unnötig viel Zeit damit, nach den genauen Namen zu suchen. Es ist einfacher, die genauen Namen einiger Parameter zu vergessen, als ihre Reihenfolge zu vergessen.
Das hat mich einmal gebissen, als ich eine DateTime
API verwendete, die zwei Geschwisterklassen für Punkte und Dauer mit fast identischen Schnittstellen hatte. Während DateTime->new(...)
akzeptiert ein second => 30
Argument, das DateTime::Duration->new(...)
wollte seconds => 30
, und ähnliches für andere Einheiten. Ja, es ist absolut sinnvoll, aber das hat mir gezeigt, dass die genannten Parameter einfach zu bedienen sind.
Schlechte Namen machen es nicht einmal einfacher zu lesen
Ein anderes Beispiel, wie benannte Parameter schlecht sein können, ist wahrscheinlich die R- Sprache. Dieser Code erstellt einen Datenplot:
plot(plotdata$n, plotdata$mu, type="p", pch=17, lty=1, bty="n", ann=FALSE, axes=FALSE)
Sie sehen zwei Positionsargumente für die x- und y -Datenzeilen und dann eine Liste benannter Parameter. Es gibt viel mehr Optionen mit Standardeinstellungen, und es werden nur die aufgeführt, deren Standardeinstellungen ich ändern oder explizit angeben wollte. Wenn wir einmal ignorieren, dass dieser Code magische Zahlen verwendet und von der Verwendung von Aufzählungen (falls R welche hat!) Profitieren könnte, ist das Problem, dass viele dieser Parameternamen ziemlich unkenntlich sind.
pch
ist das Plotzeichen, das für jeden Datenpunkt gezeichnet wird. 17
ist ein leerer Kreis oder so ähnlich.
lty
ist der Leitungstyp. Hier 1
ist eine durchgezogene Linie.
bty
ist der Boxtyp. Durch das Festlegen von wird "n"
vermieden, dass ein Rahmen um das Diagramm gezogen wird.
ann
Steuert das Erscheinungsbild von Achsenanmerkungen.
Für jemanden, der nicht weiß, was jede Abkürzung bedeutet, sind diese Optionen eher verwirrend. Dies zeigt auch, warum R diese Bezeichnungen verwendet: Nicht als selbstdokumentierender Code, sondern (als dynamisch typisierte Sprache) als Schlüssel, um die Werte ihren korrekten Variablen zuzuordnen.
Eigenschaften von Parametern und Signaturen
Funktionssignaturen können die folgenden Eigenschaften haben:
- Argumente können geordnet oder ungeordnet sein,
- benannt oder unbenannt,
- erforderlich oder optional.
- Signaturen können auch nach Größe oder Typ überladen werden.
- und kann eine nicht spezifizierte Größe mit varargs haben.
Verschiedene Sprachen landen auf verschiedenen Koordinaten dieses Systems. In C sind Argumente geordnet, unbenannt, immer erforderlich und können varargs sein. In Java ist die Situation ähnlich, außer dass Signaturen überladen werden können. In Objective C werden Signaturen sortiert, benannt und benötigt und können nicht überladen werden, da es sich nur um syntaktischen Zucker um C handelt.
Dynamisch typisierte Sprachen mit varargs (Befehlszeilenschnittstellen, Perl,…) können optionale benannte Parameter emulieren. Sprachen mit Überladung der Signaturgröße haben so etwas wie optionale Positionsparameter.
So implementieren Sie keine benannten Parameter
Wenn wir an benannte Parameter denken, nehmen wir normalerweise benannte, optionale, ungeordnete Parameter an. Die Implementierung ist schwierig.
Optionale Parameter können Standardwerte haben. Diese müssen von der aufgerufenen Funktion angegeben werden und sollten nicht in den aufrufenden Code übersetzt werden. Andernfalls können die Standardeinstellungen nicht aktualisiert werden, ohne den gesamten abhängigen Code neu zu kompilieren.
Eine wichtige Frage ist nun, wie die Argumente tatsächlich an die Funktion übergeben werden. Mit geordneten Parametern können die Argumente in einem Register oder in ihrer inhärenten Reihenfolge auf dem Stapel übergeben werden. Wenn wir die Register für einen Moment ausschließen, besteht das Problem darin, wie ungeordnete optionale Argumente auf den Stapel gelegt werden.
Dafür benötigen wir eine gewisse Reihenfolge der optionalen Argumente. Was passiert, wenn der Deklarationscode geändert wird? Da die Reihenfolge irrelevant ist, sollte eine Neuordnung in der Funktionsdeklaration die Position der Werte auf dem Stapel nicht verändern. Wir sollten auch prüfen, ob das Hinzufügen eines neuen optionalen Parameters möglich ist. Aus Benutzersicht scheint dies so zu sein, da Code, der diesen Parameter zuvor nicht verwendet hat, weiterhin mit dem neuen Parameter funktionieren sollte. Ausgenommen hiervon sind also Bestellungen, bei denen beispielsweise die Reihenfolge in der Erklärung oder die alphabetische Reihenfolge verwendet wird.
Berücksichtigen Sie dies auch im Hinblick auf die Untertypisierung und das Liskov-Substitutionsprinzip. In der kompilierten Ausgabe sollten dieselben Anweisungen in der Lage sein, die Methode für einen Untertyp mit möglicherweise neuen benannten Parametern und für einen Supertyp aufzurufen.
Mögliche Implementierungen
Wenn wir keine definitive Reihenfolge haben können, benötigen wir eine ungeordnete Datenstruktur.
Die einfachste Implementierung besteht darin, einfach den Namen der Parameter zusammen mit den Werten zu übergeben. Auf diese Weise werden benannte Parameter in Perl oder mit Befehlszeilentools emuliert. Dies löst alle oben genannten Erweiterungsprobleme, kann jedoch eine enorme Platzverschwendung darstellen - keine Option für leistungskritischen Code. Außerdem ist die Verarbeitung dieser benannten Parameter jetzt viel komplizierter als das einfache Entfernen von Werten von einem Stapel.
Tatsächlich kann der Platzbedarf durch die Verwendung von String-Pooling verringert werden, wodurch spätere String-Vergleiche auf Zeigervergleiche reduziert werden können (es sei denn, es kann nicht garantiert werden, dass statische Strings tatsächlich gepoolt werden. In diesem Fall müssen die beiden Strings verglichen werden Detail).
Stattdessen könnten wir auch eine clevere Datenstruktur übergeben, die als Wörterbuch benannter Argumente fungiert. Dies ist auf der Anruferseite günstig, da der Tastensatz am Anrufort statisch bekannt ist. Dies würde es ermöglichen, eine perfekte Hash-Funktion zu erstellen oder einen Versuch vorab zu berechnen. Der Angerufene muss noch prüfen, ob alle möglichen Parameternamen vorhanden sind, was etwas teuer ist. So etwas wird von Python verwendet.
Es ist also in den meisten Fällen einfach zu teuer
Wenn eine Funktion mit benannten Parametern ordnungsgemäß erweiterbar sein soll, kann keine endgültige Reihenfolge angenommen werden. Es gibt also nur zwei Lösungen:
- Nehmen Sie die Reihenfolge der benannten Parameter in die Signatur auf und lassen Sie spätere Änderungen nicht zu. Dies ist nützlich für selbstdokumentierenden Code, hilft jedoch nicht bei optionalen Argumenten.
- Übergeben Sie eine Schlüsselwert-Datenstruktur an den Angerufenen, der dann nützliche Informationen extrahieren muss. Dies ist im Vergleich sehr teuer und wird normalerweise nur in Skriptsprachen verwendet, bei denen die Leistung nicht im Vordergrund steht.
Andere Fallstricke
Die Variablennamen in einer Funktionsdeklaration haben normalerweise eine interne Bedeutung und sind nicht Teil der Schnittstelle - auch wenn sie in vielen Dokumentationswerkzeugen weiterhin angezeigt werden. In vielen Fällen möchten Sie unterschiedliche Namen für eine interne Variable und das entsprechende benannte Argument. Sprachen, bei denen die von außen sichtbaren Namen eines benannten Parameters nicht ausgewählt werden können, erhalten nicht viel davon, wenn der Variablenname nicht im Hinblick auf den aufrufenden Kontext verwendet wird.
Ein Problem bei Emulationen benannter Argumente ist die fehlende statische Überprüfung auf der Anruferseite. Dies ist besonders leicht zu vergessen, wenn Sie ein Wörterbuch mit Argumenten übergeben (Sie betrachten, Python). Dies ist wichtig, da das Übergeben eines Wörterbuchs eine häufige Problemumgehung darstellt, z foo({bar: "baz", qux: 42})
. B. in JavaScript:. Hier können weder die Typen der Werte noch die Existenz oder Abwesenheit bestimmter Namen statisch überprüft werden.
Emulieren benannter Parameter (in statisch typisierten Sprachen)
Die einfache Verwendung von Zeichenfolgen als Schlüssel und eines Objekts als Wert ist bei Vorhandensein einer statischen Typprüfung nicht sehr nützlich. Benannte Argumente können jedoch mit Strukturen oder Objektliteralen emuliert werden:
// Java
static abstract class Arguments {
public String bar = "default";
public int qux = 0;
}
void foo(Arguments args) {
...
}
/* using an initializer block */
foo(new Arguments(){{ bar = "baz"; qux = 42; }});