Проблемы с отложенной итерацией возврата доходности

Я знаю, что yield return использует ленивую загрузку, но мне интересно, могу ли я неправильно использовать итератор или, возможно, мне нужен рефакторинг.

Мой метод рекурсивного итератора возвращает всех предков данного PageNode, включая сам pageNode.

public class PageNodeIterator {
    //properties and constructor left out for brevity

    public IEnumerable<IPageNode> ancestorsOf(IPageNode pageNode) {
        if(pageNode == null) throw new ArgumentNullException(("pageNode"));

        if (pageNode.url != pageNodeService.rootUrl) {
            yield return pageNode;
            if (pageNode.parent != null)
                foreach (var node in ancestorsOf(pageNode.parent))
                    yield return node;
        }
    }
}

В моем вызове ancestorsOf я вызываю метод, а затем меняю порядок возвращаемых IEnumerable, но поскольку загрузка откладывается, вызов на самом деле не происходит, пока я не вызову ToArray() в следующей строке и в этот момент pageNodeService в моем итераторе метод имеет значение null, и генерируется исключение нулевой ссылки.

ancestors = pageNodeIterator.ancestorsOf(currentNode).Reverse();
return ancestors.ToArray()[1].parent.children;

Итак, мне интересно, где я ошибся. Как правильно использовать итератор в этом случае, если вообще?

Мне также интересно, почему pageNodeService имеет значение null во время выполнения. Даже выполнение откладывается, не должно ли оно по-прежнему иметь значение?


person bflemi3    schedule 23.07.2013    source источник
comment
Остерегайтесь (очень) рекурсивного IEnumerable с yield. У него очень неожиданные и нежелательные характеристики памяти. blogs.msdn.com/b/ wesdyer/archive/2007/03/23/ Рассмотрите возможность поддержки собственного стека/очереди: blogs.msdn.com/b/ericlippert/archive/2005/08/01/   -  person spender    schedule 23.07.2013
comment
Что происходит, когда у вас есть 2 возврата yield в функции ??   -  person Jonesopolis    schedule 23.07.2013
comment
Нет проблем с доходностью 2+. Государственная машина сделает остановки в большем количестве мест.   -  person pero    schedule 23.07.2013
comment
в этот момент pageNodeService в моем методе итератора имеет значение null. Почему? Этого не должно быть.   -  person Henk Holterman    schedule 23.07.2013
comment
Сумасшедший. Я не знал, что это возможно. Кажется более запутанным, чем необходимо.   -  person Jonesopolis    schedule 23.07.2013
comment
@bflemi3: ваш метод работает правильно, когда вы удаляете итератор? Сначала убедитесь, что вы правильно сделали алгоритм. Может быть, написать несколько юнит-тестов. Затем добавьте поддержку итератора. Ваш код останется почти таким же.   -  person pero    schedule 23.07.2013
comment
@PetarRepac Еще не пробовал, но у меня есть другие рекурсивные методы, которые работают нормально. Я почти уверен, что это связано с отложенной загрузкой из yield return, просто не знаю почему.   -  person bflemi3    schedule 23.07.2013
comment
X является предком X ?   -  person pero    schedule 23.07.2013
comment
@PetarRepac в случае этого метода требовалось вернуть всех предков, включая начальный pageNode. Возможно, название немного вводит в заблуждение.   -  person bflemi3    schedule 23.07.2013
comment
@HenkHolterman Я согласен, но у меня недостаточно хорошее понимание .net framework, чтобы ответить на этот вопрос. Хотя это кажется подозрительным - я посмотрю на это.   -  person bflemi3    schedule 23.07.2013
comment
я думаю, может быть, это с .children, я думаю, что дети также являются пейджнодами.   -  person terrybozzio    schedule 23.07.2013
comment
Откуда pageNodeService? Как вы это устанавливаете? Вы когда-нибудь сбрасывали его на null? В идеале, не могли бы вы включить короткий, но полный код, демонстрирующий это?   -  person svick    schedule 23.07.2013


Ответы (4)


На самом деле это не ответ... скорее предложение альтернативной реализации, исключающей рекурсию. Слишком долго, чтобы опубликовать в качестве комментария.

    public IEnumerable<IPageNode> ancestorsOf(IPageNode pageNode) {
        if(pageNode == null) throw new ArgumentNullException(("pageNode"));
        Stack<IPageNode> stack = new Stack<IPageNode>();
        stack.Push(pageNode);
        while(stack.Any())
        {
            IPageNode n=stack.Pop();
            if (n.url != pageNodeService.rootUrl) {
                yield return n;
                if(n.parent != null)
                {
                    stack.Push(n.parent);
                }
            }
        }
    }

