L'uso delle tuple .NET 4.0 nel mio codice C # è una decisione di progettazione scadente?


166

Con l'aggiunta della classe Tuple in .net 4, ho cercato di decidere se usarli nel mio progetto è una scelta sbagliata o meno. A mio modo di vedere, una Tupla può essere una scorciatoia per scrivere una classe di risultati (sono sicuro che ci sono anche altri usi).

Così questo:

public class ResultType
{
    public string StringValue { get; set; }
    public int IntValue { get; set; }
}

public ResultType GetAClassedValue()
{
    //..Do Some Stuff
    ResultType result = new ResultType { StringValue = "A String", IntValue = 2 };
    return result;
}

È equivalente a questo:

public Tuple<string, int> GetATupledValue()
{
    //...Do Some stuff
    Tuple<string, int> result = new Tuple<string, int>("A String", 2);
    return result;
}

Quindi, lasciando da parte la possibilità che mi manchi il punto di Tuple, l'esempio con una Tupla è una cattiva scelta di design? A me sembra meno disordine, ma non come auto-documentante e pulito. ResultTypeCiò significa che con il tipo , sarà molto chiaro in seguito cosa significa ogni parte della classe ma hai del codice extra da mantenere. Con Tuple<string, int>ciò dovrai cercare e capire cosa Itemrappresenta ciascuno , ma scrivi e mantieni meno codice.

Qualsiasi esperienza che hai avuto con questa scelta sarebbe molto apprezzata.


16
@Boltbait: perché la tupla proviene dalla teoria degli insiemi en.wikipedia.org/wiki/Tuple
Matthew Whited,

16
@BoltBait: una "tupla" è un insieme ordinato di valori e non ha necessariamente a che fare con le righe di un database.
FrustratedWithFormsDesigner,

19
Le tuple in c # sarebbero ancora più belle se ci fosse qualche buona azione di disimballaggio delle tuple :)
Justin,

4
@ John Saunders La mia domanda riguarda in particolare le decisioni di progettazione in c #. Non uso altri linguaggi .net, quindi non sono sicuro di come le tuple li influenzino. Quindi l'ho contrassegnato c #. Mi è sembrato ragionevole ma anche il passaggio a .net va bene.
Jason Webb,

13
@ John Saunders: sono abbastanza sicuro che stia chiedendo delle decisioni di progettazione relative all'uso o al non utilizzo di Tuple in un'applicazione scritta esclusivamente in C #. Non credo che stia chiedendo delle decisioni di progettazione che sono state prese per rendere il linguaggio C #.
Brett Widmeier,

Risposte:


163

Le tuple sono grandi se controlli sia la loro creazione che il loro utilizzo: puoi mantenere il contesto, che è essenziale per comprenderle.

Su un'API pubblica, tuttavia, sono meno efficaci. Il consumatore (non tu) deve indovinare o cercare la documentazione, soprattutto per cose come Tuple<int, int>.

Li userei per membri privati ​​/ interni, ma uso le classi di risultati per membri pubblici / protetti.

Questa risposta ha anche alcune informazioni.


19
La differenza tra pubblico / privato è davvero importante, grazie per averlo sollevato. Restituire le tuple nella tua API pubblica è una pessima idea, ma internamente dove le cose potrebbero essere strettamente accoppiate (ma va bene ) va bene.
Clinton Pierce,

36
Strettamente accoppiati o strettamente accoppiati? ;)
contactmatt

