0

I am trying to create a for loop that invokes a function of several instance of class A in a dictionary, and if there is no value for a key, it creates it and then invokes it. It seems to me as if there must be a way to create a value upon first access to a key.

I am currently using this code though I think it is not the best possible practice:

(dictionary[i] = dictionary.ContainsKey(arr[i]) ? dictionary[i] : new A()).Push(10);

Is there a cleaner for such a problem in C#?

5
  • dictionary[i] = dictionary[i]? Commented Dec 13, 2017 at 12:37
  • it would be good if you show complete code (at least a method - not part of it) and present desired output here as well. Otherwise it`s not clear what you want. Commented Dec 13, 2017 at 12:38
  • 1
    Possible duplicate of .NET Dictionary: get or create new Commented Dec 13, 2017 at 12:43
  • What is your definition of "cleaner"? Commented Dec 13, 2017 at 12:46
  • Using if instead of cryptic one-liner will be cleaner for example. Commented Dec 13, 2017 at 13:03

2 Answers 2

2

ConcurrentDictionary has a GetOrAdd method (and other useful methods like AddOrUpdate, TryRemove etc.). If just a plain dictionary had GetOrAdd you could use that...

Luckily, you can create an extension method in a static class which you probably should name DictionaryExtensions:

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

    if (dictionary.TryGetValue(key, out var existingValue))
        return existingValue;
    var value = valueFactory(key);
    dictionary.Add(key, value);
    return value;
}

How to use it:

dictionary.GetOrAdd(i, () => new A()).Push(10);

This version uses a value factory so that new A() is only executed in case it is required. Another ConcurrentDictionary.GetOrAdd() overload uses a value provided as parameter which you might consider as an alternative.

I find that creating extension methods like this that closely mirrors the methods on ConcurrentDictionary is very useful.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you so much for the reply! I really liked the idea of passing the initialization function into the GetOrAdd method.
1

I'd say a cleaner code would look something like this:

var key = arr[i];
var hasKey = dictionary.ContainsKey(key);

if (!hasKey)
    dictionary.Add(key, new A());

var itemToUse = dictionary[key];
itemToUse.Push(10);

Although it seems to me you are looking for something shorter. I guess what you are really asking is a short-hand method that does:

Returns the value for a given key if the key exists, else adds the key to the dictionary with some default value.

I think the above code tells a lot more about the intent, but in case you want something different, I can think of following two solutions.

The first one is an extension method for getting the item:

public static TValue Get<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue)
{
    var hasKey = dictionary.ContainsKey(key);

    if (!hasKey)
        dictionary.Add(key, defaultValue);

    return dictionary[key];
}

You would use it as:

dict.Get(arr[i], defaultValue: new A())
    .Push(10);

The second solution I can think of is a new derivative of Dictionary:

class DefaultDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
    private readonly Func<TKey, TValue> _defaultValueFactory;

    public DefaultDictionary(TValue defaultValue)
    {
        _defaultValueFactory = new Func<TKey, TValue>(x => defaultValue);
    }

    public DefaultDictionary(Func<TValue> defaultValueFactory)
    {
        _defaultValueFactory = new Func<TKey, TValue>(x => defaultValueFactory()) ?? throw new ArgumentNullException(nameof(defaultValueFactory));
    }

    public DefaultDictionary(Func<TKey, TValue> defaultValueFactory)
    {
        _defaultValueFactory = defaultValueFactory ?? throw new ArgumentNullException(nameof(defaultValueFactory));
    }

    public new TValue this[TKey key]
    {
        get
        {
            var hasKey = ContainsKey(key);

            if (!hasKey)
            {
                var defaultValue = _defaultValueFactory(key);
                Add(key, defaultValue);
            }

            return base[key];
        }
        set
        {
            base[key] = value;
        }
    }
}

The usage of this goes like:

var dictionary = new DefaultDictionary<string, A>(() => new A());
// ...
dictionary[arr[i]].Push(10);

I must warn you about something, this derivative of Dictionary hides the index operator. And since using IDictionary as types for members is a common practice (e.g. private IDictionary<string, A> dictionary as a member), you can't use the overloaded version without casting. So either cast your variable to DefaultDictionary every time you want to use the overloaded indexer, or have an interface for this new dictionary like:

interface IDefaultDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    new TValue this[TKey key] { get; set; }
}

And have your members, variables use it as their defining type:

private IDefaultDictionary<string, A> dictionary;

But this also means as a concrete class you must now use DefaultDictionary, and that's the trade-off.

2 Comments

Thank you so much for the reply! I learned alot by reading your comment and I really like your solution. In a real world problem deriving from dictionary definately looks more robust and maintainable but for my small practice code extension method seems like the better fit.
You're welcome, of course, the extension is a great way to solve the problem. I just find it more appropriate to use the indexing operator when you have some hash oriented mechanism to do gets and sets; otherwise, the extension solves the situation. Preferences :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.