1

Let's say that I have a ConcurrentDictionary:

var dict = new ConcurrentDictionary<string, someObject>();
dict.TryAdd("0_someA_someB_someC", obj0);
dict.TryAdd("1_someA_someB_someC", obj1);
dict.TryAdd("2_someA_someB_someC", obj2);
dict.TryAdd("3_someA_someB_someC", obj3);

The <number>_ in the keys is incremented and being a dictionary, there is no guarantee that the elements are in order.

Now, imagine I wanted to remove all items from the dictionary that have the number less than 2. I have no idea what the keys will look like, only that they will be prefixed with a number as above.

How can I remove all elements from the dictionary who's key starts with a value less than 2?

For example, the resulting dict after this process will look like this:

dict.TryAdd("2_someA_someB_someC", obj2);
dict.TryAdd("3_someA_someB_someC", obj3);
pookie
  • 3,796
  • 6
  • 49
  • 105
  • 1
    You can't do this *atomically* without a lock. That might be an issue if you're using a `ConcurrentDictionary` (but not necessarily, depending on why you're using `ConcurrentDictionary` and why you're removing keys). `ImmutableSortedDictionary` is a thing, but may or may not be a good match for your scenario -- again, depending on use. – Jeroen Mostert Sep 17 '18 at 12:16
  • 2
    For efficiency's sake, instead of indexing on a string, consider a custom key type (or a simple tuple) where these "fields" are split out into actual fields, or else multiple dictionaries (in turn, you could group these in their own custom collection). A concatenated string is a quick and easy way of producing a unique key, but not a good idea if you commonly have to parse the keys. Even if you can't modify callers, keeping a redundant extra collection where you map these keys for quick lookup can be worth it. – Jeroen Mostert Sep 17 '18 at 12:32
  • @JeroenMostert that is a very interesting idea. I never considered that a possibility. Any chance you can provide a link to an example. – pookie Sep 17 '18 at 12:36
  • The `ValueTuple` is easy enough in recent versions of C#: `var d = new ConcurrentDictionary<(int theNumber, string compoundKey), someObject>(); d.TryAdd((2, "2_someA_someB_someC"), obj0); d.Keys.Where(k => k.theNumber < 2)`. `ValueTuple` has suitable implementations of `GetHashCode` and `Equals` to work as a dictionary key. This only optimizes the string parsing and not the fact that we still have to go through all the keys, but that may be enough. The custom adapter that splits dictionaries is a bunch more work that I don't feel like working on. :-P – Jeroen Mostert Sep 17 '18 at 12:50
  • @JeroenMostert Great, thanks. I'll have a play with this. – pookie Sep 17 '18 at 12:54

4 Answers4

4

Presuming it has always this format, you can use LINQ:

var keysToRemove = dict.Keys.Where(key => int.Parse(key.Remove(key.IndexOf('_'))) < 2).ToList();
keysToRemove.ForEach(key => dict.TryRemove(key, out someObject obj));

String.Remove removes the part starting from _ and then parses the remaiming first part, the number. It will only select the keys which number is lower than 2. This list will be used to remove the items from the dictionary. Of course you need a lock to make this thread-safe.

Tim Schmelter
  • 450,073
  • 74
  • 686
  • 939
  • I cannot see anything wrong with your answer. It appears to answer my question perfectly. I did notice that as soon as you posted it (immediately), someone down voted yours and upvoted Selman Genç's answer. Not saying the two events are related, just pointing out that I noticed it. Regarding thread-safety, I was under the impression that the point of the `Concurrent` collections was to allow devs to focus on programming rather than concurrency issues. Are you saying that even with Concurrent collections, we need to use locks and semaphores? – pookie Sep 17 '18 at 12:35
  • 2
    @pookie: the concurrent collections offer *individual operations* that are efficiently concurrent without further locking. As soon as you want to do more than one thing in an all-or-nothing way, there's still no such thing as a free lunch. Consider the case where this code fetches all the keys, then another thread inserts more keys that also match the check. Those keys will not be gone by the time this loop finishes. Likewise, if another thread removes and then inserts a matching key with a new value, this code can remove that value, which may or may not be what you want. – Jeroen Mostert Sep 17 '18 at 12:39
  • @pookie: there's a lot going on in these two lines. The `Keys`-collection that i'm using is basically a snapshots of the `ConcurrentDictionary` at the time i have asked for it. Then i'm enumerating all keys and afterwards remove matching items which could take some time. In this time it's possible that the `ConcurrentDictionary` receives more items from another thread that would match the criteria. So while i'm going to delete them, the state of the dictionary has already changed. If you want to prevent that you need a `lock`. – Tim Schmelter Sep 17 '18 at 12:45
  • @JeroenMostert and TimSchmelter Thanks, that makes sense. I thought it was too good to be true... – pookie Sep 17 '18 at 12:47
  • Tempted to downvote because of the suggestion to use locks for thread-safety. Combining a `ConcurrentDictionary` with locks makes no sense. If you need to lock, use a `Dictionary` instead. Because when locking you can't be selective. You must lock on **every** access to the collection (both write **and** read). Locking partially is as good as not locking at all. And locking completely a `ConcurrentDictionary` means using two synchronization mechanisms, one on top of the other, with the one being redundant and adding only overhead. – Theodor Zoulias Apr 13 '20 at 22:41
1
  1. Parse the number that comes before the first underscore (Tip: IndexOf and Substring)
  2. Convert it to integer (Tip: int.TryParse)
  3. Compare number to the value (2 in this case)
  4. Filter the keys applying this method, store them in a collection. Iterate over the collection and call TryRemove method to remove entries associated with the key.
Selman Genç
  • 100,147
  • 13
  • 119
  • 184
0

You would need to iterate across the dictionary collecting the keys that match the criteria and then iterate across that list of keys deleting from the dictionary. A for-each across a dictionary returns items with Key and Value properties, so you can examine the Key property to decide whether or not to delete. You cannot delete in the same loop as this will result in an error.

open-collar
  • 1,404
  • 1
  • 16
  • 22
0

You could use Split to split the key into an array on the _ character, convert the first item in the resulting array into an int (note this will throw if the key doesn't start with an int), and if it's less than 2, remove it from the dictionary:

foreach (var item in dict.Where(kvp => int.Parse(kvp.Key.Split('_')[0]) < 2))
{
    SomeObject tempObject;
    dict.TryRemove(item.Key, out tempObject);
}
Rufus L
  • 36,127
  • 5
  • 30
  • 43