Skip to content

[API Proposal]: Dictionary.GetOrAdd #116710

Closed as not planned
Closed as not planned
@verdie-g

Description

@verdie-g

Background and motivation

One pattern that I feel I write everyday, especially when replacing GroupBy for perf reason, is this

Dictionary<int, List<Blog>> blogsByUserId = [];

// ...

if (!blogsByUserId.TryGet(userId, out List<Blog> blogs))
{
    blogs = [];
}

blogs.Add(blog);

It's verbose and requires two dictionary accesses. It could be rewritten with CollectionsMarshal.GetValueRefOrAddDefault but it's an uncommon API so it requires more effort to understand for the reader

ref List<Blog>? blogs = ref CollectionsMarshal.GetValueRefOrAddDefault(blogsByUserId, userId, out bool exists);
if (!exists)
{
    blogs = [];
}

blogs.Add(blog);

A GetOrAdd method would greatly simplified that code. I understand that we want to avoid adding helpers for every possible use-cases but I believe it's such a common pattern that it deserves to be built-in.

API Proposal

The implementation would reside right next to the helper Dictionary.GetValueOrDefault.

namespace System.Collections.Generic;

public static class CollectionExtensions
{
    public static TValue GetOrAdd<TKey, TValue>(
        this Dictionary<TKey, TValue> dictionary,
        TKey key,
        Func<TKey, TValue> valueFactory)
        where TKey : notnull;

    public static TValue GetOrAdd<TKey, TValue, TArg>(
        this Dictionary<TKey, TValue> dictionary,
        TKey key,
        Func<TKey, TState, TValue> valueFactory,
        TArg factoryArgument)
        where TKey : notnull;
}

Inspired by ConcurrentDictionary<TKey,TValue>.GetOrAdd.

The implementation would leverage CollectionsMarshal.GetValueRefOrAddDefault.

API Usage

The original snippet could be rewritten with

blogsByUserId.GetOrAdd(userId, static _ => []).Add(blog);

Alternative Designs

IDictionary extension

CollectionsMarshal.GetValueRefOrAddDefault only work on Dictionary. Still, we could do a

public static TValue GetOrAdd<TKey, TValue>(
    this IDictionary<TKey, TValue> dictionary,
    TKey key,
    Func<TKey, TValue> valueFactory)
{
    if (dictionary is Dictionary<TKey, TValue> d)
    {
        ref TValue? value = ref CollectionsMarshal.GetValueRefOrAddDefault(d, key, out bool exists);
        if (!exists)
        {
            value = valueFactory(key);
        }
    }
    else
    {
        // ...
    }
}

That would have an impact of the performance.

Dictionary instance method

Sounds like a good practice to use an extension method rather than an instance one when possible.

Risks

Break existing GetOrAdd extensions

I would expect many existing implementations of GetOrAdd, see this sourcegraph query for example. This proposal would introduce a potential source breaking change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions