C# yield 사용 방법에 대한 질문

안녕하세요. :smile:

제가 이번에 C# yield return에 대해 알아보고 있는데요.
실무에서 단한번도 사용해본 적이 없기에, 이번 기회에 배워보고자 하는데요.

그래서 문법이나 사용 방법에 대해서도 알아보고 잘 동작하는지 테스트도 해봤습니다.

public IEnumerable<int> GetNumbers(int start, int max)
{
    for (int i = start; i < max; i ++)
    {
        yield return i;
    }
}

public IEnumerable<Email> Emails
{
    get
    {
        yield return new Email { Seq = 0, Name = "gmail.com" }
        yield return new Email { Seq = 1, Name = "outlook.com" }
        yield return new Email { Seq = 2, Name = "icloud.com" }
        yield return new Email { Seq = 3, Name = "naver.com" }
    }
}

하지만 이 기술을 어떤 상황에서 사용해야 하는지,
어떠한 부분이나 상황을 대처하면 좋을지 잘 이해하고 싶습니다.

질문을 읽어주셔서 감사합니다. :smile:

좋아요 1

IEnumerable<T>는 연속된 데이터를 제공할 수 있는지에 대한 인터페이스 입니다. 대표적인게 Array<T> 또는 List<T>이고요, IEnumerable<T>를 구현하기 위해서는 IEnumerator<T>를 제공해야 하는데 이는 연속된 데이터를 순회할 수 있는 인터페이스입니다.

대표적으로 사용되는 곳은 foreach입니다. foreach를 통해 연속된 데이터가 원자적으로 접근되며, 그 데이터를 하나씩 꺼내서 사용할 수 있게 되는데요,

이 인터페이스를 사용하게 되면 일반적으로 많이 쓰는 Array<T>IList<T>처럼 결정된 목록과 다른점이 있습니다.

결정된, 즉, 목록이 이미 완성이 되어 순회할 때 그 목록이 변경되지 않는 형태인데요,

var list = new[] { 1, 2, 3, 4, 5 };
foreach (var i in list)
{
   Console.WriteLine(i);
}

이런 목록을 결정된 목록이라고 할 수 있겠습니다.

비결정 목록은, 순회하는 그 시점, 심지어 순회하는 동안에도 원자 목록이 결정되지 않다는 의미인데요,
네트워크 데이터로 유사하게 이해하실 수 있습니다.

원격 데이터가 내 PC에 결과적으로 모두 저장될 때까지, 그 데이터의 목록은 내 PC에 결정적이지 않다고 말할 수 있습니다. 스트림 데이터라고도 하는데, 이런 성질의 데이터는 모두 비결정 데이터입니다.

비결정 데이터는 목록을 순회하면서 그 데이터의 스트리밍 상태(전이)가 변경될 수 도 있다는 것을 의미합니다.

yield는 이러한 비결정 데이터(목록)을 순차적 코드의 흐름으로 결정할 수 있게 도와줍니다. yield를 사용하지 않는다면 IEnumerator<T>의 구현체에서 이것을 해줘야 하는데, 분기문(if 또는 switch)이 들어가야 해서 소스코드가 이해하기 힘든 코드가 될 수 있는데, yield를 사용하게 되면 목록을 소비하는 쪽과 제공하는 쪽이 yield return의 기점으로 구분되기 때문에 코드가 깔끔해지게 됩니다.

  1. IEnumerable<T>은 연속된 데이터를 의미합니다.
  2. 이것을 스트림 데이터라고도 합니다.
  3. 이런 목록은 결정된것과 결정되지 않은 목록 모두를 의미합니다.
  4. 결정되지 않은 목록의 대표되는 예는 네트워크 스트림 입니다, 그리고 게임 프로그래밍에서 상태의 변경에 따라 다양한 모션 효과나 이벤트가 달라지게 되므로, 이것도 결정되지 않은 스트림이 됩니다.
  5. yield는 결정되지 않은 스트림을 순차적 코딩으로 효과적으로 구현할 수 있게 해줍니다.

비결정 스트림 데이터가 필요한 모든것에 yield를 적극적으로 사용할 수 있습니다.

좋아요 3

저는 yield 키워드를 자주 사용하는데, 주로 어떤 객체의 추상화된 데이터를 모두 뽑는 경우인 것 같아요.

아래 코드를 기준으로 보면 NumManager의 속성의 모든 아이템을 반환하는 코드(GetNumber())가 될 것 같아요.
단, 이때 주의할 점이 예제로 넣은 UpdateStatus() 함수의 동작 시점에 따라 GetNumber()의 결과가 달라질 수 있다는 점 입니다.
이 부분은 위의 @dimohy 님께서 설명해주셨으니 패스합니다.

interface ISomeNumber
{
	int Num { get; }
}

class PositiveNum : ISomeNumber
{
	public int Num { get; }
}

class NegativeNum : ISomeNumber
{
	public int Num { get; }
}

class InvalidNum : ISomeNumber
{
	public int Num { get; }
}

class NumManager
{
	public List<PositiveNum> PositiveNums { get; }
	public List<NegativeNum> NegativeNums { get; }
	public List<InvalidNum> InvalidNums { get; }

	public void UpdateStatus(...)
	{
		// Property의 값을 업데이트
	}

	public IEnumerable<ISomeNumber> GetNumber()
	{
		if (PositiveNums != null)
		{
			foreach (var num in PositiveNums)
				yield return num;
		}

		if (NegativeNums != null)
		{
			foreach (var num in NegativeNums)
				yield return num;
		}

		if (InvalidNums != null)
		{
			foreach (var num in InvalidNums)
				yield return num;
		}
	}
}
좋아요 3

사실 이 예제는 LINQ의 Concat을 이용하면 간단하게 처리할 수 있습니다.
그렇지만 대략 이런 느낌으로 확장할 수 있다는 것만 아셔도 좋을 것 같아요.

좋아요 1

좀 쓸데 없는 이야기가 많지만, 다음의 글도 읽어보시면 도움이 되실 것입니다.

.NET Framework: 444. clojure와 C#을 통해 이해하는 Sequence와 Vector형식의 차이점 (sysnet.pe.kr)

좋아요 2