Non potrebbe essere utile la documentazione appropriata qui? Ad esempio, Scala StringOps(fondamentalmente una classe di "metodi di estensione" per Java String) ha un metodo parition(Char => Boolean): (String, String)(accetta il predicato, restituisce una tupla di 2 stringhe). È ovvio quale sia l'output (ovviamente uno sarà il personaggio per cui il predicato era vero e l'altro per il quale era falso, ma non è chiaro quale sia quale). È la documentazione che lo chiarisce (e la corrispondenza del modello può essere utilizzata per chiarire nell'uso quale è quale).
Kat,

@ Mike: Questo rende difficile sbagliare e sbagliare, il segno distintivo di un'API che causerà dolore a qualcuno da qualche parte :-)
Bryan Watts,

79

Per come la vedo io, una Tupla è una scorciatoia per scrivere una classe di risultati (sono sicuro che ci sono anche altri usi).

Ci sono davvero altri usi utili perTuple<> - la maggior parte di essi comporta l'astrazione della semantica di un particolare gruppo di tipi che condividono una struttura simile e il loro trattamento semplicemente come un insieme ordinato di valori. In tutti i casi, un vantaggio delle tuple è che evitano di ingombrare il tuo spazio dei nomi con classi di soli dati che espongono proprietà ma non metodi.

Ecco un esempio di un uso ragionevole per Tuple<>:

var opponents = new Tuple<Player,Player>( playerBob, playerSam );

Nell'esempio sopra vogliamo rappresentare una coppia di avversari, una tupla è un modo conveniente per accoppiare queste istanze senza dover creare una nuova classe. Ecco un altro esempio:

var pokerHand = Tuple.Create( card1, card2, card3, card4, card5 );

Una mano di poker può essere pensata solo come un insieme di carte - e la tupla (può essere) un modo ragionevole di esprimere quel concetto.

mettendo da parte la possibilità che mi manchi il punto di Tuple, l'esempio con una Tupla è una cattiva scelta di design?

Restituire Tuple<>istanze fortemente tipizzate come parte di un'API pubblica per un tipo pubblico è raramente una buona idea. Come riconosci tu stesso, le tuple richiedono che le parti coinvolte (autore della biblioteca, utente della biblioteca) concordino in anticipo sullo scopo e l'interpretazione dei tipi di tupla utilizzati. È abbastanza impegnativo creare API intuitive e chiare, l'utilizzo Tuple<>pubblico oscura solo l'intento e il comportamento dell'API.

Anche i tipi anonimi sono una specie di tupla , tuttavia sono fortemente tipizzati e consentono di specificare nomi chiari e informativi per le proprietà appartenenti al tipo. Ma i tipi anonimi sono difficili da usare in diversi metodi: sono stati principalmente aggiunti per supportare tecnologie come LINQ in cui le proiezioni produrrebbero tipi a cui normalmente non vorremmo assegnare nomi. (Sì, so che i tipi anonimi con gli stessi tipi e le proprietà con nome sono consolidati dal compilatore).

La mia regola empirica è: se lo restituirai dalla tua interfaccia pubblica, rendilo un tipo denominato .

Un'altra mia regola empirica per l'uso delle tuple è: argomenti del metodo name e variabili localc di tipo Tuple<>nel modo più chiaro possibile - fai in modo che il nome rappresenti il ​​significato delle relazioni tra gli elementi della tupla. Pensa al mio var opponents = ...esempio.

Ecco un esempio di un caso reale in cui ho usato Tuple<>per evitare di dichiarare un tipo di solo dati da utilizzare solo all'interno del mio assembly . La situazione implica il fatto che quando si utilizzano dizionari generici contenenti tipi anonimi, diventa difficile utilizzare il TryGetValue()metodo per trovare elementi nel dizionario perché il metodo richiede un outparametro che non può essere nominato:

public static class DictionaryExt 
{
    // helper method that allows compiler to provide type inference
    // when attempting to locate optionally existent items in a dictionary
    public static Tuple<TValue,bool> Find<TKey,TValue>( 
        this IDictionary<TKey,TValue> dict, TKey keyToFind ) 
    {
        TValue foundValue = default(TValue);
        bool wasFound = dict.TryGetValue( keyToFind, out foundValue );
        return Tuple.Create( foundValue, wasFound );
    }
}

public class Program
{
    public static void Main()
    {
        var people = new[] { new { LastName = "Smith", FirstName = "Joe" },
                             new { LastName = "Sanders", FirstName = "Bob" } };

        var peopleDict = people.ToDictionary( d => d.LastName );

        // ??? foundItem <= what type would you put here?
        // peopleDict.TryGetValue( "Smith", out ??? );

        // so instead, we use our Find() extension:
        var result = peopleDict.Find( "Smith" );
        if( result.First )
        {
            Console.WriteLine( result.Second );
        }
    }
}

PS Esiste un altro modo (più semplice) per aggirare i problemi derivanti da tipi anonimi nei dizionari, ovvero utilizzare la varparola chiave per consentire al compilatore di "dedurre" il tipo per te. Ecco quella versione:

var foundItem = peopleDict.FirstOrDefault().Value;
if( peopleDict.TryGetValue( "Smith", out foundItem ) )
{
   // use foundItem...
}

5
@stakx: Sì, sono d'accordo. Ma tieni presente che l'esempio è in gran parte inteso a illustrare casi in cui l'uso di a Tuple<> potrebbe avere senso. Ci sono sempre alternative ...
LBushkin,

1
Penso che sostituire i parametri con una Tuple, come in TryGetValue o TryParse, sia uno dei pochi casi in cui potrebbe avere senso avere un'API pubblica che restituisca una Tuple. In effetti, almeno una lingua .NET apporta automaticamente questa modifica API per te.
Joel Mueller,

1
@stakx: anche le tuple sono immutabili, mentre non lo sono gli array.
Robert Paulson,

2
Ho capito che le Tuple sono utili solo come oggetti immutabili per giocatori di poker.
Gennady Vanin Геннадий Ванин

2
+1 Ottima risposta - Adoro i tuoi due esempi iniziali che in realtà mi hanno leggermente cambiato idea. Non ho mai visto esempi prima in cui una tupla era almeno tanto appropriata quanto una struttura o tipo personalizzato. Tuttavia, il tuo ultimo "esempio di vita reale" per me sembra non aggiungere più valore. I primi due esempi sono perfetti e semplici. L'ultimo è un po 'difficile da capire e il tuo tipo di "PS" lo rende inutile poiché il tuo approccio alternativo è molto più semplice e non richiede tuple.
Chiccodoro,

18

Le tuple possono essere utili ... ma possono anche essere un dolore in seguito. Se hai un metodo che restituisce Tuple<int,string,string,int>come fai a sapere quali sono questi valori in seguito. Erano ID, FirstName, LastName, Ageo erano UnitNumber, Street, City, ZipCode.


9

Le tuple sono un'aggiunta piuttosto deludente al CLR dal punto di vista di un programmatore C #. Se si dispone di una raccolta di elementi di lunghezza variabile, non è necessario che abbiano nomi statici univoci al momento della compilazione.

Ma se si dispone di una raccolta di lunghezza costante, ciò implica che la posizione fissa delle posizioni nella raccolta abbia ciascuno un significato predefinito specifico. Ed è sempre meglio dare loro dei nomi statica appropriato in questo caso, invece di dover ricordare il significato di Item1, Item2ecc

Le classi anonime in C # forniscono già un'ottima soluzione per l'uso privato più comune delle tuple e danno nomi significativi agli oggetti, quindi in realtà sono superiori in questo senso. L'unico problema è che non possono uscire dai metodi con nome. Preferirei vedere revocata tale limitazione (forse solo per metodi privati) piuttosto che avere un supporto specifico per le tuple in C #:

private var GetDesserts()
{
    return _icecreams.Select(
        i => new { icecream = i, topping = new Topping(i) }
    );
}

public void Eat()
{
    foreach (var dessert in GetDesserts())
    {
        dessert.icecream.AddTopping(dessert.topping);
        dessert.Eat();
    }
}

Come caratteristica generale, ciò che mi piacerebbe vedere sarebbe che .net definisse uno standard per alcuni tipi speciali di tipi anonimi, tale che ad esempio struct var SimpleStruct {int x, int y;}definirebbe una struttura con un nome che inizia con una guida associata a semplici strutture e ha qualcosa di simile {System.Int16 x}{System.Int16 y}aggiunto; qualsiasi nome che inizi con quel GUID sarebbe richiesto per rappresentare una struttura contenente solo quegli elementi e contenuti specifici specificati; se più definizioni per lo stesso nome sono nell'ambito e corrispondono, tutte sarebbero considerate dello stesso tipo.
supercat

Una cosa del genere sarebbe utile per alcuni tipi di tipi, tra cui classi e strutture mutabili e immutabili, nonché interfacce e delegati da utilizzare in situazioni in cui la struttura di corrispondenza dovrebbe essere considerata come una semantica di corrispondenza. Mentre ci sono certamente ragioni per cui può essere utile avere due tipi di delegati che hanno gli stessi parametri non considerati intercambiabili, sarebbe anche utile se si potesse specificare che un metodo vuole un delegato che accetta una combinazione di parametri ma oltre a ciò non non importa che tipo di delegato sia.
supercat

È un peccato che se due persone diverse vogliono scrivere funzioni che assumono come delegati di input che necessitano di un singolo refparametro, l'unico modo in cui sarà possibile avere un delegato che può essere passato a entrambe le funzioni sarà se la persona produce e compila un delegare il tipo e lo consegna all'altro. Non è possibile che entrambe le persone possano indicare che i due tipi dovrebbero essere considerati sinonimi.
supercat

Hai qualche esempio più concreto di dove useresti le lezioni anonime anziché Tuple? Ho appena sfogliato 50 posizioni nella mia base di codice in cui sto usando le tuple, e tutti sono valori di ritorno (in genere yield return) o sono elementi di raccolte che vengono utilizzati altrove, quindi non andare per le classi anonime.

2
@JonofAllTrades - le opinioni probabilmente variano su questo, ma provo a progettare i metodi pubblici come se fossero usati da qualcun altro, anche se quella persona sono io, quindi mi sto offrendo un buon servizio clienti! Tendi a chiamare una funzione più volte di quanto la scrivi. :)
Daniel Earwicker il

