수색…


소개

문에서 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

.NET Fiddle에서의 라이브 데모

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

.NET Fiddle에서의 라이브 데모

인수를 올바르게 확인

리턴 값이 열거 될 때까지 반복자 메소드는 실행되지 않습니다. 그러므로 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

.NET Fiddle에서의 라이브 데모

메서드가 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 파일을 디 컴파일하는 것이 좋습니다) :

  1. C # 컴파일러는 IEnumerable<BigInteger>IEnumerator<BigInteger> (ildasm에서 <Fibonacci>d__0 을 구현하는 클래스를 생성합니다.
  2. 이 클래스는 상태 시스템을 구현합니다. 상태는 메소드의 현재 위치와 로컬 변수의 값으로 구성됩니다.
  3. 가장 흥미로운 코드는 bool IEnumerator.MoveNext() 메서드에 있습니다. 기본적으로 MoveNext() 는 다음을 수행합니다.
    • 현재 상태를 복원합니다. prevcurrent 와 같은 변수는 클래스의 필드가됩니다 (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;
    }


Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow