Restituendo due valori, Tuple vs 'out' vs 'struct'


88

Considera una funzione che restituisce due valori. Possiamo scrivere:

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

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

// Using struct:
MyStruct MyFunction(string input)

Qual è la migliore pratica e perché?


La stringa non è un tipo di valore. Penso che volessi dire "considera una funzione che restituisce due valori".
Eric Lippert

@ Eric: hai ragione. Intendevo tipi immutabili.
Xaqron

e cosa c'è di sbagliato in una classe?
Lukasz Madon

1
@lukas: Niente, ma sicuramente non è nelle migliori pratiche. Questo è un valore leggero (<16 KB) e se aggiungo un codice personalizzato, lo seguirò structcome Ericmenzionato.
Xaqron

1
Direi di usarlo solo quando hai bisogno del valore di ritorno per decidere se devi elaborare i dati di ritorno, come in TryParse, altrimenti dovresti sempre restituire un oggetto strutturato, come se l'oggetto strutturato dovesse essere di tipo valore o un riferimento il tipo dipende dall'uso aggiuntivo che fai dei dati
MikeT

Risposte:


94

Ognuno di loro ha i suoi pro e contro.

I parametri di uscita sono veloci ed economici, ma richiedono di trasmettere una variabile e di fare affidamento sulla mutazione. È quasi impossibile utilizzare correttamente un parametro out con LINQ.

Le tuple fanno pressione sulla raccolta dei rifiuti e non si documentano da sole. "Item1" non è molto descrittivo.

Le strutture personalizzate possono essere lente da copiare se sono grandi, ma sono auto-documentanti e sono efficienti se sono piccole. Tuttavia è anche un problema definire un intero gruppo di strutture personalizzate per usi banali.

Sarei propenso alla soluzione della struttura personalizzata a parità di altre condizioni. Ancora meglio però è creare una funzione che restituisca solo un valore . Perché stai restituendo due valori in primo luogo?

AGGIORNAMENTO: si noti che le tuple in C # 7, fornite sei anni dopo la scrittura di questo articolo, sono tipi di valore e quindi è meno probabile che creino pressione di raccolta.


2
La restituzione di due valori è spesso un sostituto per non avere tipi di opzione o ADT.
Anton Tykhyy

2
Dalla mia esperienza con altre lingue, direi che generalmente le tuple vengono utilizzate per raggruppare gli elementi in modo rapido e sporco. Di solito è meglio creare una classe o una struttura semplicemente perché ti consente di nominare ogni elemento. Quando si usano le tuple, il significato di ogni valore può essere difficile da determinare. Ma ti evita di prendere il tempo per creare la classe / struttura che può essere eccessiva se detta classe / struttura non viene usata altrove.
Kevin Cathcart

23
@Xaqron: Se trovi che l'idea di "dati con un timeout" è comune nel tuo programma, potresti considerare di creare un tipo generico "TimeLimited <T>" in modo che il tuo metodo restituisca un TimeLimited <string> o TimeLimited <Uri> o qualsiasi altra cosa. La classe TimeLimited <T> può quindi avere metodi di supporto che ti dicono "quanto tempo ci resta?" o "è scaduto?" o qualsiasi altra cosa. Prova a catturare una semantica interessante come questa nel sistema dei tipi.
Eric Lippert

3
Assolutamente, non userei mai Tuple come parte dell'interfaccia pubblica. Ma anche per il codice "privato", ottengo un'enorme leggibilità da un tipo appropriato invece di usare Tuple (soprattutto quanto è facile creare un tipo interno privato con Proprietà automatiche).
Soluzione

2
Cosa significa pressione di raccolta?
rotola il

27

Aggiungendo alle risposte precedenti, C # 7 porta tuple di tipo valore, a differenza di System.Tuplequesto è un tipo di riferimento e offre anche una semantica migliorata.

Puoi comunque lasciarli senza nome e utilizzare la .Item*sintassi:

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

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

Ma ciò che è veramente potente in questa nuova funzionalità è la possibilità di avere tuple con nome. Quindi potremmo riscrivere quanto sopra in questo modo:

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

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

La destrutturazione è supportata anche:

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


2
Ho ragione nel pensare che questo restituisca fondamentalmente una struttura con riferimenti come membri sotto il cofano?
Austin_Anderson

4
sappiamo come le prestazioni di questo si confrontano con l'utilizzo di parametri?
SpaceMonkey

20

Penso che la risposta dipenda dalla semantica di ciò che la funzione sta facendo e dalla relazione tra i due valori.

Ad esempio, i TryParsemetodi accettano un outparametro per accettare il valore analizzato e restituiscono a boolper indicare se l'analisi è riuscita o meno. I due valori non appartengono realmente insieme, quindi, semanticamente, ha più senso e l'intento del codice è più facile da leggere, utilizzare il outparametro.

Se, tuttavia, la tua funzione restituisce le coordinate X / Y di qualche oggetto sullo schermo, allora i due valori si appartengono semanticamente e sarebbe meglio usare a struct.

