C# Language
Ключевое слово доходности
Поиск…
Вступление
Когда вы используете ключевое слово yield в инструкции, вы указываете, что метод, оператор или get accessor, в котором он отображается, является итератором. Использование yield для определения итератора устраняет необходимость в явном дополнительном классе (класс, который содержит состояние для перечисления) при реализации шаблона IEnumerable и IEnumerator для пользовательского типа коллекции.
Синтаксис
- return return [TYPE]
- разрыв дохода
замечания
Полагая ключевое слово yield
в методе с типом возвращаемого значения IEnumerable
, IEnumerable<T>
, IEnumerator
или IEnumerator<T>
сообщает компилятору сгенерировать реализацию возвращаемого типа ( IEnumerable
или IEnumerator
), который при циклическом запуске метод до каждого «урожая», чтобы получить каждый результат.
Ключевое слово yield
полезно, когда вы хотите вернуть «следующий» элемент теоретически неограниченной последовательности, поэтому вычисление всей последовательности заблаговременно было бы невозможно или при вычислении полной последовательности значений перед возвратом приводило бы к нежелательной паузе для пользователя ,
yield break
также может быть использован для прекращения последовательности в любое время.
Поскольку ключевому слову yield
требуется тип интерфейса итератора в качестве возвращаемого типа, например IEnumerable<T>
, вы не можете использовать его в методе async, так как это возвращает объект Task<IEnumerable<T>>
.
дальнейшее чтение
Простое использование
Ключевое слово yield
используется для определения функции, которая возвращает IEnumerable
или IEnumerator
(а также их производные обобщенные варианты), значения которых генерируются лениво, когда вызывающий выполняет итерацию по возвращенной коллекции. Подробнее о цели в разделе замечаний .
В следующем примере есть оператор возврата доходности, который находится внутри цикла for
.
public static IEnumerable<int> Count(int start, int count)
{
for (int i = 0; i <= count; i++)
{
yield return start + i;
}
}
Тогда вы можете назвать это:
foreach (int value in Count(start: 4, count: 10))
{
Console.WriteLine(value);
}
Консольный выход
4
5
6
...
14
Живая демонстрация на .NET скрипке
Каждая итерация тела оператора foreach
создает вызов функции итератора Count
. Каждый вызов функции итератора переходит к следующему исполнению оператора yield return
, который возникает во время следующей итерации цикла for
.
Более подходящее использование
public IEnumerable<User> SelectUsers()
{
// Execute an SQL query on a database.
using (IDataReader reader = this.Database.ExecuteReader(CommandType.Text, "SELECT Id, Name FROM Users"))
{
while (reader.Read())
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);
yield return new User(id, name);
}
}
}
Конечно, есть другие способы получить IEnumerable<User>
из базы данных SQL - это просто демонстрирует, что вы можете использовать yield
чтобы превратить все, что имеет семантику «последовательность элементов» в IEnumerable<T>
которую кто-то может перебирать через ,
Раннее прекращение
Вы можете расширить функциональность существующих методов yield
, передав одно или несколько значений или элементов, которые могли бы определить условие завершения в функции, вызвав yield break
чтобы остановить выполнение внутреннего цикла.
public static IEnumerable<int> CountUntilAny(int start, HashSet<int> earlyTerminationSet)
{
int curr = start;
while (true)
{
if (earlyTerminationSet.Contains(curr))
{
// we've hit one of the ending values
yield break;
}
yield return curr;
if (curr == Int32.MaxValue)
{
// don't overflow if we get all the way to the end; just stop
yield break;
}
curr++;
}
}
Вышеуказанный метод будет перебирать из заданной start
позиции до тех пор, пока не будет earlyTerminationSet
одно из значений в earlyTerminationSet
.
// Iterate from a starting point until you encounter any elements defined as
// terminating elements
var terminatingElements = new HashSet<int>{ 7, 9, 11 };
// This will iterate from 1 until one of the terminating elements is encountered (7)
foreach(var x in CountUntilAny(1,terminatingElements))
{
// This will write out the results from 1 until 7 (which will trigger terminating)
Console.WriteLine(x);
}
Выход:
1
2
3
4
5
6
Живая демонстрация на .NET скрипке
Правильно проверяющие аргументы
Метод итератора не выполняется до тех пор, пока не будет перечислено возвращаемое значение. Поэтому выгодно утверждать предпосылки за пределами итератора.
public static IEnumerable<int> Count(int start, int count)
{
// The exception will throw when the method is called, not when the result is iterated
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));
return CountCore(start, count);
}
private static IEnumerable<int> CountCore(int start, int count)
{
// If the exception was thrown here it would be raised during the first MoveNext()
// call on the IEnumerator, potentially at a point in the code far away from where
// an incorrect value was passed.
for (int i = 0; i < count; i++)
{
yield return start + i;
}
}
Вызов бокового кода (использование):
// Get the count
var count = Count(1,10);
// Iterate the results
foreach(var x in count)
{
Console.WriteLine(x);
}
Выход:
1
2
3
4
5
6
7
8
9
10
Живая демонстрация на .NET скрипке
Когда метод использует yield
для генерации перечислимого, компилятор создает конечный автомат, который при повторном запуске будет работать с кодом до yield
. Затем он возвращает полученный элемент и сохраняет его состояние.
Это означает, что вы не узнаете о недопустимых аргументах (передаете null
и т. Д.) При первом вызове метода (поскольку это создает конечный автомат), только когда вы пытаетесь получить доступ к первому элементу (потому что только тогда код внутри метод get запускается конечным автоматом). Обернув его обычным методом, который сначала проверяет аргументы, вы можете проверить их при вызове метода. Это пример неудачи.
При использовании C # 7+ функция CountCore
может быть удобно скрыта в функции Count
как локальная функция . См. Пример здесь .
Возвращает другой Перечислимый в методе, возвращающем Enumerable
public IEnumerable<int> F1()
{
for (int i = 0; i < 3; i++)
yield return i;
//return F2(); // Compile Error!!
foreach (var element in F2())
yield return element;
}
public int[] F2()
{
return new[] { 3, 4, 5 };
}
Ленивая оценка
Только когда оператор foreach
переходит к следующему элементу, блок итератора оценивает до следующего оператора yield
.
Рассмотрим следующий пример:
private IEnumerable<int> Integers()
{
var i = 0;
while(true)
{
Console.WriteLine("Inside iterator: " + i);
yield return i;
i++;
}
}
private void PrintNumbers()
{
var numbers = Integers().Take(3);
Console.WriteLine("Starting iteration");
foreach(var number in numbers)
{
Console.WriteLine("Inside foreach: " + number);
}
}
Это приведет к выводу:
Запуск итерации
Внутри итератора: 0
Внутри foreach: 0
Внутри итератора: 1
Внутри foreach: 1
Внутри итератора: 2
Внутри foreach: 2
Как следствие:
- «Начальная итерация» сначала печатается, даже если метод итератора был вызван до того, как строка напечатала его, потому что строка
Integers().Take(3);
на самом деле не запускает итерацию (не былIEnumerator.MoveNext()
вызовIEnumerator.MoveNext()
) - Линии, печатающие на консоли, чередуются между тем, который находится внутри метода итератора, и внутри внутри
foreach
, а не все внутри метода итератора, оценивая сначала - Эта программа завершается из-за метода
.Take()
, хотя метод итератора имеет значениеwhile true
которое никогда не прерывается.
Попробуйте ... наконец-то
Если метод итератора имеет выход внутри try...finally
, возвращаемый IEnumerator
выполнит оператор finally
когда Dispose
вызывается на нем, если текущая точка оценки находится внутри блока try
.
С учетом функции:
private IEnumerable<int> Numbers()
{
yield return 1;
try
{
yield return 2;
yield return 3;
}
finally
{
Console.WriteLine("Finally executed");
}
}
При звонке:
private void DisposeOutsideTry()
{
var enumerator = Numbers().GetEnumerator();
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.Dispose();
}
Затем он печатает:
1
При звонке:
private void DisposeInsideTry()
{
var enumerator = Numbers().GetEnumerator();
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.Dispose();
}
Затем он печатает:
1
2
Наконец выполнен
Использование yield для создания IEnumerator при внедрении IEnumerable
Интерфейс IEnumerable<T>
имеет единственный метод GetEnumerator()
, который возвращает IEnumerator<T>
.
Хотя ключевое слово yield
можно использовать для непосредственного создания IEnumerable<T>
, его также можно использовать точно так же, чтобы создать IEnumerator<T>
. Единственное, что изменяется, это тип возвращаемого метода.
Это может быть полезно, если мы хотим создать собственный класс, который реализует IEnumerable<T>
:
public class PrintingEnumerable<T> : IEnumerable<T>
{
private IEnumerable<T> _wrapped;
public PrintingEnumerable(IEnumerable<T> wrapped)
{
_wrapped = wrapped;
}
// This method returns an IEnumerator<T>, rather than an IEnumerable<T>
// But the yield syntax and usage is identical.
public IEnumerator<T> GetEnumerator()
{
foreach(var item in _wrapped)
{
Console.WriteLine("Yielding: " + item);
yield return item;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
(Обратите внимание, что этот конкретный пример является просто иллюстративным и может быть более чисто реализован с помощью одного метода итератора, возвращающего IEnumerable<T>
.)
Простая оценка
Ключевое слово yield
позволяет ленивую оценку коллекции. Принудительная загрузка всей коллекции в память называется нетерпеливой оценкой .
Следующий код показывает это:
IEnumerable<int> myMethod()
{
for(int i=0; i <= 8675309; i++)
{
yield return i;
}
}
...
// define the iterator
var it = myMethod.Take(3);
// force its immediate evaluation
// list will contain 0, 1, 2
var list = it.ToList();
Вызов ToList
, ToDictionary
или ToArray
заставит немедленную оценку перечисления, извлекая все элементы в коллекцию.
Ленивый пример оценки: номера Фибоначчи
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; // also add reference to System.Numberics
namespace ConsoleApplication33
{
class Program
{
private static IEnumerable<BigInteger> Fibonacci()
{
BigInteger prev = 0;
BigInteger current = 1;
while (true)
{
yield return current;
var next = prev + current;
prev = current;
current = next;
}
}
static void Main()
{
// print Fibonacci numbers from 10001 to 10010
var numbers = Fibonacci().Skip(10000).Take(10).ToArray();
Console.WriteLine(string.Join(Environment.NewLine, numbers));
}
}
}
Как это работает под капотом (я рекомендую декомпилировать полученный файл .exe в инструменте IL Disaambler):
- Компилятор C # генерирует класс, реализующий
IEnumerable<BigInteger>
иIEnumerator<BigInteger>
(<Fibonacci>d__0
in ildasm). - Этот класс реализует конечный автомат. Состояние состоит из текущей позиции в методе и значениях локальных переменных.
- Самый интересный код - в методе
bool IEnumerator.MoveNext()
. В основном, чтоMoveNext()
делает:- Восстанавливает текущее состояние. Переменные типа
prev
иcurrent
становятся полями в нашем классе (<current>5__2
и<prev>5__1
in ildasm). В нашем методе мы имеем две позиции (<>1__state
): сначала в открывающей фигурной скобке, второй - приyield return
. - Выполняет код до следующего
yield return
yield break
илиyield break
/}
. - Для
yield return
результатаyield return
значение сохраняется, поэтомуCurrent
свойство может вернуть его.true
возвращается. В этот момент текущее состояние сохраняется снова для следующего вызоваMoveNext
. - Для метода
yield break
/}
метод возвращаетfalse
итерация выполняется.
- Восстанавливает текущее состояние. Переменные типа
Также обратите внимание, что 10001-е число составляет 468 байт. State machine сохраняет только current
и prev
переменные как поля. Хотя если мы хотим сохранить все числа в последовательности от первой до 10000-й, объем потребляемой памяти будет превышать 4 мегабайта. Такая ленивая оценка, если она правильно используется, в некоторых случаях может уменьшить объем памяти.
Разница между разрывом и выходом
Использование yield break
в отличие от break
может быть не столь очевидным, как можно подумать. В Интернете много плохих примеров, где использование этих двух взаимозаменяемых и на самом деле не демонстрирует разницы.
Запутанная часть состоит в том, что оба ключевых слова (или ключевые фразы) имеют смысл только внутри циклов ( foreach
, while
...). Когда выбирать один за другим?
Важно понимать, что как только вы используете ключевое слово yield
в методе, вы эффективно превращаете этот метод в итератор . Единственная цель такого метода состоит в том, чтобы затем перебирать конечный или бесконечный набор и выводить (выводить) его элементы. Как только цель будет выполнена, нет причин продолжать выполнение метода. Иногда это происходит, естественно, с последней закрывающей скобкой метода }
. Но иногда вы хотите досрочно завершить метод. В обычном (без повторения) методе вы должны использовать ключевое слово return
. Но вы не можете использовать return
в итераторе, вы должны использовать yield break
. Другими словами, yield break
для итератора совпадает с return
стандартного метода. Принимая во внимание, что оператор break
просто завершает ближайший цикл.
Давайте посмотрим несколько примеров:
/// <summary>
/// Yields numbers from 0 to 9
/// </summary>
/// <returns>{0,1,2,3,4,5,6,7,8,9}</returns>
public static IEnumerable<int> YieldBreak()
{
for (int i = 0; ; i++)
{
if (i < 10)
{
// Yields a number
yield return i;
}
else
{
// Indicates that the iteration has ended, everything
// from this line on will be ignored
yield break;
}
}
yield return 10; // This will never get executed
}
/// <summary>
/// Yields numbers from 0 to 10
/// </summary>
/// <returns>{0,1,2,3,4,5,6,7,8,9,10}</returns>
public static IEnumerable<int> Break()
{
for (int i = 0; ; i++)
{
if (i < 10)
{
// Yields a number
yield return i;
}
else
{
// Terminates just the loop
break;
}
}
// Execution continues
yield return 10;
}