¿Por qué Dictionary no tiene AddRange?

115

El título es lo suficientemente básico, ¿por qué no puedo:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic.AddRange(MethodThatReturnAnotherDic());
Custodio
fuente
2
Hay muchos que no tienen AddRange, lo que siempre me ha desconcertado. Como Colección <>. Siempre parecía extraño que List <> lo tuviera, pero no Collection <> u otros objetos IList e ICollection.
Tim
39
Voy a sacar un Eric Lippert aquí: "porque nadie diseñó, especificó, implementó, probó, documentó y envió esa función".
Gabe Moothart
3
@ Gabe Moothart - eso es exactamente lo que había asumido. Me encanta usar esa línea con otras personas. Aunque lo odian. :)
Tim
2
@GabeMoothart, ¿no sería más sencillo decir "Porque no puedes" o "Porque no es así" o incluso "Porque"? - Supongo que no es tan divertido decirlo o algo así. --- Mi pregunta de seguimiento a su respuesta (supongo que está citada o parafraseada) es "¿Por qué nadie diseñó, especificó, implementó, probó, documentó y envió esa característica?", A lo que muy bien podría estar forzado a responder con "Porque nadie lo hizo", que está a la par con las respuestas que sugerí anteriormente. La única otra opción que puedo imaginar no es sarcástica y es lo que realmente se estaba preguntando el OP.
Code Jockey

Respuestas:

76

Un comentario a la pregunta original lo resume bastante bien:

porque nadie diseñó, especificó, implementó, probó, documentó y envió esa característica. - @Gabe Moothart

¿Como qué? Bueno, probablemente porque el comportamiento de combinar diccionarios no se puede razonar de una manera que se ajuste a las pautas del Framework.

AddRangeno existe porque un rango no tiene ningún significado para un contenedor asociativo, ya que el rango de datos permite entradas duplicadas. Por ejemplo, si tiene una IEnumerable<KeyValuePair<K,T>>colección que no protege contra entradas duplicadas.

El comportamiento de agregar una colección de pares clave-valor, o incluso fusionar dos diccionarios, es sencillo. Sin embargo, el comportamiento de cómo lidiar con múltiples entradas duplicadas no lo es.

¿Cuál debería ser el comportamiento del método cuando se trata de un duplicado?

Hay al menos tres soluciones en las que puedo pensar:

  1. lanzar una excepción para la primera entrada que es un duplicado
  2. lanzar una excepción que contiene todas las entradas duplicadas
  3. Ignorar duplicados

Cuando se lanza una excepción, ¿cuál debería ser el estado del diccionario original?

Addcasi siempre se implementa como una operación atómica: tiene éxito y actualiza el estado de la colección, o falla, y el estado de la colección no cambia. Como AddRangepuede fallar debido a errores duplicados, la forma de mantener su comportamiento consistente con Addsería hacerlo también atómico lanzando una excepción en cualquier duplicado, y dejar el estado del diccionario original sin cambios.

Como consumidor de API, sería tedioso tener que eliminar iterativamente elementos duplicados, lo que implica que AddRangedebería lanzar una única excepción que contenga todos los valores duplicados.

La elección luego se reduce a:

  1. Lanza una excepción con todos los duplicados, dejando solo el diccionario original.
  2. Ignore los duplicados y continúe.

Hay argumentos para respaldar ambos casos de uso. Para hacer eso, ¿agregas una IgnoreDuplicatesbandera a la firma?

La IgnoreDuplicatesbandera (cuando se establece en verdadero) también proporcionaría una aceleración significativa, ya que la implementación subyacente omitiría el código para la verificación duplicada.

Así que ahora, tiene una bandera que le permite AddRangeadmitir ambos casos, pero tiene un efecto secundario no documentado (que es algo que los diseñadores de Framework trabajaron muy duro para evitar).

Resumen

Como no existe un comportamiento claro, coherente y esperado cuando se trata de tratar con duplicados, es más fácil no tratarlos todos juntos y no proporcionar el método para empezar.

Si tiene que fusionar diccionarios continuamente, por supuesto, puede escribir su propio método de extensión para fusionar diccionarios, que se comportará de una manera que funcione para su (s) aplicación (es).

Alan
fuente
37
Totalmente falso, un diccionario debería tener un AddRange (IEnumerable <KeyValuePair <K, T >> Values)
Gusman
19
Si puede tener Agregar, también debería poder agregar varios. El comportamiento cuando intenta agregar elementos con claves duplicadas debe ser el mismo que cuando agrega una sola clave duplicada.
Uriah Blatherwick
5
AddMultiplees diferente a AddRange, independientemente de que su implementación sea inestable: ¿Lanzas una excepción con una matriz de todas las claves duplicadas? ¿O lanza una excepción en la primera clave duplicada que encuentra? ¿Cuál debería ser el estado del diccionario si se lanza una excepción? ¿Prístino, o todas las llaves que tuvieron éxito?
Alan
3
Bien, ahora tengo que iterar manualmente mi enumerable y agregarlos individualmente, con todas las advertencias sobre los duplicados que mencionaste. ¿Cómo se resuelve algo omitirlo del marco?
doug65536
4
@ doug65536 Porque usted, como consumidor de API, ahora puede decidir lo que quiere hacer con cada individuo Add, ya sea envolver cada uno Adden a try...catchy capturar los duplicados de esa manera; o use el indexador y sobrescriba el primer valor con el valor posterior; o comprobar de forma preventiva el uso ContainsKeyantes de intentarlo Add, conservando así el valor original. Si el marco tuviera un método AddRangeo AddMultiple, la única forma sencilla de comunicar lo sucedido sería mediante una excepción, y el manejo y la recuperación involucrados no serían menos complicados.
Zev Spitz
36

