Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.NET Dictionary: get or create new

I often find myself creating a Dictionary with a non-trivial value class (e.g. List), and then always writing the same code pattern when filling in data.

For example:

var dict = new Dictionary<string, List<string>>();
string key = "foo";
string aValueForKey = "bar";

That is, I want to insert "bar" into the list that corresponds to key "foo", where key "foo" might not be mapped to anything.

This is where I use the ever-repeating pattern:

List<string> keyValues;
if (!dict.TryGetValue(key, out keyValues))
  dict.Add(key, keyValues = new List<string>());
keyValues.Add(aValueForKey);

Is there a more elegant way of doing this?

Related questions that don't have answers to this question:

  • Is there an IDictionary implementation that returns null on missing key instead of throwing?
  • Find-or-insert with only one lookup in c# dictionary
  • Dictionary returning a default value if the key does not exist
like image 753
Rok Strniša Avatar asked Apr 24 '13 13:04

Rok Strniša


6 Answers

We have a slightly different take on this, but the effect is similar:

public static TValue GetOrCreate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key) 
    where TValue : new()
{
    if (!dict.TryGetValue(key, out TValue val))
    {
        val = new TValue();
        dict.Add(key, val);
    }

    return val;
}

Called:

var dictionary = new Dictionary<string, List<int>>();

List<int> numbers = dictionary.GetOrCreate("key");

It makes use of the generic constraint for public parameterless constructors: where TValue : new().

To help with discovery, unless the extension method is quite specific to a narrow problem, we tend to place extension methods in the namespace of the type they are extending, in this case:

namespace System.Collections.Generic

Most of the time, the person using the type has the using statement defined at the top, so IntelliSense would also find the extension methods for it defined in your code.

like image 142
Adam Houldsworth Avatar answered Sep 23 '22 04:09

Adam Houldsworth


As with so many programming problems, when you find yourself doing something a lot, refactor it into a method:

public static void MyAdd<TKey, TCollection, TValue>(
    this Dictionary<TKey, TCollection> dictionary, TKey key, TValue value)
    where TCollection : ICollection<TValue>, new()
{
    TCollection collection;
    if (!dictionary.TryGetValue(key, out collection))
    {
        collection = new TCollection();
        dictionary.Add(key, collection);
    }
    collection.Add(value);
}
like image 21
Servy Avatar answered Sep 26 '22 04:09

Servy


If you use .Net Core you can use Dictionary<>.TryAdd().

var dict = new Dictionary<string, string>();
dict.TryAdd("foo", "bar"); // returns bool whether it added or not feel free to ignore.
var myValue = dict["foo"];
like image 38
Alexander Oh Avatar answered Sep 26 '22 04:09

Alexander Oh


For further readers, here are some extensions in every flavour I thought fit. You could also do something with an out parameter if you need to check if you have added a value but i think you can use containskey or something already for that.

You can use GetOrAddNew to retrieve an item, or create and add it to the dict. You can use the various overloads of GetOrAdd to add a new value. This could be the default so e.g. NULL or 0 but you can also provide a lambda to construct an object for you, with any kind of constructor arguments you'd like.

var x = new Dictionary<string, int>();
var val = x.GetOrAdd("MyKey", (dict, key) => dict.Count + 2);
var val2 = x.GetOrAdd("MyKey", () => Convert.ToInt32("2"));
var val3 = x.GetOrAdd("MyKey", 1);
    /// <summary>
    /// Extensions for dealing with <see cref="Dictionary{TKey,TValue}"/>
    /// </summary>
    public static class DictionaryExtensions
    {
        public static TValue GetOrAddNew<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue defaultValue = default) 
            where TValue : new() 
            => dict.GetOrAdd(key, (values, innerKey) => EqualityComparer<TValue>.Default.Equals(default(TValue), defaultValue) ? new TValue() : defaultValue);

        public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue defaultValue = default)
            => dict.GetOrAdd(key, (values, innerKey) => defaultValue);

        public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TValue> valueProvider)
            => dict.GetOrAdd(key, (values, innerKey) => valueProvider());

        public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TKey, TValue> valueProvider)
            => dict.GetOrAdd(key, (values, innerKey) => valueProvider(key));

        public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<IDictionary<TKey, TValue>, TKey, TValue> valueProvider)
        {
            if (dict == null) throw new ArgumentNullException(nameof(dict));
            if (key == null) throw new ArgumentNullException(nameof(key));
            if (valueProvider == null) throw new ArgumentNullException(nameof(valueProvider));

            if (dict.TryGetValue(key, out var foundValue))
                return foundValue;

            dict[key] = valueProvider(dict, key);
            return dict[key];
        }
    }
like image 27
sommmen Avatar answered Sep 23 '22 04:09

sommmen


Here is a solution in case the constructor requires a parameter.

public static TValue GetOrCreate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TValue> createNew)
    {
        if (!dict.TryGetValue(key, out var val))
        {
            val = createNew();
            dict.Add(key, val);
        }

        return val;
    }

Simple to use:

MyDict.GetOrCreate(si.Id, createNew: () => new ObjectKnowingItsId(si.Id))
like image 29
HeikoG Avatar answered Sep 27 '22 04:09

HeikoG


And what about this?

var keyValues = dictionary[key] = dictionary.ContainsKey(key) ? dictionary[key] : new List<string>();
keyValues.Add(aValueForKey);
like image 39
George Avatar answered Sep 24 '22 04:09

George