Esiste uno scopo specifico per elenchi eterogenei?


13

Provenendo da un background C # e Java, sono abituato a rendere le mie liste omogenee, e questo ha senso per me. Quando ho iniziato a raccogliere Lisp, ho notato che le liste possono essere eterogenee. Quando ho iniziato a rovistare con la dynamicparola chiave in C #, ho notato che, a partire da C # 4.0, ci possono essere anche elenchi eterogenei:

List<dynamic> heterogeneousList

La mia domanda è: qual è il punto? Sembra che un elenco eterogeneo avrà molto più sovraccarico durante l'elaborazione e che se è necessario archiviare tipi diversi in un unico posto potrebbe essere necessaria una struttura dati diversa. La mia ingenuità sta allevando la sua brutta faccia o ci sono davvero momenti in cui è utile avere un elenco eterogeneo?


1
Intendevi ... ho notato che le liste possono essere eterogenee ...?
Ingegnere mondiale

In che modo è List<dynamic>diverso (per la tua domanda) dal fare semplicemente List<object>?
Peter K.

@WorldEngineer sì, lo faccio. Ho aggiornato il mio post. Grazie!
Jetti

@PeterK. Immagino per l'uso quotidiano, non c'è alcuna differenza. Tuttavia, non tutti i tipi in C # derivano da System.Object, quindi ci sarebbero casi limite in cui vi sono differenze.
Jetti

Risposte:


16

Il documento Collezioni eterogenee fortemente tipizzate di Oleg Kiselyov, Ralf Lämmel e Keean Schupke contiene non solo un'implementazione di elenchi eterogenei in Haskell, ma anche un esempio motivante di quando, perché e come utilizzeresti le liste. In particolare, lo stanno utilizzando per l'accesso al database controllato in fase di compilazione e sicuro. (Pensa a LINQ, infatti, l'articolo a cui fanno riferimento è l'articolo di Haskell di Erik Meijer e altri che ha portato a LINQ.)

Citando dal paragrafo introduttivo del documento HLists:

Ecco un elenco aperto di esempi tipici che richiedono raccolte eterogenee:

  • Una tabella di simboli che dovrebbe memorizzare voci di diverso tipo è eterogenea. È una mappa finita, in cui il tipo di risultato dipende dal valore dell'argomento.
  • Un elemento XML è tipizzato in modo eterogeneo. In effetti, gli elementi XML sono raccolte nidificate vincolate da espressioni regolari e dalla proprietà 1-ambiguity.
  • Ogni riga restituita da una query SQL è una mappa eterogenea dai nomi delle colonne alle celle. Il risultato di una query è un flusso omogeneo di righe eterogenee.
  • L'aggiunta di un sistema a oggetti avanzato a un linguaggio funzionale richiede raccolte eterogenee di un tipo che combina record estensibili con sottotipi e un'interfaccia di enumerazione.

Nota che gli esempi che hai fornito nella tua domanda non sono in realtà elenchi eterogenei nel senso che la parola è comunemente usata. Sono elenchi debolmente tipizzati o non tipizzati . In realtà, sono in realtà elenchi omogenei , poiché tutti gli elementi sono dello stesso tipo: objecto dynamic. Sei quindi costretto a eseguire cast o instanceoftest non controllati o qualcosa del genere, al fine di essere effettivamente in grado di lavorare in modo significativo con gli elementi, il che li rende debolmente digitati.


Grazie per il link e la tua risposta. Fai notare che gli elenchi non sono veramente eterogenei ma tipizzati in modo debole. Non vedo l'ora di leggere quel giornale (probabilmente domani, stasera mi
procurerò

5

Per farla breve, contenitori eterogenei scambiano le prestazioni di runtime per flessibilità. Se vuoi avere un “elenco di cose” senza riguardo al particolare tipo di cose, l'eterogeneità è la strada da percorrere. I lisp sono tipicamente tipicamente dinamici, e quasi tutto è comunque un contro-elenco di valori inscatolati, quindi ci si aspetta un piccolo calo delle prestazioni. Nel mondo Lisp, la produttività del programmatore è considerata più importante delle prestazioni di runtime.

In un linguaggio tipizzato in modo dinamico, i contenitori omogenei avrebbero effettivamente un leggero sovraccarico rispetto a quelli eterogenei, poiché tutti gli elementi aggiunti dovrebbero essere controllati.

