it-swarm.com.de

Warum ist Typinferenz nützlich?

Ich lese Code viel häufiger als ich Code schreibe, und ich gehe davon aus, dass die meisten Programmierer, die an industrieller Software arbeiten, dies tun. Der Vorteil der Typinferenz, den ich annehme, ist weniger Ausführlichkeit und weniger geschriebener Code. Wenn Sie jedoch häufiger Code lesen, möchten Sie wahrscheinlich lesbaren Code.

Der Compiler leitet den Typ ab. Dafür gibt es alte Algorithmen. Die eigentliche Frage ist jedoch, warum ich als Programmierer beim Lesen des Codes auf den Typ meiner Variablen schließen möchte. Ist es nicht schneller für jemanden, nur den Typ zu lesen, als zu überlegen, welcher Typ da ist?

Edit: Abschließend verstehe ich, warum es nützlich ist. Aber in der Kategorie der Sprachfunktionen sehe ich es in einem Eimer mit Überladung des Bedieners - in einigen Fällen nützlich, beeinträchtigt aber die Lesbarkeit, wenn es missbraucht wird.

37
m3th0dman

Werfen wir einen Blick auf Java. Java kann keine Variablen mit abgeleiteten Typen haben. Dies bedeutet, dass ich den Typ häufig buchstabieren muss, auch wenn es für einen menschlichen Leser völlig offensichtlich ist, um welchen Typ es sich handelt:

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

Und manchmal ist es einfach nur ärgerlich, den ganzen Typ zu buchstabieren.

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

Diese ausführliche statische Eingabe stört mich, den Programmierer. Die meisten Typanmerkungen sind sich wiederholende Zeilenfüller, inhaltsfreie Regurgiationen dessen, was wir bereits wissen. Ich mag jedoch statisches Tippen, da es bei der Erkennung von Fehlern sehr hilfreich sein kann. Daher ist die Verwendung von dynamischem Tippen nicht immer eine gute Antwort. Typinferenz ist das Beste aus beiden Welten: Ich kann die irrelevanten Typen weglassen, aber trotzdem sicher sein, dass mein Programm (Typ-) auscheckt.

Während Typinferenz für lokale Variablen sehr nützlich ist, sollte sie nicht für öffentliche APIs verwendet werden, die eindeutig dokumentiert werden müssen. Und manchmal sind die Typen wirklich wichtig, um zu verstehen, was im Code vor sich geht. In solchen Fällen wäre es dumm, sich nur auf die Typinferenz zu verlassen.

Es gibt viele Sprachen, die Typinferenz unterstützen. Zum Beispiel:

  • C++. Das Schlüsselwort auto löst eine Typinferenz aus. Ohne sie wäre es die Hölle, die Typen für Lambdas oder für Einträge in Containern zu formulieren.

  • C #. Sie können Variablen mit var deklarieren, wodurch eine begrenzte Form der Typinferenz ausgelöst wird. Es verwaltet immer noch die meisten Fälle, in denen Sie Typinferenz wünschen. An bestimmten Stellen können Sie den Typ vollständig weglassen (z. B. in Lambdas).

  • Haskell und jede Sprache in der ML-Familie. Obwohl die hier verwendete spezifische Variante der Typinferenz sehr leistungsfähig ist, werden häufig Typanmerkungen für Funktionen aus zwei Gründen angezeigt: Die erste ist die Dokumentation, und die zweite ist eine Überprüfung, ob die Typinferenz tatsächlich die erwarteten Typen gefunden hat. Wenn es eine Diskrepanz gibt, gibt es wahrscheinlich eine Art Fehler.

46
amon

Es ist wahr, dass Code viel häufiger gelesen als geschrieben wird. Das Lesen nimmt jedoch auch Zeit in Anspruch, und zwei Codebildschirme sind schwieriger zu navigieren und zu lesen als ein Codebildschirm. Daher müssen wir Prioritäten setzen, um das beste Verhältnis zwischen nützlichen Informationen und Leseaufwand zu erzielen. Dies ist ein allgemeines UX-Prinzip: Zu viele Informationen überfordern und beeinträchtigen die Effektivität der Schnittstelle.

Und es ist meine Erfahrung, dass oft der genaue Typ nicht (das) wichtig ist. Sicherlich verschachteln Sie manchmal Ausdrücke: x + y * z, monkey.eat(bananas.get(i)), factory.makeCar().drive(). Jeder von diesen enthält Unterausdrücke, die einen Wert ergeben, dessen Typ nicht ausgeschrieben ist. Dennoch sind sie vollkommen klar. Wir sind damit einverstanden, den Typ nicht anzugeben, da es einfach genug ist, aus dem Kontext herauszufinden, und das Ausschreiben würde mehr schaden als nützen (das Verständnis des Datenflusses durcheinander bringen, wertvollen Bildschirm und Kurzzeitspeicherplatz beanspruchen).