Personalmente eviterei di usare a tupleper tutto ciò che sarà visibile al codice esterno a causa della sintassi scomoda per il recupero dei membri.


+1 per la semantica. La tua risposta è più appropriata con i tipi di riferimento quando possiamo lasciare il outparametro null. Esistono alcuni tipi immutabili nullable.
Xaqron

3
In realtà, i due valori in TryParse appartengono molto insieme, molto più di quanto implicito avendo uno come valore di ritorno e l'altro come parametro ByRef. In molti modi, la cosa logica da restituire sarebbe un tipo nullable. Ci sono alcuni casi in cui il pattern TryParse funziona bene, e altri in cui è un problema (è bello che possa essere usato in un'istruzione "if", ma ci sono molti casi in cui restituire un valore nullable o essere in grado di specificare un valore predefinito sarebbe più conveniente).
supercat

@supercat sarei d'accordo con andrew, non appartengono insieme. sebbene siano correlati, il ritorno ti dice se devi preoccuparti del valore, non qualcosa che deve essere elaborato in tandem. quindi dopo aver elaborato il ritorno non è più necessario per qualsiasi altra elaborazione relativa al valore out, questo è diverso dal restituire una KeyValuePair da un dizionario in cui esiste un collegamento chiaro e continuo tra la chiave e il valore. anche se sono d'accordo se i tipi nullable fossero stati in .Net 1.1, probabilmente li avrebbero usati come null sarebbe un modo corretto per non contrassegnare alcun valore
MikeT

@ MikeT: Considero eccezionalmente sfortunato che Microsoft abbia suggerito che le strutture dovrebbero essere utilizzate solo per cose che rappresentano un singolo valore, quando in realtà una struttura a campo esposto è il mezzo ideale per passare insieme un gruppo di variabili indipendenti legate insieme con nastro adesivo . L'indicatore di successo e il valore sono significativi insieme nel momento in cui vengono restituiti , anche se successivamente sarebbero più utili come variabili separate. I campi di una struttura di campo esposto memorizzati in una variabile sono utilizzabili come variabili separate. In ogni caso ...
supercat

@ MikeT: A causa dei modi in cui la covarianza è e non è supportata nel framework, l'unico trymodello che funziona con le interfacce covarianti è T TryGetValue(whatever, out bool success); quell'approccio avrebbe consentito le interfacce IReadableMap<in TKey, out TValue> : IReadableMap<out TValue>e avrebbe consentito al codice che vuole mappare istanze di Animalistanze di Cardi accettare un Dictionary<Cat, ToyotaCar>[using TryGetValue<TKey>(TKey key, out bool success). Una tale varianza non è possibile se TValueviene utilizzata come refparametro.
supercat

2

Seguirò l'approccio dell'utilizzo del parametro Out perché nel secondo approccio sarebbe necessario creare un oggetto della classe Tuple e quindi aggiungere valore ad esso, che penso sia un'operazione costosa rispetto alla restituzione del valore nel parametro out. Sebbene se desideri restituire più valori in Tuple Class (che infatti non può essere ottenuto restituendo solo un parametro out), sceglierò un secondo approccio.


Sono d'accordo con out. Inoltre c'è una paramsparola chiave che non ho menzionato per mantenere la domanda chiara.
Xaqron

2

Non hai menzionato un'altra opzione, ovvero avere una classe personalizzata invece di struct. Se ai dati è associata una semantica che può essere utilizzata dalle funzioni, o se la dimensione dell'istanza è abbastanza grande (> 16 byte come regola pratica), può essere preferita una classe personalizzata. L'uso di "out" non è consigliato nell'API pubblica a causa della sua associazione a puntatori e che richiede la comprensione del funzionamento dei tipi di riferimento.

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

Tuple è utile per uso interno, ma l'utilizzo è scomodo nell'API pubblica. Quindi, il mio voto è tra struct e class per l'API pubblica.


1
Se un tipo esiste esclusivamente allo scopo di restituire un'aggregazione di valori, direi che un semplice tipo di valore del campo esposto è la soluzione più chiara per quelle semantiche. Se non c'è niente nel tipo oltre ai suoi campi, non ci saranno dubbi sui tipi di convalida dei dati che esegue (nessuno, ovviamente), se rappresenta una vista catturata o dal vivo (una struttura a campo aperto non può agire come visualizzazione live), ecc. È meno conveniente lavorare con classi immutabili e offrono un vantaggio in termini di prestazioni solo se le istanze possono essere trasmesse più volte.
supercat

1

Non esiste una "migliore pratica". È ciò con cui ti senti a tuo agio e ciò che funziona meglio nella tua situazione. Finché sei coerente con questo, non ci sono problemi con nessuna delle soluzioni che hai pubblicato.


4
Ovviamente funzionano tutti. Nel caso in cui non ci siano vantaggi tecnici, sono comunque curioso di sapere cosa viene utilizzato maggiormente dagli esperti.
Xaqron
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.