Tengo alguna solución:

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

Que te diviertas.

ADMITIR
fuente
No es necesario ToList(), un diccionario es un IEnumerable<KeyValuePair<TKey,TValue>. Además, los métodos del segundo y tercer método se lanzarán si agrega un valor de clave existente. No es una buena idea, ¿estabas buscando TryAdd? Finalmente, el segundo se puede reemplazar conWhere(pair->!dic.ContainsKey(pair.Key)...
Panagiotis Kanavos
2
Ok, ToList()no es una buena solución, así que cambié el código. Puede usarlo try { mainDic.AddRange(addDic); } catch { do something }si no está seguro del tercer método. El segundo método funciona perfectamente.
ADM-IT
Hola Panagiotis Kanavos, espero que estés feliz.
ADM-IT
1
Gracias, robé esto.
metabuddy
14

En caso de que alguien se encuentre con esta pregunta como yo, es posible lograr "AddRange" mediante el uso de métodos de extensión IEnumerable:

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

El principal truco al combinar diccionarios es lidiar con las claves duplicadas. En el código de arriba está la parte .Select(grp => grp.First()). En este caso, simplemente toma el primer elemento del grupo de duplicados, pero puede implementar una lógica más sofisticada allí si es necesario.

Rafal Zajac
fuente
¿Qué pasa si dict1 no usa el comparador de igualdad predeterminado?
mjwills
Los métodos Linq le permiten pasar IEqualityComparer cuando sea relevante:var combined = dict1.Concat(dict2).GroupBy(kvp => kvp.Key, dict1.Comparer).ToDictionary(grp => grp.Key, grp=> grp.First(), dict1.Comparer);
Kyle McClellan
12

Mi conjetura es la falta de una salida adecuada para el usuario sobre lo que sucedió. Como no puede tener claves repetidas en un diccionario, ¿cómo manejaría fusionar dos diccionarios donde algunas claves se cruzan? Seguro que podría decir: "No me importa", pero eso está rompiendo la convención de devolver falso / lanzar una excepción para las claves repetidas.

Galón
fuente
5
¿En qué se diferencia de donde tiene un choque de teclas cuando llama Add, aparte de que puede suceder más de una vez? Tiraría lo mismo ArgumentExceptionque Addhace, ¿seguro?
nicodemus
1
@ nicodemus13 Sí, pero no sabrías qué tecla arrojó la excepción, solo que ALGUNA tecla fue una repetición.
Gal
1
@Gal concedido, pero podría: devolver el nombre de la clave en conflicto en el mensaje de excepción (útil para alguien que sabe lo que está haciendo, supongo ...), O podría ponerlo como (¿parte de?) paramName a la ArgumentException que arroja, OR-OR, podría crear un nuevo tipo de excepción (tal vez una opción suficientemente genérica podría ser NamedElementException??), ya sea lanzada en lugar de o como la innerException de una ArgumentException, que especifica el elemento con nombre que entra en conflicto ... algunas opciones diferentes, diría yo
Code Jockey
7

Podrías hacer esto

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

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

o use una Lista para agregar rango y / o usando el patrón anterior.

List<KeyValuePair<string, string>>
Valamas
fuente
1
Dictionary ya tiene un constructor que acepta otro diccionario .
Panagiotis Kanavos
1
OP quiere agregar un rango, no clonar un diccionario. En cuanto al nombre del método en mi ejemplo MethodThatReturnAnotherDic. Viene del OP. Por favor revise la pregunta y mi respuesta nuevamente.
Valamas
1

Si está tratando con un nuevo diccionario (y no tiene filas existentes que perder), siempre puede usar ToDictionary () de otra lista de objetos.

Entonces, en tu caso, harías algo como esto:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic = SomeList.ToDictionary(x => x.Attribute1, x => x.Attribute2);
WEFX
fuente
5
Ni siquiera necesita crear un nuevo diccionario, simplemente escribaDictionary<string, string> dic = SomeList.ToDictionary...
Panagiotis Kanavos
1

Si sabe que no va a tener claves duplicadas, puede hacer lo siguiente:

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

Lanzará una excepción si hay un par clave / valor duplicado.

No sé por qué esto no está en el marco; debiera ser. No hay incertidumbre; solo lanza una excepción. En el caso de este código, lanza una excepción.

Toddmo
fuente
¿Qué pasa si el diccionario original se creó usando var caseInsensitiveDictionary = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase);?
mjwills
1

Siéntase libre de usar un método de extensión 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
fuente
0

Aquí hay una solución alternativa usando c # 7 ValueTuples (literales 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
fuente
0

Como han mencionado otros, la razón por la Dictionary<TKey,TVal>.AddRangeque no se implementa es porque hay varias formas en las que puede querer manejar los casos en los que tiene duplicados. Este es también el caso de Collectiono interfaces tales como IDictionary<TKey,TVal>, ICollection<T>, etc.

Solo lo List<T>implementa, y notará que la IList<T>interfaz no lo hace, por las mismas razones: el comportamiento esperado al agregar un rango de valores a una colección puede variar ampliamente, dependiendo del contexto.

El contexto de su pregunta sugiere que no le preocupan los duplicados, en cuyo caso tiene una alternativa simple de un delineador, usando Linq:

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