Ein Grund, Ausdrücke nicht zu verschachteln, als gäbe es kein Morgen, ist, dass die Zeilen lang werden und der Wertefluss unklar wird. Die Einführung einer temporären Variablen hilft dabei, legt eine Reihenfolge fest und gibt einem Teilergebnis einen Namen. Nicht alles, was von diesen Aspekten profitiert, profitiert jedoch auch davon, dass sein Typ angegeben wird:

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

Ist es wichtig, ob user ein Entitätsobjekt, eine Ganzzahl, eine Zeichenfolge oder etwas anderes ist? Für die meisten Zwecke reicht es nicht aus, zu wissen, dass es einen Benutzer darstellt, aus der HTTP-Anforderung stammt und zum Abrufen des Namens verwendet wird, der in der unteren rechten Ecke der Antwort angezeigt wird.

Und wenn es tut wichtig ist, kann der Autor den Typ frei schreiben. Dies ist eine Freiheit, die verantwortungsbewusst genutzt werden muss, aber das Gleiche gilt für alles andere, was die Lesbarkeit verbessern kann (Variablen- und Funktionsnamen, Formatierung, API-Design, Leerraum). Und tatsächlich besteht die Konvention in Haskell und ML (wo alles ohne zusätzlichen Aufwand abgeleitet werden kann) darin, die Arten nicht-lokaler Funktionsfunktionen aufzuschreiben. und gegebenenfalls auch von lokalen Variablen und Funktionen. Nur Anfänger lassen jeden Typ ableiten.

26
user7043

Ich denke, Typinferenz ist sehr wichtig und sollte in jeder modernen Sprache unterstützt werden. Wir alle entwickeln uns in IDEs und sie könnten viel helfen, falls Sie den abgeleiteten Typ kennen wollen, nur wenige von uns hacken in vi. Denken Sie beispielsweise an Ausführlichkeits- und Zeremoniencode in Java).

  Map<String,HashMap<String,String>> map = getMap();

Aber Sie können sagen, dass es in Ordnung ist. Mein IDE wird mir helfen, es könnte ein gültiger Punkt sein. Einige Funktionen wären jedoch ohne die Hilfe der Typinferenz nicht verfügbar, z. B. anonyme C # -Typen.

 var person = new {Name="John Smith", Age = 105};

Ohne die Hilfe der Typinferenz, zum Beispiel Select, wäre Linq nicht so schön wie jetzt

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

Dieser anonyme Typ wird sauber auf die Variable abgeleitet.

Ich mag keine Typinferenz auf Rückgabetypen in Scala, da ich denke, dass Ihr Punkt hier zutrifft. Es sollte für uns klar sein, was eine Funktion zurückgibt, damit wir die API flüssiger verwenden können

4
Sleiman Jneidi

Ich denke, die Antwort darauf ist wirklich einfach: Sie erspart das Lesen und Schreiben redundanter Informationen. Besonders in objektorientierten Sprachen, in denen Sie auf beiden Seiten des Gleichheitszeichens einen Typ haben.

Hier erfahren Sie auch, wann Sie es verwenden sollten oder nicht - wenn die Informationen nicht redundant sind.

4
jmoreno

Angenommen, man sieht den Code:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Wenn someBigLongGenericType aus dem Rückgabetyp someFactoryMethod zuweisbar ist, wie wahrscheinlich ist es, dass jemand, der den Code liest, bemerkt, wenn die Typen nicht genau übereinstimmen, und wie schnell könnte jemand, der die Diskrepanz bemerkt hat erkennen, ob es beabsichtigt war oder nicht?

Durch das Zulassen von Schlussfolgerungen kann eine Sprache jemandem, der Code liest, vorschlagen, dass die Person versuchen sollte, einen Grund dafür zu finden, wenn der Typ einer Variablen explizit angegeben wird. Dies wiederum ermöglicht es Menschen, die Code lesen, ihre Bemühungen besser zu konzentrieren. Wenn im Gegensatz dazu die überwiegende Mehrheit der Fälle, in denen ein Typ angegeben wird, genau dem entspricht, was abgeleitet worden wäre, ist es möglicherweise weniger anfällig, dass jemand, der Code liest, die Zeiten bemerkt, in denen er sich geringfügig unterscheidet .

3
supercat

