Skip to content

Propose Adding a GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) to Dictionary<TKey, TValue> #15059

Closed

Description

@shmuelie

On ConcurrentDictionary<TKey, TValue> there is the very useful function TValue GetOrAdd(TKey key, Func<TKey, TValie> valueFactory). The pattern that the method represents is one that issued commonly on the "normal" Dictionary<TKey, TValue> as well. I propose adding such a method for this common use case. Besides simplifying it will also make switching between the two dictionaries simpler.

Activity

  1. JonHanna commented on Aug 18, 2015

    @JonHanna
    Contributor

    Suggested at #14676 within the comment thread too. Possible implementation at https://github.com/hackcraft/coreclr/commit/1a982053ae39ee0e418d370fdee3497ecefd0e5a

  2. shmuelie commented on Aug 18, 2015

    @shmuelie
    ContributorAuthor

    That's what I get for thinking of an idea, never getting around to suggesting it. And then remembering months later and forgetting to check again if it had been suggested. Thanks!

  3. JonHanna commented on Aug 18, 2015

    @JonHanna
    Contributor

    Never mind, I do check for duplicates and I still end up asking something that's been asked before, or submitting a PR that does the same as another.

  4. jnm2 commented on May 12, 2017

    @jnm2
    Contributor

    I assumed this meant Dictionary.GetOrAdd was added, but I'm not finding it or an IDictionary extension method. If they have been skipped, can this issue be reopened?

  5. svick commented on May 12, 2017

    @svick
    Member

    @jnm2 As far as I can tell, @SamuelEnglard closed this issue in favor of https://github.com/dotnet/corefx/issues/1942, which was about TryAdd, but also discussed GetOrAdd. But then https://github.com/dotnet/corefx/issues/1942 was closed after TryAdd was added.

    So yes, I think it makes sense to repoen this issue.

  6. jnm2 commented on May 12, 2017

    @jnm2
    Contributor

    Ugh, I can't believe this missed .NET Standard 2.0. @karelz no exceptions?

  7. reopened this on May 12, 2017
  8. jnm2 commented on May 12, 2017

    @jnm2
    Contributor

    Specifically:

    public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)
    {
        if (dictionary == null) throw new ArgumentNullException(nameof(dictionary));
    
        if (!dictionary.TryGetValue(key, out var r))
            dictionary.Add(key, r = value);
        return r;
    }
    
    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 (valueFactory == null) throw new ArgumentNullException(nameof(valueFactory));
    
        if (!dictionary.TryGetValue(key, out var r))
            dictionary.Add(key, r = valueFactory.Invoke(key));
        return r;
    }
  9. karelz commented on May 12, 2017

    @karelz
    Member

    This needs API review. I don't want to rush API reviews. That is just begging for troubles later - you miss things and then you live with your mistakes forever.

    It is convenience API, we lived without it for quite some time, we can IMO survive one more release. It is not the end of the world after all and not the last .NET Core release. It does not block scenarios.

    Probably not the answer you expected, but this is how I feel about it. We should IMO focus primarily on the really impactful things. Makes sense?

  10. jnm2 commented on May 12, 2017

    @jnm2
    Contributor

    It is indeed the answer I expected. 😄

  11. jnm2 commented on May 12, 2017

    @jnm2
    Contributor

    (Don't worry about me, I'm not the type that goes around second-guessing the team's decisions. Fyi.)

  12. karelz commented on May 12, 2017

    @karelz
    Member

    Thanks for understanding!
    IMO it is not that much about decisions, but us being open about prioritization. To ship high-quality SW, we gotta prioritize. And we gotta stabilize.

  13. jamesqo commented on Aug 1, 2017

    @jamesqo
    Contributor

    @jnm2 I don't think it would be a good idea to add GetOrAdd as an extension method on IDictionary-- it would give the impression that only 1 lookup is being made. I think it's best just to make this proposal about Dictionary<K, V>.

    @karelz This is not necessarily a convenience API. For Dictionary, this helps avoid duplicate hashtable lookups, and the last time I checked I think I saw around 50 non-test instances of

    if (!dict.ContainsKey(key))
    {
        dict.Add(key, ...);
    }

    in corefx.

  14. jnm2 commented on Aug 1, 2017

    @jnm2
    Contributor

    @jamesqo Yeah, okay. After the BCL has it for a while and we get default interface methods it might make more sense.

  15. 19 remaining items

  16. GrabYourPitchforks commented on Oct 31, 2020

    @GrabYourPitchforks
    Member

    CollectionsMarshal would definitely be more powerful. For example, if we modified the linked proposal to instead read:

    public static class CollectionsMarshal
    {
        public static ref TValue GetValueOrAddDefault<TKey, TValue>(Dictionary<TKey, TValue> @this, [NotNull] TKey key, out bool exists);
    }

    Then a "compute the hash code once" code sample would look like:

    ref TValue refToMyValue = CollectionsMarshal.GetValueOrAddDefault(dict, "myKey", out bool exists);
    if (!exists)
    {
        // the dictionary just performed the equivalent of Add("myKey", default(TValue)) - let's overwrite it now
        refToMyValue = CreateValue();
    }

    As Jan said, this would avoid the generic explosion problem, and it moves the micro-perf API to its own type. The downside is that CollectionsMarshal is by its nature somewhat unsafe. Not in the "violates type safety" sense, but more in the "you're juggling knives and could corrupt the dictionary's internal state" sense.

  17. svick commented on Oct 31, 2020

    @svick
    Member

    @jkotas

    I do not see a significant usability advantage of this method over writing a simple straightforward code:

    Keep in mind it's not really about simplifying from two lines of code to one, it's about changing this code:

    Foo GetFoo(int id)
    {
        if (!dictionary.TryGetValue(id, out var foo))
        {
            foo = CreateFoo(id);
            dictionary.Add(id, foo);
        }
        return foo;
    }

    to this code:

    Foo GetFoo(int id) => dictionary.GetOrAdd(id, CreateFoo);

    The latter seems much easier to understand to me.

  18. GrabYourPitchforks commented on Oct 31, 2020

    @GrabYourPitchforks
    Member

    As mentioned, one of the problems with providing the factory is "is the key by itself sufficient to create the value, or does creation of the value rely on more information than just the key?" If the latter (which I believe is common), you really need to provide a state object.

  19. alrz commented on Oct 31, 2020

    @alrz
    Member

    you really need to provide a state object.

    And that easily gets convoluted:

        if (!dictionary.TryGetValue(id, out var foo))
            dictionary.Add(id, foo = CreateFoo(id));
        Use(foo);
        var foo = dictionary.GetOrAdd(id, static t => t.self.CreateFoo(t.id), (self: this, id))
        Use(foo);

    I'd personally prefer the first one.

  20. FiniteReality commented on Oct 31, 2020

    @FiniteReality
        var foo = dictionary.GetOrAdd(id, static t => t.self.CreateFoo(t.id), (self: this, id))
        Use(foo);

    Wouldn't passing the state as a second parameter be way better, like ConcurrentDictionary?

    var dict = new ConcurrentDictionary<ulong, Client>();
    
    // ...
    
    var clientSocket = await serverSocket.AcceptAsync();
    var client = dict.GetOrAdd(clientId, CreateClient, socket);
    HandleClient(client);
    
    // ...
    
    static Client CreateClient(ulong clientId, Socket clientSocket)
    {
        return new Client(clientSocket);
    }
  21. GrabYourPitchforks commented on Oct 31, 2020

    @GrabYourPitchforks
    Member

    @FiniteReality We need to be careful about adding generic instance methods to generic types. There's a real risk that it could cause significant footprint or memory bloat, even for people who don't use the new feature. Generally we want new features to be pay-for-play and to incur no overhead if you don't use them.

  22. FiniteReality commented on Oct 31, 2020

    @FiniteReality

    @GrabYourPitchforks Understandable, but doesn't that somewhat fly in the face of being able to supply a state object? From what I can tell, the API @alrz suggested would have had the same issues, but would have been even more awkward to interact with.

  23. GrabYourPitchforks commented on Oct 31, 2020

    @GrabYourPitchforks
    Member

    Yes, it does fly in the face of being able to provide a state object, unless the parameter is typed as object? or some similar non-generic type.

  24. FiniteReality commented on Oct 31, 2020

    @FiniteReality

    typed as object? or some similar non-generic type.

    Oh joy! That's one of the things I hate most about using System.Threading.Timer 😅

  25. alrz commented on Nov 1, 2020

    @alrz
    Member

    There's a real risk that it could cause significant footprint or memory bloat, even for people who don't use the new feature.

    I don't understand this claim. Could you elaborate?

  26. eiriktsarpalis commented on Nov 2, 2020

    @eiriktsarpalis
    Member

    Agreed that a CollectionsMarshall approach is the preferred solution here. It would let users author their own variants of GetOrAdd as extension methods, without having to do double lookups.

  27. layomia commented on Feb 11, 2021

    @layomia
    Contributor

    I'm closing this issue in favor of #27062 which offers a more capable solution. See that issue for more details.

  28. ghost locked as resolved and limited conversation to collaborators on Mar 14, 2021
  29. eiriktsarpalis commented on Oct 29, 2021

    @eiriktsarpalis
    Member

    FWIW here's how one can author the originally proposed method as an extension method in .NET 6:

    public static class CollectionExtensions
    {
        public static TValue GetOrAdd<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> valueFactory)
            where TKey : notnull
        {
            if (dictionary == null)
            {
                throw new ArgumentNullException(nameof(dictionary));
            }
    
            if (valueFactory == null)
            {
                throw new ArgumentNullException(nameof(valueFactory));
            }
    
            ref TValue? value = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out bool exists);
            if (!exists)
            {
                value = valueFactory(key);
            }
    
            return value!;
        }
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions