Ответ 4
Давайте немного перемотаем: ключевое слово yield
переведено, как многие другие сказали конечному автомату.
На самом деле это не совсем похоже на использование встроенной реализации, которая будет использоваться за кулисами, а компилятор переписывает код, связанный с yield
в конечный автомат, реализуя один из соответствующих интерфейсов (возвращаемый тип метода, содержащего ключевые слова yield
).
(Конечный) конечный автомат - это просто кусок кода, который в зависимости от того, где вы находитесь в коде (в зависимости от предыдущего состояния, ввода), переходит к другому действию состояния, и это в значительной степени то, что происходит, когда вы используете и получаете с методом, возвращающим тип IEnumerator<T>
/IEnumerator
. Ключевое слово yield
- это то, что собирается создать другое действие для перехода к следующему состоянию из предыдущего, поэтому управление состоянием создается в реализации MoveNext()
.
Именно это и собирается делать компилятор C/Roslyn: проверить наличие ключевого слова yield
плюс тип возвращаемого значения содержащего метода, будь то IEnumerator<T>
, IEnumerable<T>
, IEnumerator
или IEnumerable
а затем создать закрытый класс, отражающий этот метод, объединяющий необходимые переменные и состояния.
Если вы заинтересованы в деталях того, как конечный автомат и как итерации переписываются компилятором, вы можете проверить эти ссылки на Github:
Общая информация 1: AsyncRewriter
(используется при написании кода async
/await
также наследуется от StateMachineRewriter
поскольку он также использует AsyncRewriter
автомат.
Как упоминалось выше, состояние машины сильно отражается в bool MoveNext()
генерируется выполнение в котором есть switch
+ иногда некоторые старомодный goto
на основе состояния поля, которое представляет различные пути исполнения для различных состояний в вашем методе.
Код, сгенерированный компилятором из пользовательского кода, выглядит не очень хорошо, в основном потому, что компилятор добавляет некоторые странные префиксы и суффиксы тут и там
Например, код:
public class TestClass
{
private int _iAmAHere = 0;
public IEnumerator<int> DoSomething()
{
var start = 1;
var stop = 42;
var breakCondition = 34;
var exceptionCondition = 41;
var multiplier = 2;
// Rest of the code... with some yield keywords somewhere below...
Переменные и типы, связанные с этим фрагментом кода выше, после компиляции будут выглядеть так:
public class TestClass
{
[CompilerGenerated]
private sealed class <DoSomething>d__1 : IEnumerator<int>, IDisposable, IEnumerator
{
// Always present
private int <>1__state;
private int <>2__current;
// Containing class
public TestClass <>4__this;
private int <start>5__1;
private int <stop>5__2;
private int <breakCondition>5__3;
private int <exceptionCondition>5__4;
private int <multiplier>5__5;
Что касается самого конечного автомата, давайте взглянем на очень простой пример с фиктивным ветвлением для получения некоторых четных/нечетных вещей.
public class Example
{
public IEnumerator<string> DoSomething()
{
const int start = 1;
const int stop = 42;
for (var index = start; index < stop; index++)
{
yield return index % 2 == 0 ? "even" : "odd";
}
}
}
Будет переведено в MoveNext
как:
private bool MoveNext()
{
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
<start>5__1 = 1;
<stop>5__2 = 42;
<index>5__3 = <start>5__1;
break;
case 1:
<>1__state = -1;
goto IL_0094;
case 2:
{
<>1__state = -1;
goto IL_0094;
}
IL_0094:
<index>5__3++;
break;
}
if (<index>5__3 < <stop>5__2)
{
if (<index>5__3 % 2 == 0)
{
<>2__current = "even";
<>1__state = 1;
return true;
}
<>2__current = "odd";
<>1__state = 2;
return true;
}
return false;
}
Как видите, эта реализация далеко не прямолинейна, но она делает свою работу!
Общая информация 2. Что происходит с возвращаемым типом метода IEnumerable
/IEnumerable<T>
?
Что ж, вместо того, чтобы просто генерировать класс, реализующий IEnumerator<T>
, он будет генерировать класс, который реализует как IEnumerable<T>
так и IEnumerator<T>
так что реализация IEnumerator<T> GetEnumerator()
будет использовать тот же сгенерированный класс.
Теплое напоминание о нескольких интерфейсах, которые реализуются автоматически при использовании ключевого слова yield
:
public interface IEnumerable<out T> : IEnumerable
{
new IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
T Current { get; }
}
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
Вы также можете проверить этот пример с другими путями/ветвлениями и полной реализацией путем переписывания компилятором.
Это было создано с SharpLab, вы можете поиграть с этим инструментом, чтобы попробовать разные пути выполнения, связанные с yield
и посмотреть, как компилятор перезапишет их как MoveNext
автомат в реализации MoveNext
.
О второй части вопроса, т. yield break
, ответили здесь
Это указывает, что итератор подошел к концу. Вы можете думать о yield break как о выражении возврата, которое не возвращает значение.