Давайте немного перемотаем: ключевое слово yield
переведено, как многие другие говорили конечному автомату.
На самом деле это не совсем то же самое, что использование встроенной реализации, которая будет использоваться за кулисами, а скорее компилятор, переписывающий код, связанный с yield
, в конечный автомат путем реализации одного из соответствующих интерфейсов (возвращаемый тип метод, содержащий ключевые слова yield
).
A (конечный) конечный автомат - это просто кусок кода, который в зависимости от того, где вы находитесь в коде (в зависимости от предыдущего состояния, ввода), переходит к другому действию состояния, и это в значительной степени что происходит, когда вы используете метод и возвращаете метод с типом возврата IEnumerator<T>
/ IEnumerator
. Ключевое слово yield
- это то, что создает другое действие для перехода к следующему состоянию из предыдущего, поэтому управление состоянием создается в реализации MoveNext()
.
Именно это и собирается делать компилятор C # / Roslyn: проверить наличие ключевого слова yield
плюс тип возвращаемого значения содержащего метода, будь то IEnumerator<T>
, IEnumerable<T>
, IEnumerator
или IEnumerable
, а затем создайте закрытый класс, отражающий этот метод, объединяющий необходимые переменные и состояния.
Если вам интересны подробности того, как конечный автомат и как итерации переписываются компилятором, вы можете проверить эти ссылки на Github:
Викторины 1 : AsyncRewriter
(используется при написании кода async
/ await
также наследуется от StateMachineRewriter
, поскольку он также использует конечный автомат позади.
Как уже упоминалось, конечный автомат в значительной степени отражен в сгенерированной реализации 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
.
О второй части вопроса, т. Е. yield break
, на него ответили здесь
Указывает, что итератор подошел к концу. Вы можете думать о
yield break в качестве оператора возврата, который не возвращает значение.