Ich sehe, dass es bereits einige gute Antworten gibt. Einige davon werde ich wiederholen, aber manchmal möchten Sie die Dinge einfach in Ihre eigenen Worte fassen. Ich werde mit einigen Beispielen aus C++ kommentieren, da dies die Sprache ist, mit der ich am besten vertraut bin.

Was notwendig ist, ist niemals unklug. Typinferenz ist erforderlich, um andere Sprachfunktionen praktisch zu machen. In C++ ist es möglich, nicht aussprechbare Typen zu haben.

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

C++ 11 fügte Lambdas hinzu, die ebenfalls nicht aussprechbar sind.

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

Typinferenz untermauert auch Vorlagen.

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

Ihre Fragen lauteten jedoch: "Warum sollte ich als Programmierer beim Lesen des Codes auf den Typ meiner Variablen schließen wollen? Ist es für niemanden schneller, nur den Typ zu lesen, als zu überlegen, welcher Typ vorhanden ist?"

Typinferenz beseitigt Redundanz. Wenn es um das Lesen von Code geht, ist es manchmal schneller und einfacher, redundante Informationen im Code zu haben, aber Redundanz kann die nützlichen Informationen überschatten. Zum Beispiel:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Es ist nicht sehr vertraut mit der Standardbibliothek, damit ein C++ - Programmierer erkennt, dass i ein Iterator von i = v.begin() ist, sodass die explizite Typdeklaration von begrenztem Wert ist. Durch seine Anwesenheit werden wichtigere Details verdeckt (z. B. dass i auf den Anfang des Vektors zeigt). Die feine Antwort von @amon liefert ein noch besseres Beispiel für die Ausführlichkeit, die wichtige Details überschattet. Im Gegensatz dazu werden durch die Verwendung der Typinferenz die wichtigen Details stärker hervorgehoben.

std::vector<int> v;
auto i = v.begin();

Obwohl das Lesen von Code wichtig ist, reicht es nicht aus. Irgendwann müssen Sie aufhören zu lesen und neuen Code schreiben. Redundanz im Code macht das Ändern von Code langsamer und schwieriger. Angenommen, ich habe das folgende Codefragment:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Für den Fall, dass ich den Werttyp des Vektors ändern muss, um den Code doppelt zu ändern:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

In diesem Fall muss ich den Code an zwei Stellen ändern. Im Gegensatz zur Typinferenz, bei der der ursprüngliche Code lautet:

std::vector<int> v;
auto i = v.begin();

Und der geänderte Code:

std::vector<double> v;
auto i = v.begin();

Beachten Sie, dass ich jetzt nur noch eine Codezeile ändern muss. Wenn Sie dies auf ein großes Programm extrapolieren, kann die Typinferenz Änderungen an Typen viel schneller übertragen als mit einem Editor.

Redundanz im Code kann zu Fehlern führen. Jedes Mal, wenn Ihr Code davon abhängt, dass zwei Informationen gleich gehalten werden, besteht die Möglichkeit eines Fehlers. Zum Beispiel gibt es eine Inkonsistenz zwischen den beiden Typen in dieser Anweisung, die wahrscheinlich nicht beabsichtigt ist:

int pi = 3.14159;

Redundanz erschwert das Erkennen von Absichten. In einigen Fällen kann die Typinferenz leichter zu lesen und zu verstehen sein, da sie einfacher ist als die explizite Typspezifikation. Betrachten Sie das Codefragment:

int y = sq(x);

Für den Fall, dass sq(x) ein int zurückgibt, ist es nicht offensichtlich, ob y ein int ist, da es sich um den Rückgabetyp von sq(x) oder weil es zu den Anweisungen passt, die y verwenden. Wenn ich anderen Code so ändere, dass sq(x) nicht mehr int zurückgibt, ist allein in dieser Zeile nicht sicher, ob der Typ von y aktualisiert werden soll. Im Gegensatz zum gleichen Code, jedoch mit Typinferenz:

auto y = sq(x);

In diesem Fall ist die Absicht klar, y muss derselbe Typ sein, der von sq(x) zurückgegeben wird. Wenn der Code den Rückgabetyp von sq(x) ändert, ändert sich der Typ von y automatisch.

In C++ gibt es einen zweiten Grund, warum das obige Beispiel mit Typinferenz einfacher ist. Typinferenz kann keine implizite Typkonvertierung einführen. Wenn der Rückgabetyp von sq(x) nicht int ist, fügt der Compiler stillschweigend eine implizite Konvertierung in int ein. Wenn der Rückgabetyp von sq(x) ein komplexer Typ ist, der operator int() definiert, kann dieser versteckte Funktionsaufruf beliebig komplex sein.

2
Bowie Owens