it-swarm.com.de

Ist es eine schlechte Idee, verschiedene Datentypen von einer einzelnen Funktion in einer dynamisch typisierten Sprache zurückzugeben?

Meine Primärsprache ist statisch typisiert (Java). In Java müssen Sie von jeder Methode einen einzelnen Typ zurückgeben. Beispielsweise können Sie keine Methode haben, die bedingt ein String oder bedingt ein Integer zurückgibt. In JavaScript ist dies beispielsweise sehr gut möglich.

In einer statisch typisierten Sprache verstehe ich, warum dies eine schlechte Idee ist. Wenn jede Methode Object zurückgibt (das gemeinsame übergeordnete Element, von dem alle Klassen erben), haben Sie und der Compiler keine Ahnung, womit Sie es zu tun haben. Sie müssen alle Ihre Fehler zur Laufzeit entdecken.

In einer dynamisch typisierten Sprache gibt es möglicherweise nicht einmal einen Compiler. In einer dynamisch typisierten Sprache ist mir nicht klar, warum eine Funktion, die mehrere Typen zurückgibt, eine schlechte Idee ist. Aufgrund meines Hintergrunds in statischen Sprachen vermeide ich das Schreiben solcher Funktionen, aber ich befürchte, dass ich mir über eine Funktion Gedanken mache, die meinen Code auf eine Weise sauberer machen könnte, die ich nicht sehen kann.


Bearbeiten: Ich werde mein Beispiel entfernen (bis ich mir ein besseres vorstellen kann). Ich denke, es lenkt die Leute, auf einen Punkt zu antworten, den ich nicht ansprechen möchte.

68
Daniel Kaplan

Im Gegensatz zu anderen Antworten gibt es Fälle, in denen die Rückgabe verschiedener Typen akzeptabel ist.

Beispiel 1

sum(2, 3) → int
sum(2.1, 3.7) → float

In einigen statisch typisierten Sprachen ist dies mit Überladungen verbunden, sodass wir davon ausgehen können, dass es mehrere Methoden gibt, von denen jede den vordefinierten festen Typ zurückgibt. In dynamischen Sprachen kann dies dieselbe Funktion sein, die implementiert ist als:

var sum = function (a, b) {
    return a + b;
};

Gleiche Funktion, verschiedene Arten von Rückgabewerten.

Beispiel 2

Stellen Sie sich vor, Sie erhalten eine Antwort von einer OpenID/OAuth-Komponente. Einige OpenID/OAuth-Anbieter enthalten möglicherweise weitere Informationen, z. B. das Alter der Person.

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Facebook',
//     name: {
//         firstName: 'Hello',
//         secondName: 'World',
//     },
//     email: '[email protected]',
//     age: 27
// }

Andere hätten das Minimum, wäre es eine E-Mail-Adresse oder das Pseudonym.

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Google',
//     email: '[email protected]'
// }

Wieder gleiche Funktion, unterschiedliche Ergebnisse.

Hier ist der Vorteil der Rückgabe verschiedener Typen besonders wichtig in einem Kontext, in dem Sie sich nicht für Typen und Schnittstellen interessieren, sondern dafür, welche Objekte tatsächlich enthalten sind. Stellen wir uns zum Beispiel vor, eine Website enthält eine ausgereifte Sprache. Dann kann die findCurrent() folgendermaßen verwendet werden:

var user = authProvider.findCurrent();
if (user.age || 0 >= 16) {
    // The person can stand mature language.
    allowShowingContent();
} else if (user.age) {
    // OpenID/OAuth gave the age, but the person appears too young to see the content.
    showParentalAdvisoryRequestedMessage();
} else {
    // OpenID/OAuth won't tell the age of the person. Ask the user himself.
    askForAge();
}

Eine Umgestaltung in Code, bei dem jeder Anbieter seine eigene Funktion hat, die einen genau definierten, festen Typ zurückgibt, würde nicht nur die Codebasis verschlechtern und eine Codeduplizierung verursachen, sondern auch keinen Nutzen bringen. Man kann am Ende Horror machen wie:

var age;
if (['Facebook', 'Yahoo', 'Blogger', 'LiveJournal'].contains(user.provider)) {
    age = user.age;
}
43

Im Allgemeinen ist es aus den gleichen Gründen eine schlechte Idee. Das moralische Äquivalent in einer statisch typisierten Sprache ist eine schlechte Idee: Sie haben keine Ahnung, welcher konkrete Typ zurückgegeben wird, also haben Sie keine Ahnung, was Sie mit dem Ergebnis tun können (abgesehen von der wenige Dinge, die mit jedem Wert gemacht werden können). In einem statischen Typsystem haben Sie vom Compiler überprüfte Anmerkungen zu Rückgabetypen und dergleichen, aber in einer dynamischen Sprache ist das gleiche Wissen noch vorhanden - es ist nur informell und wird in Gehirn und Dokumentation anstatt im Quellcode gespeichert.

In vielen Fällen gibt es jedoch einen Reim und einen Grund, zu dem der Typ zurückgegeben wird, und der Effekt ähnelt einer Überladung oder einem parametrischen Polymorphismus in statischen Typsystemen. Mit anderen Worten, der Ergebnistyp ist vorhersehbar, ist einfach nicht ganz so einfach auszudrücken.

Beachten Sie jedoch, dass es andere Gründe geben kann, warum eine bestimmte Funktion schlecht ausgelegt ist: Beispielsweise ist eine sum -Funktion, die bei ungültigen Eingaben false zurückgibt, eine schlechte Idee, vor allem, weil dieser Rückgabewert nutzlos und fehleranfällig ist (0 <- > falsche Verwirrung).

31
user7043

In dynamischen Sprachen sollten Sie nicht fragen, ob return verschiedene Typen, sondern Objekte mit verschiedenen APIs. Da sich die meisten dynamischen Sprachen nicht wirklich für Typen interessieren, verwenden Sie verschiedene Versionen von Ententypisierung .

Bei der Rückgabe sind verschiedene Typen sinnvoll

Zum Beispiel ist diese Methode sinnvoll:

def load_file(file): 
    if something: 
       return ['a ', 'list', 'of', 'strings'] 
    return open(file, 'r')

Da sowohl die Datei als auch eine Liste von Zeichenfolgen (in Python) iterable sind, die Zeichenfolgen zurückgeben. Sehr unterschiedliche Typen, dieselbe API (es sei denn, jemand versucht, Dateimethoden in einer Liste aufzurufen, dies ist jedoch eine andere Geschichte).

Sie können list oder Tuple bedingt zurückgeben (Tuple ist eine unveränderliche Liste in Python).

Formal sogar tun:

def do_something():
    if ...: 
        return None
    return something_else

oder:

function do_something(){
   if (...) return null; 
   return sth;
}

gibt verschiedene Typen zurück, da sowohl python None als auch Javascript null eigenständige Typen sind.

Alle diese Anwendungsfälle hätten ihr Gegenstück in statischen Sprachen, die Funktion würde nur eine ordnungsgemäße Schnittstelle zurückgeben.

Wenn Sie Objekte mit einer anderen API bedingt zurückgeben, ist dies eine gute Idee

Ob die Rückgabe verschiedener APIs eine gute Idee ist, macht IMO in den meisten Fällen keinen Sinn. Das einzige vernünftige Beispiel, das mir in den Sinn kommt, ist etwas in der Nähe von was @MainMa gesagt hat : Wenn Ihre API unterschiedlich viele Details bereitstellen kann, ist es möglicherweise sinnvoll, mehr Details zurückzugeben, wenn diese verfügbar sind.

26
jb.

Ihre Frage bringt mich dazu, ein wenig zu weinen. Nicht für die von Ihnen angegebene Beispielverwendung, sondern weil jemand diesen Ansatz unwissentlich zu weit führt. Es ist nur ein kurzer Schritt von lächerlich nicht wartbarem Code entfernt.

Der Anwendungsfall der Fehlerbedingung macht irgendwie Sinn, und das Nullmuster (alles muss ein Muster sein) in statisch typisierten Sprachen macht das Gleiche . Ihr Funktionsaufruf gibt ein object oder null zurück.

Aber es ist ein kurzer Schritt zu sagen "Ich werde dies verwenden, um ein Factory-Muster zu erstellen" und entweder foo oder bar oder baz je nach Stimmung der Funktion. Das Debuggen wird zu einem Albtraum, wenn der Anrufer foo erwartet, aber bar erhalten hat.

Ich glaube also nicht, dass du aufgeschlossen bist. Sie sind bei der Verwendung der Sprachfunktionen angemessen vorsichtig.

Offenlegung: Mein Hintergrund ist in statisch typisierten Sprachen, und ich habe im Allgemeinen in größeren, unterschiedlichen Teams gearbeitet, in denen die Notwendigkeit einer Wartung besteht Code war ziemlich hoch. Meine Perspektive ist also wahrscheinlich auch verzerrt.

9
user53019

Mit Generics in Java können Sie einen anderen Typ zurückgeben, während die Sicherheit des statischen Typs erhalten bleibt. Geben Sie einfach den Typ an, den Sie im Parameter generic type des Funktionsaufrufs zurückgeben möchten.

Ob Sie in Javascript einen ähnlichen Ansatz verwenden könnten, ist natürlich eine offene Frage. Da Javascript eine dynamisch typisierte Sprache ist, scheint die Rückgabe eines object die naheliegende Wahl zu sein.

Wenn Sie wissen möchten, wo ein dynamisches Rückgabeszenario möglicherweise funktioniert, wenn Sie es gewohnt sind, in einer statisch typisierten Sprache zu arbeiten, sollten Sie das Schlüsselwort dynamic in C # verwenden. Rob Conery konnte erfolgreich einen objektrelationalen Mapper schreiben in 400 Codezeilen mit dem Schlüsselwort dynamic.

Natürlich ist alles, was dynamic wirklich tut, das Umschließen einer object -Variablen mit einer gewissen Sicherheit vom Laufzeittyp.

6
Robert Harvey

Tatsächlich ist es überhaupt nicht ungewöhnlich, verschiedene Typen zurückzugeben, selbst in einer statisch typisierten Sprache. Deshalb haben wir zum Beispiel Gewerkschaftstypen.

Tatsächlich geben Methoden in Java fast immer einen von vier Typen zurück: eine Art Objekt oder null oder eine Ausnahme, oder sie geben überhaupt nicht zurück.

In vielen Sprachen werden Fehlerbedingungen als Unterprogramme modelliert, die entweder einen Ergebnistyp oder einen Fehlertyp zurückgeben. Zum Beispiel in Scala:

def transferMoney(amount: Decimal): Either[String, Decimal]

Dies ist natürlich ein dummes Beispiel. Der Rückgabetyp bedeutet "entweder eine Zeichenfolge oder eine Dezimalstelle zurückgeben". Konventionell ist der linke Typ der Fehlertyp (in diesem Fall eine Zeichenfolge mit der Fehlermeldung) und der rechte Typ der Ergebnistyp.

Dies ähnelt Ausnahmen, mit der Ausnahme, dass Ausnahmen auch Kontrollflusskonstrukte sind. Tatsächlich entsprechen sie in ihrer Ausdruckskraft GOTO.

4
Jörg W Mittag

Es wurde noch keine Antwort erwähnt SOLID Prinzipien. Insbesondere sollten Sie dem Liskov-Substitutionsprinzip folgen, dass jede Klasse, die einen anderen Typ als den erwarteten Typ erhält, immer noch mit allem arbeiten kann, was sie bekommen, ohne etwas zu tun um zu testen, welcher Typ zurückgegeben wird.

Wenn Sie also einige zusätzliche Eigenschaften auf ein Objekt werfen oder eine zurückgegebene Funktion mit einer Art Dekorateur umschließen, der immer noch das erreicht, was mit der ursprünglichen Funktion erreicht werden sollte, sind Sie gut, solange kein Code, der Ihre Funktion aufruft, jemals darauf angewiesen ist Verhalten in einem beliebigen Codepfad.

Anstatt eine Zeichenfolge oder eine Ganzzahl zurückzugeben, könnte ein besseres Beispiel die Rückgabe einer Sprinkleranlage oder einer Katze sein. Dies ist in Ordnung, wenn der aufrufende Code nur functionInQuestion.hiss () aufrufen soll. Tatsächlich haben Sie eine implizite Schnittstelle, die der aufrufende Code erwartet, und eine dynamisch typisierte Sprache zwingt Sie nicht dazu, die Schnittstelle explizit zu machen.

Leider werden es Ihre Mitarbeiter wahrscheinlich tun, so dass Sie wahrscheinlich sowieso die gleiche Arbeit in Ihrer Dokumentation ausführen müssen, außer es gibt keine allgemein akzeptierte, knappe, maschinenanalysierbare Methode, wie dies bei der Definition einer Schnittstelle der Fall ist in einer Sprache, die sie hat.

4
psr

Ich denke, es ist eine schlechte Idee, verschiedene Typen unter bestimmten Bedingungen zurückzugeben. Dies kommt unter anderem häufig vor, wenn die Funktion einen oder mehrere Werte zurückgeben kann. Wenn nur ein Wert zurückgegeben werden muss, erscheint es möglicherweise sinnvoll, nur den Wert zurückzugeben, anstatt ihn in ein Array zu packen, um zu vermeiden, dass er in der aufrufenden Funktion entpackt werden muss. Dies (und die meisten anderen Fälle davon) verpflichtet den Anrufer jedoch, beide Typen zu unterscheiden und zu behandeln. Die Funktion ist leichter zu überlegen, wenn immer der gleiche Typ zurückgegeben wird.

4
user116240

Die "schlechte Praxis" besteht unabhängig davon, ob Ihre Sprache statisch typisiert ist. Die statische Sprache lenkt Sie mehr von diesen Praktiken ab, und Sie finden möglicherweise mehr Benutzer, die sich über "schlechte Praktiken" in einer statischen Sprache beschweren, da es sich um eine formellere Sprache handelt. Die zugrunde liegenden Probleme liegen jedoch in einer dynamischen Sprache vor, und Sie können feststellen, ob sie gerechtfertigt sind.

Hier ist der unangenehme Teil dessen, was Sie vorschlagen. Wenn ich nicht weiß, welcher Typ zurückgegeben wird, kann ich den Rückgabewert nicht sofort verwenden. Ich muss etwas darüber "entdecken".

total = sum_of_array([20, 30, 'q', 50])
if (type_of(total) == Boolean) {
  display_error(...)
} else {
  record_number(total)
}

Oft ist diese Art der Codeumschaltung einfach eine schlechte Praxis. Dies erschwert das Lesen des Codes. In diesem Beispiel sehen Sie, warum das Auslösen und Abfangen von Ausnahmefällen beliebt ist. Anders ausgedrückt: Wenn Ihre Funktion nicht das kann, was sie sagt, sollte sie nicht erfolgreich zurückkehren. Wenn ich Ihre Funktion aufrufe, möchte ich Folgendes tun:

total = sum_of_array([20, 30, 'q', 50])
display_number(total)

Da die erste Zeile erfolgreich zurückgegeben wird, gehe ich davon aus, dass total tatsächlich die Summe des Arrays enthält. Wenn es nicht erfolgreich zurückkehrt, springen wir zu einer anderen Seite meines Programms.

Verwenden wir ein anderes Beispiel, bei dem es nicht nur um die Weitergabe von Fehlern geht. Vielleicht versucht sum_of_array in einigen Fällen, klug zu sein und eine für Menschen lesbare Zeichenfolge zurückzugeben, z. B. "Das ist meine Schließfachkombination!" genau dann, wenn das Array [11,7,19] ist. Ich habe Probleme, an ein gutes Beispiel zu denken. Auf jeden Fall gilt das gleiche Problem. Sie müssen den Rückgabewert überprüfen, bevor Sie etwas damit tun können:

total = sum_of_array([20, 30, 40, 50])
if (type_of(total) == String) {
  write_message(total)
} else {
  record_number(total)
}

