0

I am trying to create a function whereby I can pass in a functor/predicate that can slot into a dictionary's 'Where' method.

(cardPool is the dictionary of type 'cardStats') Pseudo of what I'd like to do:

void CardStats findCard(Predicate<CardStats> pred)
{
    return cardPool.Where(pred);
}

This code obviously wont work but is simply a rough example of the functionality I am looking for. I have had no problems setting this up for lists, but for a Dictionary, its really got me stumped.

Any help would be great, thanks!

Edit: Ahh sorry I should have mentioned more: Cardstats is the value, the key is of type int. I'd like to sift through the values (cardStats) and test their properties such as ID(int) or name(string).

Dahlvash
  • 19
  • 1
  • 5
  • Dictionaries have two generic type parameters (the key type and the value type). Your dictionary can't just have one type. – JLRishe Dec 23 '14 at 21:15
  • 1
    Well is `CardStats` the value type or the key type? It sounds like you may just want to use either the `Values` or `Keys` property, then use `Where` as normal... – Jon Skeet Dec 23 '14 at 21:15

2 Answers2

7

Dictionary<TKey, TValue> implements IEnumerable<KeyValuePair<TKey, TValue>>, so its Where extension method takes a predicate of type Func<KeyValuePair<TKey, TValue>, bool>.

You could implement your method like this:

void CardStats findCard(Func<int, CardStats, bool> pred)
{
    return cardPool.Where(kv => pred(kv.Key, kv.Value))
                   .Select(kv => kv.Value)
                   .FirstOrDefault();
}

And use it like this:

CardStats stats = myCards.findCard((id, stats) => id == 7);

or

CardStats stats = myCards.findCard((id, stats) => stats.Name == "Ace of Clubs");

Note that using Where on a dictionary doesn't take advantage of the dictionary's quick lookup features and basically treats it as a linear collection of key-value pairs.


One more comment: I would suggest providing a method that returns an IEnumerable of found cards if there are several. Or you could provide one that does that, and one that just returns the first match:
void IEnumerable<CardStats> findCards(Func<int, CardStats, bool> pred)
{
    return cardPool.Where(kv => pred(kv.Key, kv.Value))
                   .Select(kv => kv.Value);
}

void CardStats findCard(Func<int, CardStats, bool> pred)
{
    return findCards(pred).FirstOrDefault();
}
JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • Ahh sorry I should have mentioned more: Cardstats is the value, the key is of type int. I'd like to sift through the values (cardStats) and test their properties such as ID(int) or name(string). – Dahlvash Dec 23 '14 at 21:25
  • "key-value" is important to look for. Performance you need to consider in large dictionary. – Chaturvedi Dewashish Dec 23 '14 at 21:27
  • You can also do cardPool.Where(pred).ToList() to get all the values matching the pred – Chaturvedi Dewashish Dec 23 '14 at 21:29
  • @ChaturvediDewashish You can't directly pass a `Func` to the `Where()` method because it expects a `Func, bool>`. I chose to use the former in the method signature because it allows writing lambdas like `(id, stat) => id == 5` instead of `kv => kv.Key == 5`, which is less clear in its intent. – JLRishe Dec 23 '14 at 21:34
  • @JLRishe Thanks. Nice catch. Actually I had wrapped it as Expression>. It helped me build complex predicates. http://stackoverflow.com/questions/11490893/how-does-predicatebuilder-work – Chaturvedi Dewashish Dec 23 '14 at 21:38
  • Thank you, JLRishe! Just gave it a whirl and it does exactly what I was looking for. – Dahlvash Dec 23 '14 at 21:49
1

I would use FirstOrDefault as the first statement because it will stop as soon it finds a matching element. another thing is that I will consider using something else than a dictionary - because when using it this way is abuse if its indexed purpose.

anyway, this is the code I will use:

public CardStats Find(Func<CardStats, bool> predicate)
{
    KeyValuePair<int, Roster> kvCard = cardPool.FirstOrDefault(kvp => predicate(kvp.Value));
    if (kvCard.Equals(default(KeyValuePair<int, Roster>)))
        return null;
    return kvCard.Value;
}
EladTal
  • 2,167
  • 1
  • 18
  • 10
  • LINQ uses lazy evaluation, so it will not go through the remainder of the collection after the first item is found if you use `.Where().Select().FirstOrDefault()`. `.FirstOrDefault(pred)` is a little more concise, but then you need a null check as you have here. You can use the `?.` operator to simplify your code a bit, but my approach allows implementing select-multi and select-single helpers without duplicating code. – JLRishe Sep 03 '22 at 10:42