8

Simile alla parola chiave var, è inteso come una comodità, ma è facilmente abusato.

Secondo la mia più umile opinione, non esporre Tuplecome classe di ritorno. Usalo privatamente, se richiesto da una struttura di dati di un servizio o di un componente, ma restituisci classi ben conosciute da metodi pubblici.

// one possible use of tuple within a private context. would never
// return an opaque non-descript instance as a result, but useful
// when scope is known [ie private] and implementation intimacy is
// expected
public class WorkflowHost
{
    // a map of uri's to a workflow service definition 
    // and workflow service instance. By convention, first
    // element of tuple is definition, second element is
    // instance
    private Dictionary<Uri, Tuple<WorkflowService, WorkflowServiceHost>> _map = 
        new Dictionary<Uri, Tuple<WorkflowService, WorkflowServiceHost>> ();
}

2
Le lingue "RAD" tradizionali (Java è l'esempio migliore) hanno sempre scelto la sicurezza ed evitando di spararsi negli scenari di tipo piede. Sono contento che Microsoft stia correndo qualche rischio con C # e dia alla lingua un buon potere, anche se può essere usato in modo improprio.
Matt Greer,

4
La parola chiave var non è possibile abusare. Forse intendevi dinamico?
Chris Marisic,

@ Chris Marisic, solo per chiarire che non intendevo nello stesso scenario - hai assolutamente ragione, varnon puoi essere restituito o esistere al di fuori del suo ambito dichiarato. tuttavia, è ancora possibile usarlo oltre lo scopo previsto ed essere "abusato"
johnny g

5
Continuo a sostenere che quel periodo non può essere abusato. È semplicemente una parola chiave per definire una definizione di variabile più breve. La tua argomentazione forse riguarda i tipi anonimi, ma anche quelli non possono essere abusati perché sono ancora normali tipi collegati staticamente, ora la dinamica è una storia completamente diversa.
Chris Marisic,

4
var può essere abusato nel senso che se viene abusato a volte può causare problemi alla persona che legge il codice. Soprattutto se non sei in Visual Studio e non hai intellisense.
Matt Greer,

5

L'uso di una classe come ResultTypeè più chiaro. Puoi dare nomi significativi ai campi nella classe (mentre con una tupla verrebbero chiamati Item1e Item2). Ciò è ancora più importante se i tipi dei due campi sono gli stessi: il nome li distingue chiaramente.


Un piccolo vantaggio aggiuntivo: puoi riordinare in sicurezza i parametri per una classe. In questo modo per una tupla si romperà il codice e, se i campi hanno tipi simili, questo potrebbe non essere rilevabile al momento della compilazione.

Sono d'accordo, le Tuple vengono utilizzate quando uno sviluppatore manca di energia per pensare a un nome di classe significativo che raggruppa nomi di proprietà significativi. La programmazione riguarda la comunicazione. Le tuple sono un antipattern alla comunicazione. Avrei preferito che Microsoft lo avesse lasciato fuori dal linguaggio per costringere gli sviluppatori a prendere buone decisioni di progettazione.
mike gold,

5

Che ne dite di usare Tuple in un modello di decora-decora-decora? (Trasformazione di Schwartz per il popolo Perl). Ecco un esempio inventato, certo, ma le Tuple sembrano essere un buon modo per gestire questo tipo di cose:

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string[] files = Directory.GetFiles("C:\\Windows")
                    .Select(x => new Tuple<string, string>(x, FirstLine(x)))
                    .OrderBy(x => x.Item2)
                    .Select(x => x.Item1).ToArray();
        }
        static string FirstLine(string path)
        {
            using (TextReader tr = new StreamReader(
                        File.Open(path, FileMode.Open)))
            {
                return tr.ReadLine();
            }
        }
    }
}

