Description
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.