Зачем использовать ключевое слово yield, когда я могу просто использовать обычный IEnumerable?


171

Учитывая этот код:

IEnumerable<object> FilteredList()
{
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            yield return item;
    }
}

Почему я не должен просто кодировать это так:

IEnumerable<object> FilteredList()
{
    var list = new List<object>(); 
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            list.Add(item);
    }
    return list;
}

Я вроде понимаю, что yieldделает ключевое слово. Он сообщает компилятору, что нужно создать определенный тип вещей (итератор). Но зачем это использовать? Помимо того, что это немного меньше кода, что это делает для меня?


28
Я понимаю, что это всего лишь пример, но на самом деле код должен быть написан так: FullList.Where(IsItemInPartialList):)
BlueRaja - Danny Pflughoeft 27.12.12

Ответы:


241

Использование yieldделает коллекцию ленивой.

Допустим, вам просто нужны первые пять предметов. Ваш путь, я должен пройтись по всему списку, чтобы получить первые пять предметов. С помощью yieldя только перебираю первые пять пунктов.


14
Обратите внимание, что использование FullList.Where(IsItemInPartialList)будет столь же ленивым. Только для этого требуется гораздо меньше сгенерированного компилятором пользовательского кода. И меньше времени разработчиков на написание и сопровождение. (Конечно, это был только этот пример)
сехе

4
Это Линк, не так ли? Я думаю, что Linq делает что-то очень похожее под одеялом.
Роберт Харви

1
Да, Linq использовал отложенное выполнение ( yield return) везде, где это было возможно.
Чед Шуггинс

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

127

Преимущество блоков итераторов в том, что они работают лениво. Таким образом, вы можете написать метод фильтрации следующим образом:

public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
                                   Func<T, bool> predicate)
{
    foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

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

В качестве другого примера, вы можете легко создать бесконечный поток, используя блоки итераторов. Например, вот последовательность случайных чисел:

public static IEnumerable<int> RandomSequence(int minInclusive, int maxExclusive)
{
    Random rng = new Random();
    while (true)
    {
        yield return rng.Next(minInclusive, maxExclusive);
    }
}

Как бы вы сохранили бесконечную последовательность в списке?

В моей серии блогов Edulinq приведен пример реализации LINQ to Objects, в которой интенсивно используются блоки итераторов. LINQ, в сущности, ленив, где это может быть - и размещение вещей в списке просто не работает таким образом.


1
Я не уверен, нравится ли вам ваш RandomSequenceили нет. Для меня IEnumerable означает - в первую очередь - то, что я могу повторять с помощью foreach, но это, очевидно, привело бы к бесконечному циклу. Я бы посчитал это довольно опасным злоупотреблением концепцией IEnumerable, но YMMV.
Себастьян Неграсус

5
@SebastianNegraszus: последовательность случайных чисел логически бесконечна. IEnumerable<BigInteger>Например, вы можете легко создать представляющую последовательность Фибоначчи. Вы можете использовать foreachс этим, но ничто не IEnumerable<T>гарантирует, что это будет конечно.
Джон Скит

42

С помощью кода «list» вам необходимо обработать полный список, прежде чем вы сможете передать его на следующий шаг. Версия «yield» передает обработанный элемент немедленно на следующий шаг. Если этот «следующий шаг» содержит «.Take (10)», то версия «yield» обработает только первые 10 элементов и забудет обо всех остальных. Код "list" обработал бы все.

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


23

Вы можете использовать yieldдля возврата товаров, которых нет в списке. Вот небольшой пример, который может бесконечно перебирать список, пока не будет отменен.

public IEnumerable<int> GetNextNumber()
{
    while (true)
    {
        for (int i = 0; i < 10; i++)
        {
            yield return i;
        }
    }
}

public bool Canceled { get; set; }

public void StartCounting()
{
    foreach (var number in GetNextNumber())
    {
        if (this.Canceled) break;
        Console.WriteLine(number);
    }
}

Это пишет

0
1
2
3
4
5
6
7
8
9
0
1
2
3
4

...и т.д. на консоль, пока не отменено.


10
object jamesItem = null;
foreach(var item in FilteredList())
{
   if (item.Name == "James")
   {
       jamesItem = item;
       break;
   }
}
return jamesItem;

Когда приведенный выше код используется для циклического обхода FilteredList () и в предположении, что item.Name == "James" будет удовлетворен для 2-го элемента в списке, использование метода yieldдаст два результата. Это ленивое поведение.

Где, как метод, использующий список, добавит все n объектов в список и передаст полный список вызывающему методу.

Это именно тот случай, когда можно выделить разницу между IEnumerable и IList.


7

Лучший пример из реальной жизни, который я видел для использования, - yieldэто вычисление последовательности Фибоначчи.

Рассмотрим следующий код:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(string.Join(", ", Fibonacci().Take(10)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(15).Take(1)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(10).Take(5)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(100).Take(1)));
        Console.ReadKey();
    }

    private static IEnumerable<long> Fibonacci()
    {
        long a = 0;
        long b = 1;

        while (true)
        {
            long temp = a;
            a = b;

            yield return a;

            b = temp + b;
        }
    }
}

Это вернет:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55
987
89, 144, 233, 377, 610
1298777728820984005

Это хорошо, потому что позволяет быстро и легко вычислять бесконечные ряды, давая вам возможность использовать расширения Linq и запрашивать только то, что вам нужно.


5
Я не вижу ничего «реального мира» в расчете последовательности Фибоначчи.
Nek

Я согласен, что на самом деле это не «реальный мир», а какая классная идея.
Кейси

1

зачем использовать [yield]? Помимо того, что это немного меньше кода, что это делает для меня?

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

Когда yield действительно сияет, это когда возвращается только частичный набор. Я думаю, что лучший пример - сортировка. Предположим, у вас есть список объектов, содержащих дату и сумму в долларах за этот год, и вы хотели бы увидеть первые несколько (5) записей года.

Для этого список должен быть отсортирован по возрастанию по дате, а затем взяты первые 5. Если бы это было сделано без выхода, весь список пришлось бы отсортировать, чтобы убедиться, что последние две даты были в порядке.

Однако с выходом, после того, как первые 5 элементов были установлены, сортировка прекращается, и результаты доступны. Это может сэкономить большое количество времени.


0

Заявление о возврате доходности позволяет вам возвращать только один товар за раз. Вы собираете все элементы в списке и снова возвращаете этот список, что приводит к нехватке памяти.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.