La tua intuizione sulla scelta di una migliore struttura di dati è al punto. In generale, più contratti puoi mettere in atto sul tuo codice, più sai come funziona e più affidabile, gestibile, ecc. diventa. Tuttavia, a volte vuoi davvero un contenitore eterogeneo e dovresti averne uno se ne hai bisogno.


1
"Tuttavia, a volte vuoi davvero un contenitore eterogeneo e dovresti avere il permesso di averne uno se ne hai bisogno." - Perché però? Questa è la mia domanda Perché mai dovresti semplicemente raggruppare un mucchio di dati in un elenco casuale?
Jetti,

@Jetti: supponi di avere un elenco di impostazioni inserite dall'utente di vari tipi. Potresti creare un'interfaccia IUserSettinge implementarla più volte oppure crearne una generica UserSetting<T>, ma uno dei problemi con la tipizzazione statica è che stai definendo un'interfaccia prima di sapere esattamente come verrà utilizzata. Le cose che fai con le impostazioni dei numeri interi sono probabilmente molto diverse da quelle che fai con le impostazioni delle stringhe, quindi quali operazioni hanno senso mettere in un'interfaccia comune? Fino a quando non lo sai per certo, è meglio usare giudiziosamente la digitazione dinamica e renderla concreta in seguito.
Jon Purdy,

Vedi che è lì che incontro problemi. A me sembra un cattivo design, fare qualcosa prima che tu sappia cosa farà / sarà usato. Inoltre, in tal caso è possibile creare l'interfaccia con un valore restituito dall'oggetto. Fa la stessa cosa dell'elenco eterogeneo ma è più chiaro e più facile da correggere una volta che si è certi di quali siano i tipi utilizzati nell'interfaccia.
Jetti,

@Jetti: Questo è essenzialmente lo stesso problema, in primo luogo - una classe base universale non dovrebbe esistere in primo luogo perché, indipendentemente dalle operazioni che definisce, ci sarà un tipo per cui quelle operazioni non hanno senso. Ma se C # semplifica l'uso di un objectanziché di un dynamic, allora sicuramente usa il primo.
Jon Purdy,

1
@Jetti: questo è il polimorfismo. L'elenco contiene un numero di oggetti "eterogenei" anche se possono essere sottoclassi di una singola superclasse. Da un punto di vista Java, è possibile ottenere correttamente le definizioni di classe (o interfaccia). Per altre lingue (LISP, Python, ecc.) Non c'è alcun vantaggio nel rendere tutte le dichiarazioni giuste, poiché non c'è differenza pratica di implementazione.
S.Lott

2

Nei linguaggi funzionali (come lisp), si utilizza la corrispondenza dei modelli per determinare cosa succede a un particolare elemento in un elenco. L'equivalente in C # sarebbe una catena di istruzioni if ​​... elseif che controllano il tipo di un elemento ed eseguono un'operazione basata su quello. Inutile dire che la corrispondenza del modello funzionale è più efficiente della verifica del tipo di runtime.

L'uso del polimorfismo sarebbe una corrispondenza più stretta con la corrispondenza dei modelli. Cioè, avere gli oggetti di un elenco corrisponde a un'interfaccia particolare e chiamare una funzione su quell'interfaccia per ciascun oggetto. Un'altra alternativa sarebbe quella di fornire una serie di metodi sovraccarichi che prendono come parametro uno specifico tipo di oggetto. Il metodo predefinito che accetta Object come parametro.

public class ListVisitor
{
  public void DoSomething(IEnumerable<dynamic> list)
  {
    foreach(dynamic obj in list)
    {
       DoSomething(obj);
    }
  }

  public void DoSomething(SomeClass obj)
  {
    //do something with SomeClass
  }

  public void DoSomething(AnotherClass obj)
  {
    //do something with AnotherClass
  }

  public void DoSomething(Object obj)
  {
    //do something with everything els
  }
}

Questo approccio fornisce un'approssimazione della corrispondenza del modello di Lisp. Il modello di visitatore (come implementato qui, è un ottimo esempio di utilizzo per elenchi eterogenei). Un altro esempio potrebbe essere l'invio di messaggi in cui, ci sono listener per determinati messaggi in una coda prioritaria e usando una catena di responsabilità, il dispatcher passa il messaggio e il primo gestore che corrisponde al messaggio lo gestisce.

