C #에서 "yield"키워드의 실제 사용


76

거의 4 년의 경험 끝에, yield 키워드가 사용되는 코드를 보지 못했습니다 . 누군가이 키워드의 실제 사용법 (설명과 함께)을 보여 줄 수 있습니까? 그렇다면 가능한 방법을 쉽게 채우는 다른 방법이 있습니까?


9
LINQ의 모든 (또는 최소한)는 yield를 사용하여 구현됩니다. 또한 Unity3D 프레임 워크는 그것을 잘 사용했습니다. 함수를 계산하고 나중에 IEnumerable의 상태를 사용하여 다시 시작하는 데 사용됩니다.
Dani

2
이것을 StackOverflow로 이동해서는 안됩니까?
Danny Varod

4
@ 대니-질문은 특정 문제를 해결하는 것이 아니라 yield일반적으로 사용할 수있는 것에 대해 묻는 것이기 때문에 스택 오버플로에 적합하지 않습니다 .
ChrisF

9
진짜? 사용 하지 않은 단일 앱을 생각할 수 없습니다 .
Aaronaught

답변:


107

능률

yield키워드는 훨씬 더 효율적인 컬렉션 항목에 대해 지연 열거를 효과적으로 만듭니다. 예를 들어 foreach루프가 백만 개의 항목 중 처음 5 개 항목을 반복하는 경우 모두 yield반환되며 내부적으로 먼저 백만 개의 항목을 수집하지 않았습니다. 마찬가지로 동일한 효율성을 달성하기 위해 자체 프로그래밍 시나리오에서 리턴 값 yield과 함께 사용하려고합니다 IEnumerable<T>.

특정 시나리오에서 얻는 효율성의 예

반복자 방법이 아니고 잠재적으로 큰 컬렉션을 비효율적으로 사용할 수 있습니다
(중간 컬렉션은 많은 항목으로 구성됨)

// Method returns all million items before anything can loop over them. 
List<object> GetAllItems() {
    List<object> millionCustomers;
    database.LoadMillionCustomerRecords(millionCustomers); 
    return millionCustomers;
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in GetAllItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: One million items returned, but only 5 used. 

반복자 버전, 효율적
(중간 컬렉션이 작성되지 않음)

// Yields items one at a time as the caller's foreach loop requests them
IEnumerable<object> IterateOverItems() {
    for (int i; i < database.Customers.Count(); ++i)
        yield return database.Customers[i];
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in IterateOverItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: Only 5 items were yielded and used out of the million.

일부 프로그래밍 시나리오 단순화

다른 경우에는 목록을 정렬하고 병합하는 yield것이 중간 모음으로 정렬되어 스왑되지 않고 원하는 순서로 항목을 다시 정렬하기 때문에 목록을 정렬하고 병합하는 것이 더 쉽습니다 . 많은 시나리오가 있습니다.

한 가지 예는 두 목록의 병합입니다.

IEnumerable<object> EfficientMerge(List<object> list1, List<object> list2) {
    foreach(var o in list1) 
        yield return o; 
    foreach(var o in list2) 
        yield return o;
}

이 방법을 사용하면 하나의 연속 된 항목 목록이 생성되므로 중간 컬렉션이 필요없는 병합이 효과적으로 이루어집니다.

더 많은 정보

yield키워드 만 (리턴 형 갖는 반복자 방법의 맥락에서 사용될 수있는 IEnumerable, IEnumerator, IEnumerable<T>, 또는 IEnumerator<T>.)과 함께 특별한 관계가있다 foreach. 반복자는 특별한 방법입니다. MSDN 항복 문서반복자 문서는 흥미로운 정보와 개념에 대한 설명을 많이 포함되어 있습니다. 과의 상관 관계를해야합니다 키워드 반복자의 이해를 보완하기 위해, 너무 그것에 대해 읽어.foreach

반복자가 효율성을 달성하는 방법을 배우려면 비밀은 C # 컴파일러가 생성 한 IL 코드에 있습니다. 반복자 방법으로 생성 된 IL은 일반 (비 반복자) 방법으로 생성 된 IL과 크게 다릅니다. 이 기사 (수익 키워드는 실제로 무엇을 생성합니까?)에서 이러한 종류의 통찰력을 제공합니다.


2
그것들은 (아마도 긴) 시퀀스를 취하고 매핑이 일대일이 아닌 다른 시퀀스를 생성하는 알고리즘에 특히 유용합니다. 이에 대한 예는 다각형 클리핑입니다. 임의의 특정 에지는 일단 클리핑되면 에지를 다수 생성하거나 전혀 생성하지 않을 수있다. 반복자를 사용하면 표현하기가 훨씬 쉬워지고 항복이 가장 좋은 방법 중 하나입니다.
Donal Fellows

+1 내가 적었을 때 훨씬 더 나은 답변. 이제는 더 나은 성능을 위해 수율이 좋다는 것도 알게되었습니다.
Jan_V

3
옛날 옛적에, 나는 바이너리 네트워크 프로토콜을위한 패킷을 만들기 위해 yield를 사용했다. C #에서 가장 자연스러운 선택이었습니다.
György Andrasek

4
database.Customers.Count()전체 고객 열거를 열거 하지 않으므로 모든 항목을 통과하는 데 더 효율적인 코드가 필요합니까?
Stephen

5
나를 항문이라고 불러라. 그러나 그것은 병합이 아니라 연결이다. (그리고 linq는 이미 Concat 메소드를 가지고 있습니다.)
OldFart

4

얼마 전에 나는 실제적인 예를 보았습니다. 다음과 같은 상황이 있다고 가정 해 봅시다.

List<Button> buttons = new List<Button>();
void AddButtons()
{
   for ( int i = 0; i <= 10; i++ ) {
      var button = new Button();
      buttons.Add(button);
      button.Click += (sender, e) => 
          MessageBox.Show(String.Format("You clicked button number {0}", ???));
   }
}

버튼 객체는 컬렉션에서 자신의 위치를 ​​알지 못합니다. 동일한 제한을 적용 Dictionary<T>또는 다른 유형 모음.

yield키워드를 사용하는 솔루션은 다음과 같습니다 .

interface IHasId { int Id { get; set; } }

class IndexerList<T>: List<T>, IEnumerable<T> where T: IHasId
{
   List<T> elements = new List<T>();
   new public void Clear() { elements.Clear(); }
   new public void Add(T element) { elements.Add(element); }
   new public int Count { get { return elements.Count; } }    
   new public IEnumerator<T> GetEnumerator()
   {
      foreach ( T c in elements )
         yield return c;
   }

   new public T this[int index]
   {
      get
      {
         foreach ( T c in elements ) {
            if ( (int)c.Id == index )
               return c;
         }
         return default(T);
      }
   }
}

그리고 그것이 내가 그것을 사용하는 방법입니다.

class ButtonWithId: Button, IHasId
{
   public int Id { get; private set; }
   public ButtonWithId(int id) { this.Id = id; }
}

IndexerList<ButtonWithId> buttons = new IndexerList<ButtonWithId>();
void AddButtons()
{
   for ( int i = 10; i <= 20; i++ ) {
      var button = new ButtonWithId(i);
      buttons.Add(button);
      button.Click += (sender, e) => 
         MessageBox.Show(String.Format("You clicked button number {0}", ( (ButtonWithId)sender ).Id));
   }
}

for색인을 찾기 위해 내 컬렉션을 반복 할 필요가 없습니다 . 내 버튼에는 ID가 있으며이 색인은에서 색인으로도 사용 IndexerList<T>되므로 중복 ID 또는 색인을 피하십시오 -그것이 내가 좋아하는 것입니다! 색인 / ID는 임의의 숫자 일 수 있습니다.


2

실용적인 예는 다음과 같습니다.

http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html

표준 코드보다 yield를 사용하면 여러 가지 장점이 있습니다.

  • 반복자가 목록을 작성하는 데 사용되는 경우 리턴을 산출 할 수 있으며 호출자 는 해당 결과를 목록에 원하는지 여부를 결정할 수 있습니다 .
  • 호출자는 반복에서 수행중인 작업의 범위를 벗어난 이유로 반복을 취소하기로 결정할 수도 있습니다.
  • 코드가 약간 짧습니다.

그러나 Jan_V가 말했듯이 (몇 초 만에 나를 이겼습니다 :-) 내부적으로 컴파일러는 두 경우 모두 거의 동일한 코드를 생성하기 때문에 그것 없이도 살 수 있습니다.


1

예를 들면 다음과 같습니다.

https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158

수업은 주중에 날짜 계산을 수행합니다. 밥이 12시 30 분에 점심 시간으로 쉬는 시간으로 밥이 매주 9:30부터 17:30까지 일한다고 수업의 한 예를 말할 수 있습니다. 이 정보를 바탕으로 AscendingShifts () 함수는 제공된 날짜 사이에 작업 시프트 오브젝트를 생성합니다. 올해 1 월 1 일에서 2 월 1 일 사이에 Bob의 모든 근무 교대를 나열하려면 다음과 같이 사용하십시오.

foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
    Console.WriteLine(shift);
}

클래스는 실제로 컬렉션을 반복하지 않습니다. 그러나 두 날짜 간의 이동은 컬렉션으로 생각할 수 있습니다. yield연산자 가능 수집 자체를 만들지 않고 상상이 컬렉션을 반복 할 수있다.


1

commandSQL 명령 텍스트, 명령 유형을 설정하고 IEnumerable 'command parameters'를 반환하는 클래스 가있는 작은 db 데이터 계층이 있습니다 .

기본적으로 아이디어는 SqlCommand항상 수동으로 속성과 매개 변수를 채우는 대신 CLR 명령을 입력 하는 것입니다.

따라서 다음과 같은 기능이 있습니다.

IEnumerable<DbParameter> GetParameters()
{
    // here i do something like

    yield return new DbParameter { name = "@Age", value = this.Age };

    yield return new DbParameter { name = "@Name", value = this.Name };
}

이 상속 클래스 command클래스는 특성을 가지고 AgeName.

그런 다음 command속성으로 채워진 객체를 새로 만들어 db실제로 명령 호출 을 수행하는 인터페이스로 전달할 수 있습니다.

대체로 SQL 명령으로 작업하고 입력을 유지하는 것이 정말 쉽습니다.


1

합병 사례가 이미 수용된 답변에서 다루어졌지만 yield-merge params extension method ™를 보여 드리겠습니다.

public static IEnumerable<T> AppendParams<T>(this IEnumerable<T> a, params T[] b)
{
    foreach (var el in a) yield return el;
    foreach (var el in b) yield return el;
}

이것을 사용하여 네트워크 프로토콜의 패킷을 만듭니다.

static byte[] MakeCommandPacket(string cmd)
{
    return
        header
        .AppendParams<byte>(0, 0, 1, 0, 0, 1, 0x92, 0, 0, 0, 0)
        .AppendAscii(cmd)
        .MarkLength()
        .MarkChecksum()
        .ToArray();
}

MarkChecksum방법은, 예를 들어, 다음과 같습니다. 그리고 그것도 yield있습니다 :

public static IEnumerable<byte> MarkChecksum(this IEnumerable<byte> data, int pos = 6)
{
    foreach (byte b in data)
    {
        yield return pos-- == 0 ? (byte)data.Sum(z => z) : b;
    }
}

그러나 열거 메서드에서 Sum ()과 같은 집계 메서드를 사용할 때는 별도의 열거 프로세스를 트리거하므로주의하십시오.


1

Elastic Search .NET 예제 repo에는 yield return컬렉션을 지정된 크기의 여러 컬렉션으로 분할하는 데 사용하는 훌륭한 예제가 있습니다.

https://github.com/elastic/elasticsearch-net-example/blob/master/src/NuSearch.Domain/Extensions/PartitionExtension.cs

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
    {
        T[] array = null;
        int count = 0;
        foreach (T item in source)
        {
            if (array == null)
            {
                array = new T[size];
            }
            array[count] = item;
            count++;
            if (count == size)
            {
                yield return new ReadOnlyCollection<T>(array);
                array = null;
                count = 0;
            }
        }
        if (array != null)
        {
            Array.Resize(ref array, count);
            yield return new ReadOnlyCollection<T>(array);
        }
    }

0

Jan_V의 답변을 확장하면서 나는 실제로 관련된 사례를 보았습니다.

FindFirstFile / FindNextFile의 Kernel32 버전을 사용해야했습니다. 첫 번째 통화에서 핸들을 가져 와서 모든 후속 통화에 공급합니다. 이것을 열거 자에 싸면 foreach와 함께 직접 사용할 수있는 것을 얻습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.