Ora, avrei potuto usare un Object [] di due elementi o in questo esempio specifico una stringa [] di due elementi. Il punto è che avrei potuto usare qualsiasi cosa come secondo elemento in una Tupla usata internamente ed è piuttosto facile da leggere.


3
Puoi usare Tuple.Create invece di costruttore. È più corto
Kugel,

un tipo anonimo sarà molto più leggibile; come usare nomi di variabili reali invece di 'x'
Dave Cousineau,

Lo farebbe ora . Questo è un problema SO, le risposte pubblicate anni e anni fa non vengono mai ripulite o aggiornate per le nuove tecnologie.
Clinton Pierce,

4

Queste "tuple" IMO sono fondamentalmente tutti structtipi anonimi di accesso pubblico con membri senza nome .

L' unico posto in cui vorrei usare la tupla è quando è necessario raggruppare rapidamente alcuni dati, in un ambito molto limitato. La semantica dei dati dovrebbe essere ovvia , quindi il codice non è difficile da leggere. Quindi usare una tupla ( int, int) per (riga, col) sembra ragionevole. Ma sono difficile trovare un vantaggio rispetto a un structmembro con nome (quindi non vengono commessi errori e riga / colonna non vengono scambiate accidentalmente)

Se stai rinviando dati al chiamante o accetti dati da un chiamante, dovresti davvero utilizzare un structcon membri nominati.

