Метод расширения Linq, как найти ребенка в коллекции рекурсивно

Я уже знаком с Linq, но плохо разбираюсь в методах расширения. Надеюсь, что кто-то может мне помочь.

Итак, у меня есть этот псевдокод иерархической коллекции, то есть:

class Product
  prop name
  prop type
  prop id
  prop List<Product> children

И у меня есть список продуктов Список продуктов.

Можно ли как-нибудь найти товар в этой коллекции по идентификатору с помощью метода расширения? Другими словами, мне нужен один элемент где-то в иерархии.


person sushiBite    schedule 27.08.2010    source источник
comment
Вы имеете в виду: productsList.Where (x = ›x.Id == yourId) ;?   -  person Kirk Woll    schedule 27.08.2010
comment
Или productsList.FirstOrDefault (x = ›x.Id == yourId) ;? Это возвращает единственный объект, null, если соответствующий объект не найден.   -  person Albin Sunnanbo    schedule 27.08.2010
comment
Нет, я имею в виду, что мне нужно посмотреть как ProductsList, так и ProductList- ›Product-› Children. Это моя проблема, я могу сделать это рекурсивным методом, но мне было интересно, есть ли возможность сделать это с помощью linq-extension.   -  person sushiBite    schedule 27.08.2010


Ответы (5)


Вот общее решение, которое сокращает обход иерархии при обнаружении совпадения.

public static class MyExtensions
{
    public static T FirstOrDefaultFromMany<T>(
        this IEnumerable<T> source, Func<T, IEnumerable<T>> childrenSelector,
        Predicate<T> condition)
    {
        // return default if no items
        if(source == null || !source.Any()) return default(T);

        // return result if found and stop traversing hierarchy
        var attempt = source.FirstOrDefault(t => condition(t));
        if(!Equals(attempt,default(T))) return attempt;

        // recursively call this function on lower levels of the
        // hierarchy until a match is found or the hierarchy is exhausted
        return source.SelectMany(childrenSelector)
            .FirstOrDefaultFromMany(childrenSelector, condition);
    }
}

Чтобы использовать его в вашем случае:

var matchingProduct = products.FirstOrDefaultFromMany(p => p.children, p => p.Id == 27);
person Jay    schedule 27.08.2010
comment
Привет, я сделал одно незначительное изменение в этом коде, и это работает :) Я изменил это, если (Equals (try, default (T))) return try; to if (Equals (try! = null) return try; Работает как шарм Спасибо всем за вашу помощь. - person sushiBite; 27.08.2010
comment
@sushiBite Я думаю, что это действительно должно быть if(!Equals(attempt, default(T))) return attempt;, потому что значение по умолчанию T может не быть null (если T является типом значения). - person Jay; 27.08.2010

Вы можете сгладить древовидную структуру, используя этот метод расширения:

static IEnumerable<Product> Flatten(this IEnumerable<Product> source)
{
    return source.Concat(source.SelectMany(p => p.Children.Flatten()));
}

Использование:

var product42 = products.Flatten().Single(p => p.Id == 42);

Обратите внимание, что это, вероятно, не очень быстро. Если вам неоднократно нужно найти товар по id, создайте словарь:

var dict = products.Flatten().ToDictionary(p => p.Id);

var product42 = dict[42];
person dtb    schedule 27.08.2010
comment
Отлично, мне нравится этот метод Flatten. Если я не ошибаюсь, он будет повторяться в ширину (редактировать: я ошибаюсь, это не в ширину, хотя вопрос все еще актуален). Означает ли это, что если продукт является первым элементом в списке и вы используете First вместо Single, он не сгладит всю иерархию? Поможет ли здесь отложенное выполнение linq? - person Bubblewrap; 27.08.2010
comment
Это похоже на хорошее решение, но оно игнорирует возможность того, что список дочерних элементов может быть null. - person Gabe; 27.08.2010
comment
@Bubblewrap: Ты прав. Если вы используете First, то благодаря отложенному выполнению Flatten сгладит ровно столько, сколько необходимо. - person dtb; 27.08.2010

Я просто реорганизую решение dtb, чтобы сделать его более универсальным. Попробуйте этот метод расширения:

public static IEnumerable<T> Flatten<T, R>(this IEnumerable<T> source, Func<T, R> recursion) where R : IEnumerable<T>
{
    return source.SelectMany(x => (recursion(x) != null && recursion(x).Any()) ? recursion(x).Flatten(recursion) : null)
                 .Where(x => x != null);
}

И вы можете использовать это так:

productList.Flatten(x => x.Children).Where(x => x.ID == id);
person Jonathas Costa    schedule 10.01.2014

Альтернативное решение, использующее yield для оптимизации необходимых перечислений.

public static IEnumerable<T> SelectManyRecursive<T>(
    this IEnumerable<T> source,
    Func<T, IEnumerable<T>> childrenSelector)
{
    if (source == null)
        throw new ArgumentNullException("source");

    foreach (var i in source)
    {
        yield return i;
        var children = childrenSelector(i);
        if (children != null)
        {
            foreach (var child in SelectManyRecursive(children, childrenSelector))
            {
                yield return child;
            }
        }
    }
}

Затем вы можете найти совпадение, вызвав что-то вроде FirstOrDefault:

    var match = People.SelectManyRecursive(c => c.Children)
                      .FirstOrDefault(x => x.Id == 5);
person kampsj    schedule 10.07.2013

Если вы хотите «выполнить под-итерацию» и найти дочерний элемент в списке продуктов:

List<Product>
    Product
       Child
       Child
       Child
       Child
    Product
       Child
       Child *find this one
       Child

Вы можете использовать существующий метод расширения SelectMany. SelectMany можно использовать для «сглаживания» двухуровневой иерархии.

Вот отличное объяснение SelectMany: http://team.interknowlogy.com/blogs/danhanan/archive/2008/10/10/use-linq-s-selectmany-method-to-quot-flatten-quot-collections.aspx

Ваш синтаксис должен быть таким:

List<Product> p = GetProducts(); //Get a list of products
var child = from c in p.SelectMany(p => p.Children).Where(c => c.Id == yourID);
person Dave Swersky    schedule 27.08.2010
comment
Что ж, это хорошо, но иерархия и многоуровневая глубина для каждого продукта, поэтому у продукта A может быть 3 ребенка, 5 внуков и 100 внуков, а у продукта может быть только 1 ребенок и не может быть внука, для меня нет возможности знать. если я правильно это понимаю, мне пришлось бы использовать SelectMany () для каждого уровня иерархии? - person sushiBite; 27.08.2010
comment
Вы можете связать SelectMany вместе настолько, насколько это необходимо, чтобы достичь желаемого уровня. - person Dave Swersky; 27.08.2010