Home > OS >  A 2-in-1 check existence and store - is this possible when searching a Dictionary?
A 2-in-1 check existence and store - is this possible when searching a Dictionary?

Time:10-26

A common pattern I like to use in C# is one where instead of first checking whether something exists in a collection with Any then finding it again with First, I simply call FirstOrDefault, which both tells me whether it exists and if so, gives me a reference to it:

List<Person> people = ...;
Person found;
if ((found = people.FirstOrDefault(x => x.Age > 10)) != null) {
    // Found person with age over 10, and we have them in 'found'
    //...
}

This works when the thing being found is a reference type, and can be null. However, I was trying to do the same thing with a Dictionary's entries:

Dictionary<(int X, int Y), ITileData<(int X, int Y)>> srchField = new();
KeyValuePair<(int X, int Y), ITileData<(int X, int Y)>> next;
while ((next = srchField.FirstOrDefault(x => !x.Value.Reached)) != null) {
    // Found next, and we have it in 'next'
    //...
}

This, however, doesn't work because srchField.FirstOrDefault(...) returns a KeyValuePair<TKey, TValue>, which is a struct. I can work around this by first calling Any on srchField, and then calling First with the same predicate if Any finds anything, but this is having to do the same search twice. Is there any way I can do the 2-in-1 check existence and store here, only carrying out one search of the dictionary in LINQ?

CodePudding user response:

If you want to ignore the key in your search and your values are reference types, you can just use Values properties of a dictionary:

srchField.Values.FirstOrDefault(!v => v.Reached)

If you want to do something with keys, it would be a bit more clunky:

srchField.Where(kvp => Predicate(kvp.Key)).Select(kvp => kvp.Value).FirstOrDefault()

CodePudding user response:

Thanks to @TheodorZoulias pointing me in the right direction, and inspired by a combination of the answer from @Samuel and the nice code solution from @JonSkeet (I hadn't actually realized that if LINQ .Take(1) only finds 0 elements, it actually gives you an enumerable of 0 instead of throwing an exception), I came up with an extension method that works well for doing this:

public static class IEnumerableExtensions {
    public static bool TryGetFirst<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, out TSource first) {
        if (predicate == null) {
            throw new ArgumentNullException(nameof(predicate));
        }

        var query = source.Where(predicate).Take(1).ToList();
        if (query.Count == 0) {
            first = default;
            return false;
        }
        first = query[0];
        return true;
    }
}

Example usage:

while (srchField.TryGetFirst(x => !x.Value.Reached, out KeyValuePair<(int X, int Y), ITileData<(int X, int Y)>> next)) {
    var key = next.Key;
    var val = next.Value;
    // ... use eg. key.X, key.Y, and val ...
}
  • Related