Prendi un semplice esempio:

struct Color{ float r,g,b,a ; }
public void setColor( Color color )
{
}

La versione tupla

public void setColor( Tuple<float,float,float,float> color )
{
  // why?
}

Non vedo alcun vantaggio nell'usare la tupla al posto di una struttura con membri nominati. L'uso di membri senza nome è un passo indietro per la leggibilità e la comprensibilità del codice.

Tuple mi sembra un modo pigro per evitare di crearne uno structcon membri nominati. L'uso eccessivo della tupla, in cui senti davvero che tu / o qualcun altro che incontri il tuo codice avrebbe bisogno di membri nominati, è una brutta cosa ™ se mai ne vedessi uno.


3

Non giudicarmi, non sono un esperto, ma con le nuove Tuple in C # 7.x ora, potresti restituire qualcosa del tipo:

return (string Name1, int Name2)

Almeno ora puoi nominarlo e gli sviluppatori potrebbero vedere alcune informazioni.


2

Dipende, ovviamente! Come hai detto, una tupla può farti risparmiare tempo e codice quando desideri raggruppare alcuni elementi per il consumo locale. Puoi anche usarli per creare algoritmi di elaborazione più generici di quelli che puoi se passi una classe concreta. Non ricordo quante volte avrei desiderato avere qualcosa oltre KeyValuePair o un DataRow per passare rapidamente una data da un metodo a un altro.

D'altra parte, è del tutto possibile esagerare e passare intorno alle tuple dove puoi solo indovinare ciò che contengono. Se hai intenzione di usare una tupla tra le classi, forse sarebbe meglio creare una classe concreta.

Usate con moderazione ovviamente, le tuple possono portare a un codice più conciso e leggibile. Puoi guardare a C ++, STL e Boost per esempi di come le Tuple sono usate in altre lingue ma alla fine, dovremo tutti sperimentare per trovare il modo migliore per adattarle all'ambiente .NET.


1

