3

Anybody have a slicker way to do this? Seems like it should be easier than this, but I'm having a mental block. Basically I need to remove items from an dictionary and recurse into the values of the items that are also dictionaries.

private void RemoveNotPermittedItems(ActionDictionary menu)
{
    var keysToRemove = new List<string>();
    foreach (var item in menu)
    {
        if (!GetIsPermitted(item.Value.Call))
        {
            keysToRemove.Add(item.Key);
        }
        else if (item.Value is ActionDictionary)
        {
            RemoveNotPermittedItems((ActionDictionary)item.Value);
            if (((ActionDictionary)item.Value).Count == 0)
            {
                keysToRemove.Add(item.Key);
            }
        }
    }
    foreach (var key in (from item in menu where keysToRemove.Contains(item.Key) select item.Key).ToArray())
    {
        menu.Remove(key);
    }
}

Action dictionary is like this:

public class ActionDictionary : Dictionary<string, IActionItem>, IActionItem
Darren Kopp
  • 76,581
  • 9
  • 79
  • 93
Tim Scott
  • 15,106
  • 9
  • 65
  • 79

10 Answers10

3

You don't really need to collect the keys and iterate them again if you iterate the dictionary in reverse (from 'menu.Count - 1' to zero). Iterating in forward order will, of course, yield mutated collection exceptions if you start removing things.

I don't know what an ActionDictionary is, so I couldn't test your exact scenario, but here's an example using just Dictionary<string,object>.

    static int counter = 0;
    private static void RemoveNotPermittedItems(Dictionary<string, object> menu)
    {
        for (int c = menu.Count - 1; c >= 0; c--)
        {
            var key = menu.Keys.ElementAt(c);
            var value = menu[key];
            if (value is Dictionary<string, object>)
            {
                RemoveNotPermittedItems((Dictionary<string, object>)value);
                if (((Dictionary<string, object>)value).Count == 0)
                {
                    menu.Remove(key);
                }
            }
            else if (!GetIsPermitted(value))
            {
                menu.Remove(key);
            }
        }
    }

    // This just added to actually cause some elements to be removed...
    private static bool GetIsPermitted(object value)
    {
        if (counter++ % 2 == 0)
            return false;
        return true;
    }

I also reversed the 'if' statement, but that was just an assumption that you'd want to do type checking before calling a method to act on the item's value...it will work either way assuming 'GetIsPermitted' always returns TRUE for ActionDictionary.

Hope this helps.

Jared
  • 2,007
  • 15
  • 14
  • This is correct. The exception is that I should not reverse the IF because it would make different behavior. If a parent item is not permitted I want to remove it even if it has permitted children. – Tim Scott Oct 25 '08 at 17:04
  • @TimScott Why would you delete recursively, if we can delete the parent node without deleting the children. – Asif Mushtaq May 15 '12 at 15:32
2

To start with, your foreach loop is way more complicated than it needs to be. Just do:

foreach (var key in keysToRemove)
{
    menu.Remove(key);
}

I'm slightly surprised that Dictionary doesn't have a RemoveAll method but it doesn't look like it does...

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    This does not work. You cannot remove an item while iterating through a collection. You will get an exception that the collection has been modified. – Tim Scott Oct 24 '08 at 22:26
  • 1
    This isn't operating on the original array though, so that isn't an issue. He's talking about the second foreach, not the first. – Matthew Scharley Oct 24 '08 at 22:30
  • Gotcha, of course, Refer to my previous comment about "mental block". :) Still I'd like to think it can be done in one loop. – Tim Scott Oct 24 '08 at 22:36
  • Well, you could have a single "select" which only allowed through appropriate entries - then call ToDictionary at the end. It would be fairly ugly though. – Jon Skeet Oct 24 '08 at 22:39
  • @JesseC.Slicer: I prefer using `foreach` over `ForEach`. See Eric Lippert's blog post for more details. – Jon Skeet May 17 '12 at 15:48
  • @JonSkeet I can appreciate that, though for myself, I find a semantic difference between `List`'s `ForEach` and the number of homespun extension method `ForEach`es on `IEnumerable`. Plus, this particular example is not mutating the list itself, so I see no side effects happening. At least, I hope I'm reasoning that correctly :) – Jesse C. Slicer May 17 '12 at 15:53
  • 1
    @JesseC.Slicer: There are side-effects, otherwise it would be pointless. It's not mutating the *list*, but it's mutating the dictionary. – Jon Skeet May 17 '12 at 15:55
  • @JonSkeet Truth. Thanks for the reality cheque; I clearly need more coffee. – Jesse C. Slicer May 17 '12 at 15:56
  • @JonSkeet: How about the `Clear()` method? – Gebb May 17 '12 at 17:15
  • @JonSkeet: I thought it did what you expect from the `RemoveAll` method, but now I seem to understand that you had in mind something taking a predicate as a parameter. – Gebb May 17 '12 at 18:03
