Rückgabe von zwei Werten, Tuple vs 'out' vs 'struct'

85

Stellen Sie sich eine Funktion vor, die zwei Werte zurückgibt. Wir können schreiben:

// Using out:
string MyFunction(string input, out int count)

// Using Tuple class:
Tuple<string, int> MyFunction(string input)

// Using struct:
MyStruct MyFunction(string input)

Welches ist die beste Vorgehensweise und warum?

Xaqron
quelle
String ist kein Werttyp. Ich denke, Sie wollten sagen "Betrachten Sie eine Funktion, die zwei Werte zurückgibt".
Eric Lippert
@ Eric: Du hast recht. Ich meinte unveränderliche Typen.
Xaqron
und was ist los mit einer Klasse?
Lukasz Madon
1
@lukas: Nichts, aber es ist sicherlich nicht in Best Practices. Dies ist ein leichtgewichtiger Wert (<16 KB). Wenn ich einen benutzerdefinierten Code hinzufügen möchte, gehe ich structwie Ericerwähnt vor.
Xaqron
1
Ich würde sagen, verwenden Sie nur, wenn Sie den Rückgabewert benötigen, um zu entscheiden, ob Sie die Rückgabedaten überhaupt verarbeiten sollen, wie in TryParse, andernfalls sollten Sie immer ein strukturiertes Objekt zurückgeben, als ob das strukturierte Objekt ein Werttyp oder eine Referenz sein sollte Typ hängt davon ab, welchen zusätzlichen Nutzen Sie aus den Daten ziehen
MikeT

Antworten:

92

Sie haben jeweils ihre Vor- und Nachteile.

Unsere Parameter sind schnell und kostengünstig, erfordern jedoch die Übergabe einer Variablen und die Abhängigkeit von Mutationen. Es ist fast unmöglich, einen out-Parameter mit LINQ korrekt zu verwenden.

Tupel üben Druck auf die Speicherbereinigung aus und dokumentieren sich nicht selbst. "Item1" ist nicht sehr beschreibend.

Benutzerdefinierte Strukturen können langsam kopiert werden, wenn sie groß sind, sind jedoch selbstdokumentierend und effizient, wenn sie klein sind. Es ist jedoch auch schwierig, eine ganze Reihe von benutzerdefinierten Strukturen für triviale Zwecke zu definieren.

Ich würde dazu neigen, die benutzerdefinierte Strukturlösung zu verwenden, wenn alle anderen Dinge gleich sind. Noch besser ist es jedoch , eine Funktion zu erstellen, die nur einen Wert zurückgibt . Warum geben Sie überhaupt zwei Werte zurück?

UPDATE: Beachten Sie, dass Tupel in C # 7, die sechs Jahre nach dem Schreiben dieses Artikels ausgeliefert wurden, Werttypen sind und daher weniger wahrscheinlich Sammlungsdruck erzeugen.

Eric Lippert
quelle
2
Die Rückgabe von zwei Werten ist häufig ein Ersatz dafür, dass keine Optionstypen oder ADT vorhanden sind.
Anton Tykhyy
2
Aus meiner Erfahrung mit anderen Sprachen würde ich sagen, dass im Allgemeinen Tupel für die schnelle und schmutzige Gruppierung von Elementen verwendet werden. Es ist normalerweise besser, eine Klasse oder Struktur zu erstellen, nur weil Sie damit jedes Element benennen können. Bei Verwendung von Tupeln kann es schwierig sein, die Bedeutung jedes Werts zu bestimmen. Sie sparen sich jedoch die Zeit, um die Klasse / Struktur zu erstellen, die übertrieben sein kann, wenn diese Klasse / Struktur nicht anderweitig verwendet wird.
Kevin Cathcart
23
@Xaqron: Wenn Sie feststellen, dass die Idee von "Daten mit Zeitüberschreitung" in Ihrem Programm häufig vorkommt, können Sie einen generischen Typ "TimeLimited <T>" festlegen, damit Ihre Methode einen TimeLimited <string> oder TimeLimited zurückgibt <Uri> oder was auch immer. Die TimeLimited <T> -Klasse kann dann Hilfsmethoden haben, die Ihnen sagen, "wie lange sind wir noch übrig?" oder "ist es abgelaufen?" oder Wasauchimmer. Versuchen Sie, eine interessante Semantik wie diese im Typsystem zu erfassen.
Eric Lippert
3
Absolut, ich würde Tuple niemals als Teil der öffentlichen Schnittstelle verwenden. Aber selbst für den "privaten" Code erhalte ich eine enorme Lesbarkeit von einem richtigen Typ, anstatt Tupel zu verwenden (insbesondere, wie einfach es ist, einen privaten inneren Typ mit Auto-Eigenschaften zu erstellen).
SolutionYogi
2
Was bedeutet Sammeldruck?
rollt
23

