Alternar ou um dicionário ao atribuir a um novo objeto

12

Recentemente, passei a preferir mapear relacionamentos 1-1 usando em Dictionariesvez de Switchinstruções. Acho que é um pouco mais rápido escrever e mais fácil processar mentalmente. Infelizmente, ao mapear para uma nova instância de um objeto, não quero defini-lo assim:

var fooDict = new Dictionary<int, IBigObject>()
{
    { 0, new Foo() }, // Creates an instance of Foo
    { 1, new Bar() }, // Creates an instance of Bar
    { 2, new Baz() }  // Creates an instance of Baz
}

var quux = fooDict[0]; // quux references Foo

Dada essa construção, desperdicei ciclos de CPU e memória criando 3 objetos, fazendo o que seus construtores pudessem conter, e acabei usando apenas um deles. Também acredito que o mapeamento de outros objetos fooDict[0]nesse caso fará com que eles façam referência à mesma coisa, em vez de criar uma nova instância Fooconforme pretendido. Uma solução seria usar um lambda:

var fooDict = new Dictionary<int, Func<IBigObject>>()
{
    { 0, () => new Foo() }, // Returns a new instance of Foo when invoked
    { 1, () => new Bar() }, // Ditto Bar
    { 2, () => new Baz() }  // Ditto Baz
}

var quux = fooDict[0](); // equivalent to saying 'var quux = new Foo();'

Isso está chegando a um ponto em que é muito confuso? É fácil perder isso ()no final. Ou o mapeamento para uma função / expressão é uma prática bastante comum? A alternativa seria usar um switch:

IBigObject quux;
switch(someInt)
{
    case 0: quux = new Foo(); break;
    case 1: quux = new Bar(); break;
    case 2: quux = new Baz(); break;
}

Qual invocação é mais aceitável?

  • Dicionário, para pesquisas mais rápidas e menos palavras-chave (maiúsculas e minúsculas)
  • Alternar: Mais comumente encontrado no código, não requer o uso de um objeto Func <> para indireção.
KChaloux
fonte
2
sem o lambda, a mesma instância será retornada toda vez que você fizer a pesquisa com a mesma chave (como em fooDict[0] is fooDict[0]). tanto com o lambda eo interruptor este não é o caso
catraca aberração
@ratchetfreak Sim, eu realmente percebi isso enquanto estava digitando o exemplo. Eu acho que tomei nota em algum lugar.
KChaloux
1
Eu acho que o fato de você explicitamente colocá-lo em uma constante significa que você precisa que o objeto criado seja mutável. Mas se um dia você puder torná-los imutáveis, retornar o objeto diretamente será a melhor solução. Você pode colocar o dict em um campo const e apenas incorrer no custo de criação uma vez em todo o aplicativo.
Laurent Bourgault-Roy

Respostas:

7

Essa é uma visão interessante do padrão de fábrica . Eu gosto da combinação do dicionário e da expressão lambda; isso me fez olhar para aquele contêiner de uma nova maneira.

Estou ignorando a preocupação em sua pergunta sobre os ciclos da CPU, como você mencionou nos comentários que a abordagem não lambda não fornece o que você precisa.

Acho que qualquer uma das abordagens (switch vs. Dictionary + lambda) vai ficar bem. A única limitação é que, usando o Dicionário, você está limitando os tipos de entradas que você pode receber para gerar a classe retornada.

O uso de uma instrução switch forneceria mais flexibilidade nos parâmetros de entrada. No entanto, se isso for um problema, você poderá agrupar o Dicionário dentro de um método e obter o mesmo resultado final.

Se for novo para sua equipe, comente o código e explique o que está acontecendo. Peça uma revisão do código da equipe, acompanhe-os sobre o que foi feito e informe-os. Fora isso, parece bom.


fonte
Infelizmente, há cerca de um mês, minha equipe consistia apenas em mim (o líder desistiu). Eu não pensei em sua relevância para o padrão de fábrica. Essa é uma observação clara, na verdade.
KChaloux
1
@KChaloux: Claro, se você estivesse usando apenas o padrão Factory Method, o seu case 0: quux = new Foo(); break;torna-se case 0: return new Foo();o que é francamente tão fácil de escrever e muito mais fácil de ler do que{ 0, () => new Foo() }
PDR
@pdr Isso já foi mostrado em alguns lugares no código. Provavelmente, há uma boa razão para criar um método de fábrica no objeto que inspirou essa pergunta, mas achei que era interessante o suficiente perguntar por si próprio.
KChaloux
1
@KChaloux: Confesso que não estou interessado na recente obsessão de substituir switch / case por um dicionário. Ainda não vi um caso em que simplificar e isolar o comutador em seu próprio método não teria sido mais eficaz.
Pd
@pdr Obsessão é uma palavra forte para usar aqui. Mais consideração ao decidir como lidar com mapeamentos pontuais entre valores. Concordo que, nos casos em que é repetido, é melhor isolar um método criacional.
KChaloux 18/10/12
7

O C # 4.0 fornece a Lazy<T>classe, que é semelhante à sua segunda solução, mas grita "Inicialização lenta" mais explicitamente.

var fooDict = new Dictionary<int, Lazy<IBigObject>>()
{
    { 0, new Lazy(() => new Foo()) }, // Returns a new instance of Foo when invoked
    { 1, new Lazy(() => new Bar()) }, // Ditto Bar
    { 2, new Lazy(() => new Baz()) }  // Ditto Baz
}
Avner Shahar-Kashtan
fonte
Ooh, eu não sabia disso.
KChaloux
Oh, isso é legal!
Laurent Bourgault-Roy
2
No entanto, uma vez que o Lazy.Value é chamado, ele usa a mesma instância por toda a vida útil. Veja Inicialização preguiçosa
Dan Lyons
Obviamente, caso contrário, não seria uma inicialização lenta, apenas a reinicialização toda vez.
Avner Shahar-Kashtan
O OP afirma que ele precisa criar novas instâncias a cada vez. A segunda solução com lambdas e a terceira solução com um switch fazem isso, enquanto a primeira solução e a implementação Lazy <T> não.
Dan Lyons
2

Estilisticamente, acho que a legibilidade é igual entre eles. É mais fácil fazer injeção de dependência com o Dictionary.

Não esqueça que você deve verificar se a chave existe ao usar o Dictionarye deve fornecer um fallback, se não existir.

Eu preferiria a switchinstrução para caminhos de código estático e Dictionarypara caminhos de código dinâmico (onde você pode adicionar ou remover entradas). O compilador pode executar algumas otimizações estáticas com o switchque não pode com o Dictionary.

Curiosamente, esse Dictionarypadrão é o que as pessoas às vezes fazem no Python, porque o Python não possui a switchdeclaração. Caso contrário, eles usam cadeias if-else.

M. Dudley
fonte
1

Em geral, eu preferiria nenhum.

Tudo o que está consumindo isso deve funcionar com a Func<int, IBigObject>. Em seguida, a fonte do seu mapeamento pode ser um dicionário ou um método que possui uma instrução switch, uma chamada de serviço da web ou alguma pesquisa de arquivo ... o que seja.

Quanto à implementação, eu preferiria o Dicionário, pois é mais facilmente refatorado de 'dicionário de código rígido, chave de pesquisa, retornar resultado' para 'carregar dicionário do arquivo, chave de pesquisa, retornar resultado'.

Telastyn
fonte