2

While foreach and GetEnumerator fails, a for-loop works,

var table = new Dictionary<string, int>() {{"first", 1}, {"second", 2}};
for (int i = 0; i < table.Keys.Count; i++)//string key in table.Keys)
{
    string key = table.Keys.ElementAt(i);
    if (key.StartsWith("f"))
    {
        table.Remove(key);
    }
}

But ElementAt() is a .NET 3.5 feature.

Lex Li
  • 60,503
  • 9
  • 116
  • 147
1

In My Opinion, you can define your own generic class deriving from the KeyValuePair<...> both TKey and TValue will be List<T> and you can use the RemoveAll or the RemoveRange of the List<T> in a new RemoveRange() or RemoveAll() method in your derived class to Remove the items you want.

Writwick
  • 2,133
  • 6
  • 23
  • 54
1

I know you probably found good solution already, but just for the reason of 'slickness' if you could modify your method signatures to (I know it may not be appropriate in your scenario):

private ActionDictionary RemoveNotPermittedItems(ActionDictionary menu)
{
 return new ActionDictionary(from item in menu where GetIsPermitted(item.Value.Call) select item)
.ToDictionary(d=>d.Key, d=>d.Value is ActionDictionary?RemoveNotPermittedItems(d.Value as ActionDictionary) : d.Value));
}

And I can see couple ways where you can use your dictionary with filtered items without modification and materializing new dictionaries.

Val Bakhtin
  • 1,434
  • 9
  • 11
1

It's not much less complicated, but some idiomatic changes make it a bit shorter and easier on the eyes:

    private static void RemoveNotPermittedItems(IDictionary<string, IActionItem> menu)
    {
        var keysToRemove = new List<string>();

        foreach (var item in menu)
        {
            if (GetIsPermitted(item.Value.Call))
            {
                var value = item.Value as ActionDictionary;

                if (value != null)
                {
                    RemoveNotPermittedItems(value);
                    if (!value.Any())
                    {
                        keysToRemove.Add(item.Key);
                    }
                }
            }
            else
            {
                keysToRemove.Add(item.Key);
            }
        }

        foreach (var key in keysToRemove)
        {
            menu.Remove(key);
        }
    }

    private static bool GetIsPermitted(object call)
    {
        return ...;
    }
Jesse C. Slicer
  • 19,901
  • 3
  • 68
  • 87
1

Change the type of the keysToRemove to HashSet<string> and you'll get an O(1) Contains method. With List<string> it's O(n), which is slower as you might guess.

Gebb
  • 6,371
  • 3
  • 44
  • 56
1

untested until i'm at my VS machine tomorrow :o

private void RemoveNotPermittedItems(ActionDictionary menu)
{
    foreach(var _checked in (from m in menu
                             select new
                             {
                                 gip = !GetIsPermitted(m.Value.Call),
                                 recur = m.Value is ActionDictionary,
                                 item = m
                             }).ToArray())
    {
        ActionDictionary tmp = _checked.item.Value as ActionDictionary;
        if (_checked.recur)
        {
            RemoveNotPermittedItems(tmp);
        }
        if (_checked.gip || (tmp != null && tmp.Count == 0) {
            menu.Remove(_checked.item.Key);
        }
    }
}
ZagNut
  • 1,431
  • 15
  • 20
1

I think

public class ActionSet : HashSet<IActionItem>, IActionItem

And

bool Clean(ActionSet nodes)
    {
        if (nodes != null)
        {
            var removed = nodes.Where(n => this.IsNullOrNotPermitted(n) || !this.IsNotSetOrNotEmpty(n) || !this.Clean(n as ActionSet));

            removed.ToList().ForEach(n => nodes.Remove(n));

            return nodes.Any();
        }

        return true;
    }

    bool IsNullOrNotPermitted(IActionItem node)
    {
        return node == null || *YourTest*(node.Call);
    }

    bool IsNotSetOrNotEmpty(IActionItem node)
    {
        var hset = node as ActionSet;
        return hset == null || hset.Any();
    }

Should work fast

misha
  • 11
  • 1
1

Option 1: A Dictionary is still a Collection. Iterate over menu.Values.

You can iterate over menu.Values and remove them as you iterate. The values won't come in any sorted order (which should be fine for your case). You may need to use a for loop and adjust the index rather than using foreach - the enumerator will throw an exception if you modify the collection while iterating.

(I'll try to add the code when I'm on my dev machine Mon)

Option 2: Create a custom iterator.

Some collections returned from ListBox SelectedItems in Winforms don't really contain the collection, they provide a wrapper around the underlying collection. Kind of like CollectionViewSource in WPF. ReadOnlyCollection does something similar too.

Create a class that can "flatten" your nested dictionaries into something that can enumerate over them like they are a single collection. Implement a delete function that looks like it removes an item from the collection, but really removes from the current dictionary.

Geoff Cox
  • 6,102
  • 2
  • 27
  • 30