C# Language
수익률 키워드
수색…
소개
문에서 yield 키워드를 사용하면 해당 메서드, 연산자 또는 get 접근자가 반복기임을 나타냅니다. yield를 사용하여 반복자를 정의하면 사용자 지정 컬렉션 형식에 대해 IEnumerable 및 IEnumerator 패턴을 구현할 때 명시 적 추가 클래스 (열거 형의 상태를 보유하는 클래스)가 필요하지 않습니다.
통사론
- 수익률 반환 [TYPE]
- yield break
비고
반환 형식이 IEnumerable
, IEnumerable<T>
, IEnumerator
또는 IEnumerator<T>
메서드에 yield
키워드를 넣으면 반환 형식 ( IEnumerable
또는 IEnumerator
)의 구현을 생성하도록 컴파일러에 지시합니다. 방법을 사용하여 각 결과를 얻습니다.
yield
키워드는 이론적으로 무제한 시퀀스의 "다음"요소를 반환하려는 경우에 유용하므로 사전에 전체 시퀀스를 계산할 수 없거나 반환하기 전에 전체 값 시퀀스를 계산할 경우 사용자에게 바람직하지 않은 일시 중지가 발생할 수 있습니다 .
yield break
는 또한 언제든지 서열을 종결하는데 사용될 수있다.
yield
키워드는 IEnumerable<T>
와 같은 반환 유형으로 반복기 인터페이스 유형을 요구하기 때문에 Task<IEnumerable<T>>
객체를 반환하므로 비동기 메서드에서는이를 사용할 수 없습니다.
추가 읽기
간단한 사용법
yield
키워드는 호출자가 반환 된 컬렉션을 반복 할 때 값이 느리게 생성되는 IEnumerable
또는 IEnumerator
(파생 된 일반 변형과 함께)를 반환하는 함수를 정의하는 데 사용됩니다. 비고 섹션 에서 목적에 대해 더 자세히 읽어보십시오.
다음 예제에서는 for
루프 안에 yield return 문이 있습니다.
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
foreach
문 본문을 반복하면 Count
iterator 함수가 호출됩니다. 반복 함수의 각 호출은 for
루프의 다음 반복 중에 발생하는 yield return
문의 다음 실행으로 진행됩니다.
더 적절한 사용
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);
}
}
}
물론 SQL 데이터베이스에서 IEnumerable<User>
를 가져 오는 다른 방법이 있습니다. 이는 " yield
of elements"의미가있는 항목을 누군가가 반복 할 수있는 IEnumerable<T>
로 변환 할 수 있다는 것을 보여줍니다. .
조기 해지
yield break
를 호출하여 내부 루프가 실행되지 않도록하여 함수 내에서 종료 조건을 정의 할 수있는 하나 이상의 값 또는 요소를 전달하여 기존 yield
메소드의 기능을 확장 할 수 있습니다.
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++;
}
}
위의 메서드는 earlyTerminationSet
내의 값 중 하나가 발생할 때까지 지정된 start
위치에서 반복합니다.
// 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
삼
4
5
6
인수를 올바르게 확인
리턴 값이 열거 될 때까지 반복자 메소드는 실행되지 않습니다. 그러므로 iterator 외부에서 전제 조건을 주장하는 것이 유리하다.
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
삼
4
5
6
7
8
9
10
메서드가 yield
를 사용하여 열거 형을 생성 할 때 컴파일러는 반복 될 때 코드를 yield
실행할 상태 시스템을 만듭니다. 그런 다음 반환 된 항목을 반환하고 상태를 저장합니다.
즉, 첫 번째 요소에 액세스하려고 할 때만 (즉, 상태 시스템을 생성하기 때문에) 메소드를 처음 호출 할 때 잘못된 인수 ( null
전달 등)에 대해 알지 못합니다. 메소드가 상태 머신에 의해 실행 됨). 인수를 먼저 검사하는 정상적인 방법으로 래핑하면 메소드가 호출 될 때 확인할 수 있습니다. 이것은 실패의 예입니다.
C # 7+를 사용하면 CountCore
함수를 로컬 함수 로 Count
함수에 편리하게 숨길 수 있습니다. 여기 예제를 참조 하십시오 .
Enumerable를 반환하는 메서드 내에서 다른 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);
}
}
그러면 다음과 같이 출력됩니다.
반복 시작
iterator 내부 : 0
내부 foreach : 0
반복자 내부 : 1
내부 foreach : 1
반복자 내부 : 2
내부 foreach : 2
결과로서:
- "Starting iteration"은 줄을 인쇄하기 전에 iterator 메서드가 호출되었지만 먼저 인쇄됩니다
Integers().Take(3);
실제로 반복을 시작하지 않습니다 (IEnumerator.MoveNext()
호출을하지 않음) - 콘솔에 출력하는 행은 iterator 메소드 내부의 행과
foreach
내부의 행간에 번갈아 표시됩니다. - 이 프로그램은 iterator 메소드가 결코 깨지지 않는
while true
를 가지고 있음에도 불구하고.Take()
메소드로 인해 종료됩니다.
Try ... finally
반복자 메서드가 try...finally
내에서 yield를 갖는 경우 반환 된 IEnumerator
는 현재 평가 점이 try
블록 내부에있는 한 Dispose
가 호출 될 때 finally
문을 실행합니다.
주어진 함수 :
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
키워드는 컬렉션의 lazy-evaluation을 허용합니다. 전체 콜렉션을 강제로 메모리에로드하는 것을 eager evaluation 이라고합니다.
다음 코드는 이것을 보여줍니다 :
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));
}
}
}
후드에서 작동하는 방법 (IL Disaambler 도구에서 결과 .exe 파일을 디 컴파일하는 것이 좋습니다) :
- C # 컴파일러는
IEnumerable<BigInteger>
및IEnumerator<BigInteger>
(ildasm에서<Fibonacci>d__0
을 구현하는 클래스를 생성합니다. - 이 클래스는 상태 시스템을 구현합니다. 상태는 메소드의 현재 위치와 로컬 변수의 값으로 구성됩니다.
- 가장 흥미로운 코드는
bool IEnumerator.MoveNext()
메서드에 있습니다. 기본적으로MoveNext()
는 다음을 수행합니다.- 현재 상태를 복원합니다.
prev
및current
와 같은 변수는 클래스의 필드가됩니다 (ildasm에서는<current>5__2
및<prev>5__1
). 우리의 방법에서 우리는 두 개의 위치 (<>1__state
1 개의 상태)를 가진다. 첫 번째는 중괄호 (curly brace)에서, 두 번째는yield return
이다. - 다음
yield return
또는yield break
/}
까지 코드를 실행합니다. -
yield return
결과 값은 저장되므로Current
속성은 결과 값을 반환 할 수 있습니다.true
가 리턴됩니다. 이 시점에서 현재 상태는 다음MoveNext
호출을 위해 다시 저장됩니다. -
yield break
/}
메소드의 경우 그냥 반복을 의미하는false
반환합니다.
- 현재 상태를 복원합니다.
또한 10001 번째 숫자는 468 바이트입니다. 상태 머신은 current
변수와 prev
변수를 필드로 저장합니다. 시퀀스의 모든 숫자를 첫 번째부터 10000 번째까지 저장하려면 소비 된 메모리 크기가 4MB를 초과합니다. 따라서 게으른 평가가 적절히 사용된다면 메모리 사용량을 줄일 수 있습니다.
휴식 시간과 수익률의 차이
사용 yield break
반대로 break
일이 생각만큼 분명하지 않을 수 있습니다. 두 가지의 사용법은 상호 교환이 가능하고 그 차이를 실제로 보여주지는 못하는 많은 나쁜 예가 인터넷에 있습니다.
혼란스러운 부분은 키워드 (또는 핵심 구문)가 루프 내에서만 의미가 있다는 것입니다 ( foreach
, while
...) 그래서 다른 키워드를 선택할 때?
메서드에서 yield
키워드를 사용하면 메서드를 효과적으로 반복자 로 바꿀 수 있다는 것을 깨닫는 것이 중요 합니다 . 그런 방법의 유일한 목적은 유한 또는 무한 컬렉션을 반복하고 그 요소를 산출 (출력)하는 것입니다. 목적이 달성되면 방법 실행을 계속할 이유가 없습니다. 때로는 메소드의 마지막 닫기 괄호로 자연스럽게 발생합니다. }
그러나 때때로, 당신은 조기에 방법을 끝내기를 원합니다. 정상적인 (iterating이 아닌) 메소드에서는 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;
}