Por que o Dicionário não tem AddRange?

115

O título é básico o suficiente, por que não posso:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic.AddRange(MethodThatReturnAnotherDic());
Custódio
fonte
2
Há muitos que não têm AddRange, o que sempre me deixou perplexo. Como coleção <>. Sempre parecia estranho que List <> o tivesse, mas não Collection <> ou outros objetos IList e ICollection.
Tim de
39
Vou puxar um Eric Lippert aqui: "porque ninguém jamais projetou, especificou, implementou, testou, documentou e enviou esse recurso."
Gabe Moothart
3
@ Gabe Moothart - isso é exatamente o que eu presumi. Eu amo usar essa frase em outras pessoas. Eles odeiam isso. :)
Tim
2
@GabeMoothart não seria mais simples dizer "Porque você não pode" ou "Porque não faz" ou mesmo "Porque"? - Acho que não é tão divertido de dizer ou algo assim? --- Minha pergunta de acompanhamento à sua resposta (suponho que foi citada ou parafraseada) é "Por que ninguém nunca projetou, especificou, implementou, testou, documentou e enviou esse recurso?", À qual você pode muito bem estar forçado a responder "Porque ninguém fez", o que está de acordo com as respostas que sugeri anteriormente. A única outra opção que posso imaginar não é sarcástica e é o que o OP estava realmente se perguntando.
Code Jockey

Respostas:

76

Um comentário à pergunta original resume isso muito bem:

porque ninguém jamais projetou, especificou, implementou, testou, documentou e enviou esse recurso. - @Gabe Moothart

Por quê? Bem, provavelmente porque o comportamento de mesclar dicionários não pode ser fundamentado de uma maneira que se encaixe nas diretrizes do Framework.

AddRangenão existe porque um intervalo não tem nenhum significado para um contêiner associativo, já que o intervalo de dados permite entradas duplicadas. Por exemplo, se você tinha uma IEnumerable<KeyValuePair<K,T>>coleção, isso não protege contra entradas duplicadas.

O comportamento de adicionar uma coleção de pares de valores-chave ou mesmo mesclar dois dicionários é direto. O comportamento de como lidar com várias entradas duplicadas, no entanto, não é.

Qual deve ser o comportamento do método ao lidar com uma duplicata?

Posso pensar em pelo menos três soluções:

  1. lançar uma exceção para a primeira entrada que é uma duplicata
  2. lançar uma exceção que contém todas as entradas duplicadas
  3. Ignorar duplicatas

Quando uma exceção é lançada, qual deve ser o estado do dicionário original?

Addquase sempre é implementado como uma operação atômica: é bem-sucedido e atualiza o estado da coleção, ou falha, e o estado da coleção permanece inalterado. Como AddRangepode falhar devido a erros duplicados, a maneira de manter seu comportamento consistente com Addseria também torná-lo atômico, lançando uma exceção em qualquer duplicata, e deixar o estado do dicionário original inalterado.

Como um consumidor de API, seria tedioso ter que remover iterativamente os elementos duplicados, o que implica que o AddRangedeve lançar uma única exceção que contém todos os valores duplicados.

A escolha então se resume a:

  1. Lance uma exceção com todas as duplicatas, deixando o dicionário original sozinho.
  2. Ignore duplicatas e prossiga.

Existem argumentos para apoiar os dois casos de uso. Para fazer isso, você adiciona um IgnoreDuplicatessinalizador à assinatura?

O IgnoreDuplicatessinalizador (quando definido como verdadeiro) também forneceria uma velocidade significativa, pois a implementação subjacente ignoraria o código para verificação duplicada.

Agora, você tem um sinalizador que permite que o AddRangesuporte ambos os casos, mas tem um efeito colateral não documentado (que é algo que os designers do Framework trabalharam muito para evitar).

Resumo

Como não há um comportamento claro, consistente e esperado quando se trata de lidar com duplicatas, é mais fácil não lidar com todas elas juntas e não fornecer o método para começar.

Se você está continuamente tendo que mesclar dicionários, pode, é claro, escrever seu próprio método de extensão para mesclar dicionários, que se comportará de uma maneira que funcione para seu (s) aplicativo (s).