Le tuple sono una funzione del framework inutile in .NET 4. Penso che con C # 4.0 sia stata persa una grande opportunità. Mi sarebbe piaciuto avere tuple con membri nominati, in modo da poter accedere ai vari campi di una tupla per nome invece che per Valore1 , Valore2 , ecc ...

Avrebbe richiesto un cambio di lingua (sintassi), ma sarebbe stato molto utile.


In che modo le tuple sono una "caratteristica della lingua"? È una classe disponibile per tutte le lingue .net, vero?
Jason Webb,

11
Relativamente inutile per C #, forse. Ma System.Tuple non è stato aggiunto al framework a causa di C #. È stato aggiunto per facilitare l'interoperabilità tra F # e altre lingue.
Joel Mueller,

@Jason: non ho detto che fosse una caratteristica del linguaggio (l'ho scritto male, ma l'ho corretto prima che tu facessi questo commento).
Philippe Leybaert,

2
@Jason - le lingue con supporto diretto per le tuple hanno un supporto implicito per la decostruzione delle tuple nei loro valori dei componenti. Il codice scritto in quelle lingue in genere non accede mai e poi mai alle proprietà Item1, Item2. let success, value = MethodThatReturnsATuple()
Joel Mueller,

@Joel: questa domanda è taggata come C #, quindi si tratta in qualche modo di C #. Penso ancora che avrebbero potuto trasformarlo in una funzionalità di prima classe nella lingua.
Philippe Leybaert,

1

Personalmente non userei mai una Tupla come tipo di ritorno perché non vi è alcuna indicazione di ciò che rappresentano i valori. Le tuple hanno alcuni usi preziosi perché a differenza degli oggetti sono tipi di valore e quindi comprendono l'uguaglianza. Per questo motivo li userò come chiavi del dizionario se avrò bisogno di una chiave multipart o come chiave per una clausola GroupBy se voglio raggruppare in base a più variabili e non voglio raggruppamenti nidificati (chi vuole mai raggruppamenti nidificati?). Per superare il problema con estrema verbosità, puoi crearli con un metodo di supporto. Tieni presente che se accedi frequentemente ai membri (tramite Item1, Item2, ecc.), Probabilmente dovresti utilizzare un costrutto diverso come una struttura o una classe anonima.


0

Ho usato le tuple, sia la Tuplenuova che la nuova ValueTuple, in diversi scenari e sono arrivato alla seguente conclusione: non usare .

Ogni volta ho riscontrato i seguenti problemi:

  • il codice è diventato illeggibile a causa della mancanza di nomi forti;
  • impossibile utilizzare le funzionalità della gerarchia di classi, come DTO di classe base e DTO di classe figlio, ecc .;
  • se vengono utilizzati in più di un posto, si finisce per copiare e incollare queste brutte definizioni, anziché un nome di classe pulito.

La mia opinione è che le tuple sono un danno, non una caratteristica, di C #.

Ho critiche un po 'simili, ma molto meno dure di Func<>e Action<>. Sono utili in molte situazioni, in particolare le semplici Actione le Func<type>varianti, ma a parte questo, ho scoperto che la creazione di un tipo delegato è più elegante, leggibile, gestibile e ti dà più funzionalità, come ref/ outparametri.


Ho citato le tuple nominate - ValueTuple- e non mi piacciono neanche a me. Innanzitutto, la denominazione non è forte, è più un suggerimento, molto facile da confondere perché può essere assegnata implicitamente a uno Tupleo ValueTuplecon lo stesso numero e tipo di elementi. In secondo luogo, la mancanza di ereditarietà e la necessità di copiare e incollare l'intera definizione da un luogo all'altro sono ancora svantaggi.
Sig. TA,

Sono d'accordo con questi punti. Cerco di usare Tuple solo quando controllo il produttore e il consumatore e se non sono troppo "lontani". Significato, produttore e consumatore nello stesso metodo = "vicino", mentre produttore e consumatore in diverse assemblee = "anni luce di distanza". Quindi, se creo le tuple all'inizio di un metodo e le uso alla fine? Certo, sto bene con quello. Documento anche i termini e così via. Ma sì, sono d'accordo che generalmente non sono la scelta giusta.
Mike Christiansen,
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.