Sie könnten argumentieren, dass es für die Funktion nützlich wäre, eine Ganzzahl oder einen Gleitkommawert zurückzugeben, z.

sum_of_array(20, 30, 40) -> int
sum_of_array(23.45, 45.67, 67.789044) -> float

Aber diese Ergebnisse sind für Sie keine unterschiedlichen Typen. Sie werden beide als Zahlen behandeln, und das ist alles, was Sie interessiert. Sum_of_array gibt also den Nummerntyp zurück. Darum geht es beim Polymorphismus.

Es gibt also einige Praktiken, gegen die Sie möglicherweise verstoßen, wenn Ihre Funktion mehrere Typen zurückgeben kann. Wenn Sie sie kennen, können Sie feststellen, ob Ihre bestimmte Funktion ohnehin mehrere Typen zurückgeben soll.

4
Travis Wilson

Der einzige Ort, an dem ich sehe, dass ich verschiedene Typen sende, ist für ungültige Eingaben oder "Ausnahmen für schlechte Männer", bei denen die "außergewöhnliche" Bedingung nicht sehr außergewöhnlich ist. Zum Beispiel aus meinem Repository von PHP-Dienstprogrammfunktionen dieses gekürzte Beispiel:

function ensure_fields($consideration)
{
        $args = func_get_args();
        foreach ( $args as $a ) {
                if ( !is_string($a) ) {
                        return NULL;
                }
                if ( !isset($consideration[$a]) || $consideration[$a]=='' ) {
                        return FALSE;
                }
        }

        return TRUE;
}

Die Funktion gibt nominell ein BOOLEAN zurück, bei ungültiger Eingabe jedoch NULL. Beachten Sie, dass seit PHP 5.3, alle internen PHP Funktionen) sich auch so verhalten . Zusätzlich geben einige interne PHP -Funktionen bei nominaler Eingabe FALSE oder INT zurück, siehe:

strpos('Hello', 'e');  // Returns INT(1)
strpos('Hello', 'q');  // Returns BOOL(FALSE)
3
dotancohen

Ich halte das nicht für eine schlechte Idee! Im Gegensatz zu dieser gängigsten Meinung und wie bereits von Robert Harvey ausgeführt, haben statisch typisierte Sprachen wie Java Generics genau für Situationen wie die, die Sie fragen, eingeführt. Eigentlich Java versuchen, (wo immer möglich) die Typensicherheit beim Kompilieren beizubehalten, und manchmal vermeiden Generika die Duplizierung von Code. Warum? Da Sie dieselbe Methode oder dieselbe Klasse schreiben können, die verschiedene Typen verarbeiten/zurückgeben. Ich werde nur ein sehr kurzes Beispiel machen, um diese Idee zu zeigen:

Java 1.4

public static Boolean getBoolean(String property){
    return (Boolean) properties.getProperty(property);
}
public static Integer getInt(String property){
    return (Integer) properties.getProperty(property);
}

Java 1.5+

public static <T> getValue(String property, Class<T> clazz) throws WhateverCheckedException{
    return clazz.getConstructor(String.class).newInstance(properties.getProperty(property));
}
//the call will be
Boolean b = getValue("useProxy",Boolean.class);
Integer b = getValue("proxyPort",Integer.class);

In einer dynamischen Typisierungssprache können Sie absolut denselben Code schreiben, der für viele Typen funktioniert, da Sie zum Zeitpunkt der Kompilierung keine Typensicherheit haben. Da auch in einer statisch typisierten Sprache Generika eingeführt wurden, um dieses Problem zu lösen, ist es eindeutig ein Hinweis, dass das Schreiben einer Funktion, die verschiedene Typen in einer dynamischen Sprache zurückgibt, keine schlechte Idee ist.

3
thermz

