Existe um objetivo específico para listas heterogêneas?

13

Como tenho experiência em C # e Java, estou acostumado a minhas listas serem homogêneas, e isso faz sentido para mim. Quando comecei a pegar o Lisp, notei que as listas podem ser heterogêneas. Quando comecei a brincar com a dynamicpalavra - chave em C #, notei que, a partir do C # 4.0, também havia listas heterogêneas:

List<dynamic> heterogeneousList

Minha pergunta é qual é o objetivo? Parece que uma lista heterogênea terá muito mais sobrecarga ao processar e, se você precisar armazenar tipos diferentes em um único local, poderá precisar de uma estrutura de dados diferente. Minha ingenuidade está criando uma cara feia ou há realmente momentos em que é útil ter uma lista heterogênea?

Jetti
fonte
1
Você quis dizer ... notei que as listas podem ser heterogêneas ...?
World Engineer
Qual é a List<dynamic>diferença (para sua pergunta) de simplesmente fazer List<object>?
Peter K.
@WorldEngineer sim, eu faço. Eu atualizei minha postagem. Obrigado!
217 Jetti
@PeterK. Eu acho que para o uso diário, não há nenhuma diferença. Contudo, nem todo tipo em C # deriva de System.Object, portanto, haveria casos de borda em que existem diferenças.
1128 Jetti

Respostas:

16

O artigo Strongly Type Heterogenous Collections de Oleg Kiselyov, Ralf Lämmel e Keean Schupke contém não apenas uma implementação de listas heterogêneas em Haskell, mas também um exemplo motivador de quando, por que e como você usaria HLists. Em particular, eles o estão usando para acessar o banco de dados verificado em tempo de compilação com segurança de tipo. (Pense no LINQ, de fato, o artigo que eles estão fazendo referência é o artigo de Haskell de Erik Meijer et al. Que levou ao LINQ.)

Citando o parágrafo introdutório do documento HLists:

Aqui está uma lista aberta de exemplos típicos que exigem coleções heterogêneas:

  • Uma tabela de símbolos que deve armazenar entradas de diferentes tipos é heterogênea. É um mapa finito, onde o tipo de resultado depende do valor do argumento.
  • Um elemento XML é digitado de forma heterogênea. De fato, os elementos XML são coleções aninhadas que são restringidas por expressões regulares e pela propriedade 1-ambiguidade.
  • Cada linha retornada por uma consulta SQL é um mapa heterogêneo dos nomes das colunas para as células. O resultado de uma consulta é um fluxo homogêneo de linhas heterogêneas.
  • Adicionar um sistema de objetos avançado a uma linguagem funcional requer coleções heterogêneas de um tipo que combina registros extensíveis com subtipagem e uma interface de enumeração.

Observe que os exemplos que você deu na sua pergunta realmente não são listas heterogêneas no sentido em que a palavra é comumente usada. São listas fracamente tipadas ou não tipadas . De fato, são na verdade listas homogêneas , pois todos os elementos são do mesmo tipo: objectou dynamic. Você é forçado a executar conversões ou instanceoftestes não verificados ou algo parecido, a fim de poder trabalhar de maneira significativa com os elementos, o que os torna fracamente digitados.

Jörg W Mittag
fonte
Obrigado pelo link e sua resposta. Você enfatiza que as listas não são realmente heterogêneas, mas de tipo fraco. Estou ansioso para ler esse papel (provavelmente amanhã, tenho me uma noite de médio prazo :))
Jetti
5

Recipientes heterogêneos, curtos e longos, trocam o desempenho do tempo de execução por flexibilidade. Se você deseja ter uma "lista de itens" sem considerar o tipo específico de itens, a heterogeneidade é o caminho a seguir. Os lisps são tipicamente dinamicamente digitados, e quase tudo é uma lista de contras de valores em caixa de qualquer maneira, portanto, o pequeno resultado de desempenho é esperado. No mundo Lisp, a produtividade do programador é considerada mais importante que o desempenho do tempo de execução.

Em uma linguagem de tipo dinâmico, homogêneo contêineres teriam uma sobrecarga leve em comparação com os heterogêneos, porque todos os elementos adicionados precisariam ser verificados quanto ao tipo.

Sua intuição sobre a escolha de uma melhor estrutura de dados está no ponto. De um modo geral, quanto mais contratos você pode estabelecer no seu código, mais você sabe sobre como ele funciona e mais confiável, sustentável, etc. se torna. No entanto, às vezes você realmente deseja um contêiner heterogêneo e deve ter um se precisar.

Jon Purdy
fonte
1
"No entanto, às vezes você realmente quer um contêiner heterogêneo e deve ter um se precisar." - Porquê? Essa é a minha pergunta. Por que você precisaria apenas colocar um monte de dados em uma lista aleatória?
1128 Jetti
@Jetti: Digamos que você tenha uma lista de configurações inseridas pelo usuário de vários tipos. Você pode criar uma interface IUserSettinge implementá-la várias vezes ou criar um genérico UserSetting<T>, mas um dos problemas com a digitação estática é que você está definindo uma interface antes de saber exatamente como ela será usada. As coisas que você faz com as configurações de número inteiro provavelmente são muito diferentes das que você faz com as configurações de string, então quais operações fazem sentido colocar em uma interface comum? Até que você tenha certeza, é melhor usar criteriosamente a digitação dinâmica e torná-la concreta mais tarde.
Jon Purdy
Veja que é onde eu encontro problemas. Para mim, isso parece um design ruim, criando algo antes que você saiba o que ele fará / será usado. Além disso, nesse caso, você pode fazer a interface com um valor de retorno do objeto. Faz o mesmo que a lista heterogênea, mas é mais clara e mais fácil de corrigir quando você sabe ao certo quais são os tipos usados ​​na interface.
1112 Jetti
@Jetti: Esse é essencialmente o mesmo problema - uma classe base universal não deveria existir em primeiro lugar porque, independentemente das operações que ela define, haverá um tipo para o qual essas operações não fazem sentido. Mas se o C # facilita o uso de um, em objectvez de um dynamic, use o primeiro.
21412 Jon Purdy
1
@Jetti: É sobre isso que se trata o polimorfismo. A lista contém vários objetos "heterogêneos", embora possam ser subclasses de uma única superclasse. Do ponto de vista Java, você pode obter as definições de classe (ou interface) corretas. Para outras linguagens (LISP, Python, etc.), não há benefício em obter todas as declarações corretas, pois não há diferença prática na implementação.
S.Lott
2

Em linguagens funcionais (como lisp), você usa a correspondência de padrões para determinar o que acontece com um elemento específico em uma lista. O equivalente em C # seria uma cadeia de instruções if ... elseif que verificam o tipo de um elemento e executam uma operação com base nisso. Desnecessário dizer que a correspondência de padrões funcionais é mais eficiente que a verificação do tipo de tempo de execução.

Usar polimorfismo seria uma correspondência mais próxima da correspondência de padrões. Ou seja, fazer com que os objetos de uma lista correspondam a uma interface específica e chame uma função nessa interface para cada objeto. Outra alternativa seria fornecer uma série de métodos sobrecarregados que usem um tipo de objeto específico como parâmetro. O método padrão, tendo Object como seu parâmetro.

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
  }
}

Essa abordagem fornece uma aproximação à correspondência de padrões Lisp. O padrão de visitante (conforme implementado aqui, é um ótimo exemplo de uso para listas heterogêneas). Outro exemplo seria o envio de mensagens onde, há ouvintes para determinadas mensagens em uma fila prioritária e, usando cadeia de responsabilidade, o expedidor passa a mensagem e o primeiro manipulador que corresponde à mensagem lida com ela.