Il rovescio della medaglia sta notificando a tutti coloro che si registrano per un messaggio (ad esempio il pattern Aggregator di eventi comunemente usato per l'accoppiamento libero di ViewModels nel pattern MVVM). Uso il seguente costrutto

IDictionary<Type, List<Object>>

L'unico modo per aggiungere al dizionario è una funzione

Register<T>(Action<T> handler)

(e l'oggetto è in realtà un WeakReference per il gestore passato). Quindi qui DEVO usare List <Object> perché in fase di compilazione, non so quale sarà il tipo chiuso. In Runtime, tuttavia, posso far valere che sarà quel Tipo a essere la chiave del dizionario. Quando voglio lanciare l'evento che chiamo

Send<T>(T message)

e di nuovo risolvo l'elenco. Non c'è alcun vantaggio nell'usare Elenco <dinamico> perché devo comunque lanciarlo. Quindi, come vedi, ci sono dei meriti per entrambi gli approcci. Se hai intenzione di inviare un oggetto in modo dinamico usando l'overload del metodo, dinamico è il modo per farlo. Se sei FORZATO a lanciare indipendentemente, potresti anche usare Object.


Con la corrispondenza dei modelli, i casi sono (di solito - almeno in ML e Haskell) messi in pietra dalla dichiarazione del tipo di dati corrispondente. Le liste che contengono tali tipi non sono neppure eterogenee.

Non sono sicuro di ML e Haskell, ma Erlang può eguagliare qualsiasi cosa significhi, se sei arrivato qui perché non sono state soddisfatte altre partite, fallo.
Michael Brown,

@MikeBrown - Questo è bello, ma non copre perché uno dovrebbe utilizzare gli elenchi eterogenee e non sarebbe sempre lavorare con List <dinamica>
Jetti

4
In C #, i sovraccarichi vengono risolti al momento della compilazione . Così, il vostro codice di esempio sarà sempre chiamare DoSomething(Object)(almeno quando si utilizza objectnel foreachciclo, dynamicè una cosa del tutto diversa).
Heinzi,

@Heinzi, hai ragione ... Ho sonno oggi: P riparato
Michael Brown,

0

Hai ragione sul fatto che l'eterogeneità comporta un sovraccarico di runtime, ma soprattutto indebolisce le garanzie in fase di compilazione fornite dal typechecker. Anche così, ci sono alcuni problemi in cui le alternative sono ancora più costose.

Nella mia esperienza, gestendo byte non elaborati tramite file, socket di rete, ecc., Si verificano spesso problemi di questo tipo.

Per fare un esempio reale, prendere in considerazione un sistema per il calcolo distribuito utilizzando i futures . Un lavoratore su un singolo nodo può generare lavoro di qualsiasi tipo serializzabile, producendo un futuro di quel tipo. Dietro le quinte, il sistema invia il lavoro a un peer, quindi salva un record che associa quell'unità di lavoro al futuro particolare che deve essere compilato una volta che la risposta a quell'opera ritorna.

Dove possono essere conservati questi registri? Intuitivamente, quello che vuoi è qualcosa come un Dictionary<WorkId, Future<TValue>>, ma questo ti limita a gestire un solo tipo di futures nell'intero sistema. Il tipo più adatto è Dictionary<WorkId, Future<dynamic>>, poiché il lavoratore può lanciare il tipo appropriato quando forza il futuro.

Nota : questo esempio viene dal mondo Haskell in cui non abbiamo sottotipi. Non sarei sorpreso se ci fosse una soluzione più idiomatica per questo particolare esempio in C #, ma si spera sia ancora illustrativo.


0

ISTR afferma che Lisp non ha strutture dati diverse da un elenco, quindi se hai bisogno di qualsiasi tipo di oggetto dati aggregato, dovrà essere un elenco eterogeneo. Come altri hanno sottolineato, sono anche utili per serializzare i dati per la trasmissione o l'archiviazione. Una caratteristica interessante è che sono anche a tempo indeterminato, quindi puoi usarli in un sistema basato su un'analogia di tubi e filtri e fare in modo che le fasi di elaborazione successive aumentino o correggano i dati senza richiedere un oggetto dati fisso o una topologia del flusso di lavoro .

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.