Alan
fonte
37
Totalmente falso, um dicionário deve ter um AddRange (IEnumerable <KeyValuePair <K, T >> Values)
Gusman
19
Se puder ter Adicionar, também deve poder adicionar vários. O comportamento ao tentar adicionar itens com chaves duplicadas deve ser o mesmo de quando você adiciona uma única chave duplicada.
Uriah Blatherwick
5
AddMultipleé diferente de AddRange, independentemente de sua implementação, será difícil: você lança uma exceção com um array de todas as chaves duplicadas? Ou você lança uma exceção na primeira chave duplicada que encontrar? Qual deve ser o estado do dicionário se uma exceção for lançada? Pristine, ou todas as chaves que funcionaram?
Alan
3
Ok, agora preciso iterar manualmente meu enumerável e adicioná-los individualmente, com todas as advertências sobre duplicatas que você mencionou. Como omiti-lo do framework resolve alguma coisa?
doug65536
4
@ doug65536 Porque você, como consumidor da API, agora pode decidir o que fazer com cada indivíduo Add- embrulhar cada Addum em um try...catche capturar as duplicatas dessa forma; ou use o indexador e sobrescreva o primeiro valor pelo valor posterior; ou verifique preventivamente usando ContainsKeyantes de tentar Add, preservando assim o valor original. Se a estrutura tivesse um método AddRangeou AddMultiple, a única maneira simples de comunicar o que aconteceu seria por meio de uma exceção, e o manuseio e a recuperação envolvidos não seriam menos complicados.
Zev Spitz
36

Eu tenho alguma solução:

Dictionary<string, string> mainDic = new Dictionary<string, string>() { 
    { "Key1", "Value1" },
    { "Key2", "Value2.1" },
};
Dictionary<string, string> additionalDic= new Dictionary<string, string>() { 
    { "Key2", "Value2.2" },
    { "Key3", "Value3" },
};
mainDic.AddRangeOverride(additionalDic); // Overrides all existing keys
// or
mainDic.AddRangeNewOnly(additionalDic); // Adds new keys only
// or
mainDic.AddRange(additionalDic); // Throws an error if keys already exist
// or
if (!mainDic.ContainsKeys(additionalDic.Keys)) // Checks if keys don't exist
{
    mainDic.AddRange(additionalDic);
}

...

namespace MyProject.Helper
{
  public static class CollectionHelper
  {
    public static void AddRangeOverride<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => dic[x.Key] = x.Value);
    }

    public static void AddRangeNewOnly<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => { if (!dic.ContainsKey(x.Key)) dic.Add(x.Key, x.Value); });
    }

    public static void AddRange<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => dic.Add(x.Key, x.Value));
    }

    public static bool ContainsKeys<TKey, TValue>(this IDictionary<TKey, TValue> dic, IEnumerable<TKey> keys)
    {
        bool result = false;
        keys.ForEachOrBreak((x) => { result = dic.ContainsKey(x); return result; });
        return result;
    }

    public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
    {
        foreach (var item in source)
            action(item);
    }

    public static void ForEachOrBreak<T>(this IEnumerable<T> source, Func<T, bool> func)
    {
        foreach (var item in source)
        {
            bool result = func(item);
            if (result) break;
        }
    }
  }
}

Diverta-se.

ADMITEM
fonte
Você não precisa ToList(), um dicionário é um IEnumerable<KeyValuePair<TKey,TValue>. Além disso, o segundo e o terceiro métodos serão lançados se você adicionar um valor de chave existente. Não é uma boa ideia, você estava procurando TryAdd? Finalmente, o segundo pode ser substituído porWhere(pair->!dic.ContainsKey(pair.Key)...
Panagiotis Kanavos
2
Ok, ToList()não é uma boa solução, então mudei o código. Você pode usar try { mainDic.AddRange(addDic); } catch { do something }se não tiver certeza para o terceiro método. O segundo método funciona perfeitamente.
ADM-IT de
Olá, Panagiotis Kanavos, espero que você esteja feliz.
ADM-IT de
1
Obrigado, eu roubei isso.
metabuddy
14

Caso alguém se depare com esta questão como eu - é possível obter "AddRange" usando os métodos de extensão IEnumerable:

var combined =
    dict1.Union(dict2)
        .GroupBy(kvp => kvp.Key)
        .Select(grp => grp.First())
        .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

O principal truque ao combinar dicionários é lidar com as chaves duplicadas. No código acima é a parte .Select(grp => grp.First()). Nesse caso, ele simplesmente pega o primeiro elemento do grupo de duplicatas, mas você pode implementar uma lógica mais sofisticada lá, se necessário.

Rafal Zajac
fonte
E se dict1 não usar o comparador de igualdade padrão?
mjwills,
Os métodos Linq permitem que você passe IEqualityComparer quando relevante:var combined = dict1.Concat(dict2).GroupBy(kvp => kvp.Key, dict1.Comparer).ToDictionary(grp => grp.Key, grp=> grp.First(), dict1.Comparer);
Kyle McClellan
12

Meu palpite é a falta de saída adequada para o usuário quanto ao que aconteceu. Como você não pode ter chaves repetidas em um dicionário, como você lidaria com a fusão de dois dicionários onde algumas chaves se cruzam? Claro, você poderia dizer: "Não me importo", mas isso está quebrando a convenção de retornar false / lançar uma exceção para teclas repetidas.

Garota
fonte
5
Como isso é diferente de quando você tem um conflito de teclas ao ligar Add, além de que pode acontecer mais de uma vez. Seria o mesmo ArgumentExceptionque joga Add, certo?
nicodemus13
1
@ nicodemus13 Sim, mas você não saberia qual tecla acionou a exceção, apenas que ALGUMA tecla era uma repetição.
Gal
1
@Gal concedido, mas você poderia: retornar o nome da chave conflitante na mensagem de exceção (útil para alguém que sabe o que está fazendo, suponho ...), OU você poderia colocá-la como (parte de?) argumento paramName para o ArgumentException que você lança, OR-OR, você poderia criar um novo tipo de exceção (talvez uma opção genérica o suficiente seja NamedElementException??), lançado em vez de ou como a innerException de um ArgumentException, que especifica o elemento nomeado em conflito ... algumas opções diferentes, eu diria
Code Jockey
7

Você poderia fazer isso

Dictionary<string, string> dic = new Dictionary<string, string>();
// dictionary other items already added.
MethodThatReturnAnotherDic(dic);

public void MethodThatReturnAnotherDic(Dictionary<string, string> dic)
{
    dic.Add(.., ..);
}

ou use uma lista para adicionar intervalo e / ou usando o padrão acima.

List<KeyValuePair<string, string>>
Valamas
fonte
1
O dicionário já possui um construtor que aceita outro dicionário .
Panagiotis Kanavos
1
OP deseja adicionar intervalo, não clonar um dicionário. Quanto ao nome do método no meu exemplo MethodThatReturnAnotherDic. Vem do OP. Reveja a pergunta e a minha resposta novamente.
Valamas
1

Se você estiver lidando com um novo Dicionário (e não tiver linhas existentes para perder), você sempre pode usar ToDictionary () de outra lista de objetos.

Então, no seu caso, você faria algo assim:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic = SomeList.ToDictionary(x => x.Attribute1, x => x.Attribute2);
WEFX
fonte
5
Você nem precisa criar um novo dicionário, basta escreverDictionary<string, string> dic = SomeList.ToDictionary...
Panagiotis Kanavos
1

Se você sabe que não terá chaves duplicadas, pode fazer:

dic = dic.Union(MethodThatReturnAnotherDic()).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

Ele lançará uma exceção se houver um par de chave / valor duplicado.

Não sei por que isso não está na estrutura; deveria estar. Não há incerteza; apenas lance uma exceção. No caso desse código, ele lança uma exceção.

toddmo
fonte
E se o dicionário original foi criado usando var caseInsensitiveDictionary = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase);?
mjwills
1

Sinta-se à vontade para usar o método de extensão como este:

public static Dictionary<T, U> AddRange<T, U>(this Dictionary<T, U> destination, Dictionary<T, U> source)
{
  if (destination == null) destination = new Dictionary<T, U>();
  foreach (var e in source)
    destination.Add(e.Key, e.Value);
  return destination;
}
Franziee
fonte
0

Aqui está uma solução alternativa usando c # 7 ValueTuples (literais de tupla)

public static class DictionaryExtensions
{
    public static Dictionary<TKey, TValue> AddRange<TKey, TValue>(this Dictionary<TKey, TValue> source,  IEnumerable<ValueTuple<TKey, TValue>> kvps)
    {
        foreach (var kvp in kvps)
            source.Add(kvp.Item1, kvp.Item2);

        return source;
    }

    public static void AddTo<TKey, TValue>(this IEnumerable<ValueTuple<TKey, TValue>> source, Dictionary<TKey, TValue> target)
    {
        target.AddRange(source);
    }
}

Usado como

segments
    .Zip(values, (s, v) => (s.AsSpan().StartsWith("{") ? s.Trim('{', '}') : null, v))
    .Where(zip => zip.Item1 != null)
    .AddTo(queryParams);
Anders
fonte
0

Como outros mencionaram, o motivo pelo qual Dictionary<TKey,TVal>.AddRangenão foi implementado é porque existem várias maneiras de lidar com casos em que há duplicatas. Este é também o caso para Collectionou interfaces, tais como IDictionary<TKey,TVal>, ICollection<T>, etc.

Apenas o List<T>implementa, e você notará que a IList<T>interface não, pelos mesmos motivos: o esperado comportamento ao adicionar um intervalo de valores a uma coleção pode variar amplamente, dependendo do contexto.

O contexto de sua pergunta sugere que você não está preocupado com duplicatas e, nesse caso, você tem uma alternativa simples de uma linha, usando o Linq:

MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));
Ama
fonte