Zusätzlich zu den vorherigen Antworten bringt C # 7 Tupel vom Werttyp, im Gegensatz zu System.Tupleeinem Referenztyp, und bietet auch eine verbesserte Semantik.

Sie können sie weiterhin unbenannt lassen und die folgende .Item*Syntax verwenden:

(string, string, int) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.Item1; //John
person.Item2; //Doe
person.Item3;   //42

Aber was an dieser neuen Funktion wirklich mächtig ist, ist die Fähigkeit, Tupel benannt zu haben. Also könnten wir das Obige wie folgt umschreiben:

(string FirstName, string LastName, int Age) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.FirstName; //John
person.LastName; //Doe
person.Age;   //42

Destrukturierung wird ebenfalls unterstützt:

(string firstName, string lastName, int age) = getPerson()

Dimlucas
quelle
2
Habe ich Recht, wenn ich denke, dass dies im Grunde eine Struktur mit Referenzen als Mitglieder unter der Haube zurückgibt?
Austin_Anderson
4
Wissen wir, wie die Leistung im Vergleich zur Verwendung von Parametern ist?
SpaceMonkey
20

Ich denke, die Antwort hängt von der Semantik der Funktion und der Beziehung zwischen den beiden Werten ab.

Beispielsweise verwenden die TryParseMethoden einen outParameter, um den analysierten Wert zu akzeptieren, und geben a zurück, boolum anzugeben, ob die Analyse erfolgreich war oder nicht. Die beiden Werte gehören nicht wirklich zusammen, daher ist es semantisch sinnvoller und die Absicht des Codes ist leichter zu lesen, den outParameter zu verwenden.

Wenn Ihre Funktion jedoch X / Y-Koordinaten eines Objekts auf dem Bildschirm zurückgibt, gehören die beiden Werte semantisch zusammen, und es ist besser, a zu verwenden struct.

Ich persönlich würde es vermeiden, a tuplefür alles zu verwenden, was für externen Code sichtbar ist, da die Syntax für das Abrufen der Mitglieder umständlich ist.