Das Entwickeln von Software ist im Grunde eine Kunst und ein Handwerk des Managements von Komplexität. Sie versuchen, das System an den Punkten einzugrenzen, die Sie sich leisten können, und die Optionen an anderen Punkten einzuschränken. Die Schnittstelle der Funktion ist ein Vertrag, der dazu beiträgt, die Komplexität des Codes zu verwalten, indem das für die Arbeit mit einem beliebigen Code erforderliche Wissen eingeschränkt wird. Durch die Rückgabe verschiedener Typen erweitern Sie die Benutzeroberfläche der Funktion erheblich, indem Sie alle Schnittstellen verschiedener Typen hinzufügen, die Sie zurückgeben, UND nicht offensichtliche Regeln hinzufügen, welche Schnittstelle zurückgegeben wird.

2
Kaerber

Im dynamischen Land dreht sich alles um das Tippen von Enten. Am verantwortungsvollsten ist es, potenziell unterschiedliche Typen in einen Wrapper zu packen, der ihnen dieselbe Schnittstelle bietet.

function ThingyWrapper(thingy){ //a function constructor (class-like thingy)

    //thingy is effectively private and persistent for ThingyWrapper instances

    if(typeof thingy === 'array'){
        this.alertItems = function(){
            thingy.forEach(function(el){ alert(el); });
        }
    }
    else {
        this.alertItems = function(){
            for(var x in thingy){ alert(thingy[x]); }
        }
    }
}

function gimmeThingy(){
    var
        coinToss = Math.round( Math.random() ),//gives me 0 or 1
        arrayThingy = [1,2,3],
        objectThingy = { item1:1, item2:2, item3:3 }
    ;

    //0 dynamically evaluates to false in JS
    return new ThingyWrapper( coinToss ? arrayThingy : objectThingy );
}

gimmeThingy().alertItems(); //should be same every time except order of numbers - maybe

Es mag gelegentlich sinnvoll sein, verschiedene Typen ohne generischen Wrapper zu kacken, aber ehrlich gesagt, in sieben Jahren, in denen ich JS geschrieben habe, fand ich es nicht vernünftig oder bequem, dies sehr oft zu tun. Meistens ist das etwas, was ich im Kontext geschlossener Umgebungen wie dem Inneren eines Objekts tun würde, in dem Dinge zusammengefügt werden. Aber ich habe es nicht oft genug getan, um mir Beispiele vorzustellen.

Meistens würde ich raten, dass Sie nicht mehr so ​​viel über Typen nachdenken. Sie beschäftigen sich mit Typen, wenn Sie dies in einer dynamischen Sprache benötigen. Nicht mehr. Es ist der springende Punkt. Überprüfen Sie nicht den Typ jedes einzelnen Arguments. Das ist etwas, zu dem ich nur in einer Umgebung versucht wäre, in der dieselbe Methode auf nicht offensichtliche Weise inkonsistente Ergebnisse liefern könnte (also auf keinen Fall so etwas tun). Aber es ist nicht der Typ, der zählt, es ist, wie alles, was du mir gibst, funktioniert.

1
Erik Reppen

Perl verwendet dies häufig, da die Funktionsweise einer Funktion von ihrem Kontext abhängt. Beispielsweise könnte eine Funktion ein Array zurückgeben, wenn sie im Listenkontext verwendet wird, oder die Länge des Arrays, wenn sie an einer Stelle verwendet wird, an der ein Skalarwert erwartet wird. Von das Tutorial, das der erste Treffer für "Perl-Kontext" ist , wenn Sie dies tun:

my @now = localtime();

Dann ist @now eine Array-Variable (das bedeutet @) und enthält ein Array wie (40, 51, 20, 9, 0, 109, 5, 8, 0).

Wenn Sie stattdessen die Funktion so aufrufen, dass das Ergebnis ein Skalar sein muss, mit ($ Variablen sind Skalare):

my $now = localtime();

dann macht es etwas ganz anderes: $ now wird so etwas wie "Fri Jan 9 20:51:40 2009" sein.

Ein weiteres Beispiel, an das ich denken kann, ist die Implementierung von REST APIs), bei denen das Format der Rückgabe von den Wünschen des Clients abhängt. ZB HTML, JSON oder XML. Technisch gesehen sind dies jedoch alle Ströme von Bytes ist die Idee ähnlich.

1
RemcoGerlich