Search code examples
c#.netdictionarybase-class-library

Possible to add an element to a .NET Dictionary skipping the internal ContainsKey() call


I know about Dictionary<K,V>.TryAdd() method added in .NET Core 2.0 that improves performance by checking only once if the dictionary contains the key before adding the element unlike in:

if(!dico.ContainsKey(key)) { dico.Add(key,val); } // .Add() calls ContainsKey() a second time

However for performance reason I'd like to lazy build val only if !dico.ContainsKey(key):

if(!dico.ContainsKey(key)) { dico.Add(key, new Value()); }

In this situation TryAdd() degrades performance since the value is not lazy built.

dico.TryAdd(key,new Value());

Is there a way to both have a single ContainsKey() call AND lazy build the value? Something like AddWithNoContainsKeyCheck():

if(!dico.ContainsKey(key)) { dico.AddWithNoContainsKeyCheck(key, new Value()); }

Solution

  • Yes, it is possible, by using the advanced API CollectionsMarshal.GetValueRefOrAddDefault, available from .NET 6 and later:

    /// <summary>
    /// Uses the specified function to add a key/value pair to the dictionary,
    /// if the key does not already exist.
    /// </summary>
    public static bool TryAdd<TKey, TValue>(
        this Dictionary<TKey, TValue> dictionary,
        TKey key,
        Func<TKey, TValue> valueFactory,
        out TValue value) where TKey : notnull
    {
        ArgumentNullException.ThrowIfNull(dictionary);
        ArgumentNullException.ThrowIfNull(valueFactory);
    
        ref TValue valueRef = ref CollectionsMarshal
            .GetValueRefOrAddDefault(dictionary, key, out bool exists);
        if (!exists)
        {
            try { valueRef = valueFactory(key); }
            catch { dictionary.Remove(key); throw; }
            value = valueRef;
            return true;
        }
        value = valueRef; // It was `value = default` in the original answer
        return false;
    }
    

    The CollectionsMarshal.GetValueRefOrAddDefault returns a reference to the value stored in the dictionary. In case the valueFactory fails, it is important to remove the newly added key. Otherwise a key with a default(TValue) will be inadvertently added in the dictionary.

    Note: The extension method TryAdd in this answer offers the same functionality with the more convenient and more fluent GetOrAdd. The bool return value (conveying the information if the value was created or already existed) is rarely needed in practice.