Andrew Cooper
quelle
+1 für Semantik. Ihre Antwort ist mit Referenztypen besser geeignet, wenn wir den outParameter verlassen können null. Es gibt einige nullbare unveränderliche Typen da draußen.
Xaqron
3
Tatsächlich gehören die beiden Werte in TryParse sehr viel zusammen, weitaus mehr als impliziert, wenn einer als Rückgabewert und der andere als ByRef-Parameter verwendet wird. In vielerlei Hinsicht wäre die logische Rückgabe ein nullbarer Typ. Es gibt einige Fälle, in denen das TryParse-Muster gut funktioniert, und einige, in denen es schmerzhaft ist (es ist schön, dass es in einer "if" -Anweisung verwendet werden kann, aber es gibt viele Fälle, in denen ein nullbarer Wert zurückgegeben oder ein Standardwert angegeben werden kann wäre bequemer).
Supercat
@ Supercat Ich würde Andrew zustimmen, sie gehören nicht zusammen. Obwohl sie verwandt sind, sagt Ihnen die Rückgabe, ob Sie sich überhaupt um den Wert kümmern müssen, nicht um etwas, das zusammen verarbeitet werden muss. Nachdem Sie die Rückgabe verarbeitet haben, ist sie für keine andere Verarbeitung in Bezug auf den out-Wert mehr erforderlich. Dies unterscheidet sich von der Rückgabe eines KeyValuePair aus einem Wörterbuch, in dem eine eindeutige und fortlaufende Verknüpfung zwischen dem Schlüssel und dem Wert besteht. Obwohl ich damit einverstanden bin, wenn nullfähige Typen in .Net 1.1 enthalten wären, hätten sie sie wahrscheinlich als null verwendet.
Dies
@MikeT: Ich halte es für außerordentlich bedauerlich, dass Microsoft vorgeschlagen hat, Strukturen nur für Dinge zu verwenden, die einen einzelnen Wert darstellen, obwohl eine exponierte Feldstruktur das ideale Medium ist, um eine Gruppe unabhängiger Variablen, die mit Klebeband zusammengebunden sind, zusammenzuführen . Der Erfolgsindikator und der Wert sind zum Zeitpunkt ihrer Rückgabe zusammen von Bedeutung , auch wenn sie danach als separate Variablen nützlicher wären. Felder einer exponierten Feldstruktur, die in einer Variablen gespeichert sind, können selbst als separate Variablen verwendet werden. Auf jeden Fall ...
Supercat
@MikeT: Aufgrund der Art und Weise Kovarianz ist und nicht im Rahmen unterstützt werden , die nur tryMuster , die mit covariant Schnittstellen arbeiten , ist T TryGetValue(whatever, out bool success); Dieser Ansatz hätte Schnittstellen erlaubt IReadableMap<in TKey, out TValue> : IReadableMap<out TValue>und Code zugelassen, der Instanzen von AnimalInstanzen zuordnen möchte Car, um eine Dictionary<Cat, ToyotaCar>[using TryGetValue<TKey>(TKey key, out bool success). Eine solche Varianz ist nicht möglich, wenn TValuesie als refParameter verwendet wird.
Supercat
2

Ich werde mit dem Ansatz der Verwendung des Out-Parameters fortfahren, da Sie im zweiten Ansatz ein Objekt der Tuple-Klasse erstellen und dann einen Wert hinzufügen müssten, was meiner Meinung nach eine kostspielige Operation ist, verglichen mit der Rückgabe des Parameters value in out. Wenn Sie jedoch mehrere Werte in der Tupelklasse zurückgeben möchten (was tatsächlich nicht durch die Rückgabe eines Out-Parameters erreicht werden kann), werde ich mich für den zweiten Ansatz entscheiden.

Sumit
quelle
Ich stimme zu out. Zusätzlich gibt es ein paramsSchlüsselwort, das ich nicht erwähnt habe, um die Frage klar zu halten.
Xaqron
2

Sie haben keine weitere Option erwähnt, die eine benutzerdefinierte Klasse anstelle von struct enthält. Wenn den Daten eine Semantik zugeordnet ist, die von Funktionen bearbeitet werden kann, oder die Instanzgröße groß genug ist (als Faustregel> 16 Byte), kann eine benutzerdefinierte Klasse bevorzugt werden. Die Verwendung von "out" wird in der öffentlichen API nicht empfohlen, da sie mit Zeigern verknüpft ist und Verständnis für die Funktionsweise von Referenztypen erfordert.

https://msdn.microsoft.com/en-us/library/ms182131.aspx

Tupel ist gut für den internen Gebrauch, aber die Verwendung in öffentlichen APIs ist umständlich. Ich stimme also zwischen Struktur und Klasse für die öffentliche API.

JeanP
quelle
1
Wenn ein Typ nur zum Zweck der Rückgabe einer Aggregation von Werten vorhanden ist, würde ich sagen, dass ein einfacher Werttyp für exponierte Felder für diese Semantik am besten geeignet ist. Wenn der Typ nichts anderes als seine Felder enthält, gibt es keine Frage darüber, welche Arten der Datenüberprüfung er durchführt (offensichtlich keine), ob es sich um eine erfasste oder eine Live-Ansicht handelt (eine Open-Field-Struktur kann nicht funktionieren als Live-Ansicht) usw. Unveränderliche Klassen sind weniger bequem zu bearbeiten und bieten nur dann einen Leistungsvorteil, wenn Instanzen mehrmals weitergegeben werden können.
Supercat
0

Es gibt keine "Best Practice". Es ist das, womit Sie sich wohl fühlen und was in Ihrer Situation am besten funktioniert. Solange Sie damit einverstanden sind, gibt es kein Problem mit einer der von Ihnen veröffentlichten Lösungen.

foxy
quelle
3
Natürlich arbeiten sie alle. Falls es keinen technischen Vorteil gibt, bin ich immer noch gespannt, was hauptsächlich von Experten verwendet wird.
Xaqron