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

139 Views Asked by At

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()); }
2

There are 2 best solutions below

15
Theodor Zoulias On BEST ANSWER

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.

0
Matthew Watson On

You can use CollectionsMarshal.GetValueRefOrAddDefault() to do this, like so:

public static class Program
{
    public static void Main()
    {
        var d = new Dictionary<int, Test>();

        ref Test? element = ref CollectionsMarshal.GetValueRefOrAddDefault(d, key:0, out bool exists);

        if (!exists)
        {
            element = new Test(42);
        }

        Console.WriteLine(d[0].Value); // Prints "42"
    }
}

public sealed class Test
{
    public Test(int value)
    {
        Console.WriteLine($"Creating Test with value = {value}");
        Value = value;
    }

    public int Value { get; }
}

This will add a null element which you must overwrite with the actual value after determining that the element did not previously exist.

Note that this is a bit dodgy because we've defined the dictionary as NOT containing any null values, but here we are temporarily inserting a null value (albeit one which is immediately overwritten with a non-null value). Some care and attention is required, especially if the Test constructor can throw.