O outro lado é notificar todos os que se registram para receber uma mensagem (por exemplo, o padrão Agregador de Eventos normalmente usado para acoplamento flexível de ViewModels no padrão MVVM). Eu uso a seguinte construção

IDictionary<Type, List<Object>>

A única maneira de adicionar ao dicionário é uma função

Register<T>(Action<T> handler)

(e o objeto é realmente um WeakReference para o manipulador passado). Então, aqui eu tenho que usar a lista <object> porque, em tempo de compilação, não sei qual será o tipo fechado. No tempo de execução, no entanto, posso garantir que esse tipo seja a chave do dicionário. Quando eu quero disparar o evento que eu chamo

Send<T>(T message)

e novamente eu resolvo a lista. Não há vantagem em usar a Lista <dinâmica> porque eu preciso convertê-lo de qualquer maneira. Então, como você vê, há méritos nas duas abordagens. Se você vai despachar dinamicamente um objeto usando sobrecarga de método, dinâmico é a maneira de fazê-lo. Se você é forçado a transmitir independentemente, pode usar Object.

Michael Brown
fonte
Com a correspondência de padrões, os casos (geralmente - pelo menos em ML e Haskell) são definidos em pedra pela declaração do tipo de dados correspondente. As listas que contêm esses tipos também não são heterogêneas.
Não tenho certeza sobre ML e Haskell, mas Erlang pode corresponder a qualquer meio que, se você chegou aqui porque nenhuma outra partida foi satisfeita, faça isso.
Michael Brown
@MikeBrown - Isso é bom, mas não cobre por que alguém iria usar listas heterogêneos e não iria trabalhar sempre com List <dinâmico>
Jetti
4
Em C #, as sobrecargas são resolvidas em tempo de compilação . Assim, seu código de exemplo sempre chamará DoSomething(Object)(pelo menos ao usar objectno foreachloop; dynamicé uma coisa completamente diferente).
Heinzi
@Heinzi, você está certo ... Estou com sono hoje: P fixo
Michael Brown
0

Você está certo de que a heterogeneidade carrega sobrecarga no tempo de execução, mas o mais importante é que enfraquece as garantias em tempo de compilação fornecidas pelo verificador de máquina. Mesmo assim, existem alguns problemas em que as alternativas são ainda mais caras.

Na minha experiência, ao lidar com bytes não processados ​​por meio de arquivos, soquetes de rede etc., você costuma encontrar esses problemas.

Para dar um exemplo real, considere um sistema para computação distribuída usando futuros . Um trabalhador em um nó individual pode gerar trabalhos de qualquer tipo serializável, gerando um futuro desse tipo. Nos bastidores, o sistema envia o trabalho para um colega e salva um registro associando essa unidade de trabalho ao futuro específico que deve ser preenchido assim que a resposta a esse trabalho retornar.

Onde esses registros podem ser mantidos? Intuitivamente, o que você deseja é algo como a Dictionary<WorkId, Future<TValue>>, mas isso limita você a gerenciar apenas um tipo de futuro em todo o sistema. O tipo mais adequado é Dictionary<WorkId, Future<dynamic>>que o trabalhador pode converter para o tipo apropriado quando força o futuro.

Nota : Este exemplo vem do mundo Haskell, onde não temos subtipo. Eu não ficaria surpreso se houvesse uma solução mais idiomática para este exemplo específico em C #, mas espero que ainda seja ilustrativo.

Adão
fonte
0

ISTR que Lisp não tem quaisquer outros do que uma lista de estruturas de dados, por isso, se você precisar de qualquer tipo de objeto de dados agregados, que vai ter que ser uma lista heterogênea. Como outros já apontaram, eles também são úteis para serializar dados para transmissão ou armazenamento. Um recurso interessante é que eles também são abertos, para que você possa usá-los em um sistema baseado em uma analogia de tubos e filtros e ter etapas de processamento sucessivas para aumentar ou corrigir os dados sem exigir um objeto de dados fixo ou uma topologia de fluxo de trabalho .

TMN
fonte