Подумав об этом, вы могли бы вообще удалить стек:

public IEnumerable<IPageNode> ancestorsOf(IPageNode pageNode) {
    if(pageNode == null) throw new ArgumentNullException(("pageNode"));
    IPageNode n = pageNode;
    while(n != null && n.url != pageNodeService.rootUrl)
    {
        yield return n;
        n = n.parent;
    }
}
person spender    schedule 23.07.2013
comment
здесь читайте дальше, почему это изменение довольно важно; производительность действительно страдает в блоках рекурсивного итератора; больше, чем обычные рекурсивные функции. - person Servy; 23.07.2013
comment
Да, это лучшее объяснение, которое я предложил в своих комментариях выше. - person spender; 23.07.2013
comment
Я не понимаю, как это проходит через всех родителей данного pageNode? - person bflemi3; 23.07.2013
comment
@bflemi3: потому что родитель попадает в стек и извлекается из стека на следующей итерации цикла. Я подумал о том, что вы на самом деле делаете, и понял, что стек или рекурсия действительно не нужны. Взгляните на второй пример кода и дайте мне знать, соответствует ли он вашим намерениям. - person spender; 23.07.2013
comment
На самом деле все сводилось к тому, что pageNodeService на нем было protected set. Поэтому, когда я использовал Activator для создания экземпляра, он не смог установить это свойство, поэтому оно было нулевым. Однако я реорганизовал свой метод с вашим вторым решением, поэтому я дам вам ответ. Еще раз спасибо транжира. - person bflemi3; 23.07.2013

Я не знаю, где ваша ошибка, и StackOverflow — это не сервис для отладки вашего кода; Я бы решил вашу проблему, запустив ее в отладчике и найдя ошибку.

Однако я воспользуюсь этой возможностью, чтобы указать, что это:

public IEnumerable<IPageNode> AncestorsOf(IPageNode pageNode) {
    if(pageNode == null) throw new ArgumentNullException(("pageNode"));
    // Do stuff that yields 

немного проблематичен, потому что ни один код в блоке не запускается, пока MoveNext не будет вызван в первый раз. Другими словами, если вы делаете:

var seq = AncestorsOf(null); // Not thrown here!
using (var enumtor = seq.GetEnumerator())
{
    bool more = enumtor.MoveNext(); // Exception is thrown here!

что очень удивляет людей. Вместо этого напишите свой код следующим образом:

public IEnumerable<IPageNode> AncestorsOf(IPageNode pageNode) {
    if(pageNode == null) throw new ArgumentNullException(("pageNode"));
    return AncestorsOfIterator(pageNode);
}
private IEnumerable<IPageNode> AncestorsOfIterator(IPageNode pageNode)
{
    Debug.Assert(pageNode != null);
    // Do stuff that yields 
}
person Eric Lippert    schedule 23.07.2013

Имеет ли вообще смысл использовать yield в этом месте, поскольку при вызове Reverse все вещи должны быть все равно буферизуется, так что вместо этого вы можете просто вернуть полный список предков.

person Marwie    schedule 23.07.2013

Добавьте начальный узел вне этого итератора, если вам это нужно.

public class PageNodeIterator {
    //properties and constructor left out for brevity

    public IEnumerable<IPageNode> ancestorsOf(IPageNode pageNode) {
        if(pageNode == null) throw new ArgumentNullException(("pageNode"));

        if (pageNode.url != pageNodeService.rootUrl)
        {
            if (pageNode.parent != null ) 
            {
                yield return pageNode.parent;
                yield return ancestorsOf(pageNode.parent);
            }
        }
    }
}
person pero    schedule 23.07.2013
comment
Итерация предка должна продолжаться до pageNode.url != pageNodeService.rootUrl. Эта логика не содержится в вашем методе. - person bflemi3; 23.07.2013
comment
Обновлено, но тогда у метода должно быть какое-то другое имя, потому что мы не возвращаем только предков, есть другие требования. Может быть, лучше поместить эти дополнительные требования вне итератора? - person pero; 23.07.2013
comment
Я согласен, я посмотрю на рефакторинг этого. Спасибо Петар :) - person bflemi3; 23.07.2013