반복 메소드가 'Ref'또는 'out'매개 변수를 취할 수없는 이유는 무엇입니까?

StackOverflow https://stackoverflow.com/questions/999020

  •  14-09-2019
  •  | 
  •  

문제

나는 오늘 일찍 이것을 시도했다 :

public interface IFoo
{
    IEnumerable<int> GetItems_A( ref int somethingElse );
    IEnumerable<int> GetItems_B( ref int somethingElse );
}


public class Bar : IFoo
{
    public IEnumerable<int> GetItems_A( ref int somethingElse )
    {
        // Ok...
    }

    public IEnumerable<int> GetItems_B( ref int somethingElse )
    {
        yield return 7; // CS1623: Iterators cannot have ref or out parameters            

    }
}

이 배후의 근거는 무엇입니까?

도움이 되었습니까?

해결책

C# 반복자는 내부적으로 상태 기계입니다. 당신이 할 때마다 yield return 무언가, 당신이 중단 한 곳은 로컬 변수의 상태와 함께 저장되어 다시 돌아와서 계속해서 계속할 수 있습니다.

이 상태를 유지하기 위해 C# Compiler는 로컬 변수와 계속 해야하는 장소를 보유하는 클래스를 만듭니다. 가질 수 없습니다 ref 또는 out 수업에서 필드로서 가치. 결과적으로 매개 변수를 다음과 같이 선언 할 수있는 경우 ref 또는 out, 우리가 중단했을 때 함수의 완전한 스냅 샷을 유지할 방법이 없을 것입니다.

편집하다: 기술적으로, 돌아 오는 모든 방법은 아닙니다 IEnumerable<T> 반복자로 간주됩니다. 사용하는 사람들 yield 시퀀스를 직접 생성하는 것은 반복자로 간주됩니다. 따라서 반복기를 두 가지 방법으로 분할하는 것은 멋지고 일반적인 해결 방법이지만 방금 말한 것과 모순되지 않습니다. 외부 방법 (사용하지 않는 방법 yield 직접)입니다 ~ 아니다 반복자로 간주됩니다.

다른 팁

메소드에서 반복기와 int를 모두 반환하려면 해결 방법이 다음과 같습니다.

public class Bar : IFoo
{
    public IEnumerable<int> GetItems( ref int somethingElse )
    {
        somethingElse = 42;
        return GetItemsCore();
    }

    private IEnumerable<int> GetItemsCore();
    {
        yield return 7;
    }
}

반복자 메소드 내부의 코드는 없음 (즉, 기본적으로 포함하는 메소드 yield return 또는 yield break)까지 실행됩니다 MoveNext() 열거 자의 메소드가 호출됩니다. 그래서 당신이 사용할 수 있다면 out 또는 ref 반복자 방법에서는 다음과 같은 놀라운 동작을 얻을 수 있습니다.

// This will not compile:
public IEnumerable<int> GetItems( ref int somethingElse )
{
    somethingElse = 42;
    yield return 7;
}

// ...
int somethingElse = 0;
IEnumerable<int> items = GetItems( ref somethingElse );
// at this point somethingElse would still be 0
items.GetEnumerator().MoveNext();
// but now the assignment would be executed and somethingElse would be 42

이것은 일반적인 함정이며 관련 문제는 다음과 같습니다.

public IEnumerable<int> GetItems( object mayNotBeNull ){
  if( mayNotBeNull == null )
    throw new NullPointerException();
  yield return 7;
}

// ...
IEnumerable<int> items = GetItems( null ); // <- This does not throw
items.GetEnumerators().MoveNext();                    // <- But this does

따라서 좋은 패턴은 반복자 메소드를 두 부분으로 분리하는 것입니다. 하나는 즉시 실행하고 게으르게 실행 해야하는 코드를 포함하는 것입니다.

public IEnumerable<int> GetItems( object mayNotBeNull ){
  if( mayNotBeNull == null )
    throw new NullPointerException();
  // other quick checks
  return GetItemsCore( mayNotBeNull );
}

private IEnumerable<int> GetItemsCore( object mayNotBeNull ){
  SlowRunningMethod();
  CallToDatabase();
  // etc
  yield return 7;
}    
// ...
IEnumerable<int> items = GetItems( null ); // <- Now this will throw

편집하다:반복기를 움직이는 동작을 정말로 원한다면 ref-파라미터, 당신은 다음과 같은 일을 할 수 있습니다.

public static IEnumerable<int> GetItems( Action<int> setter, Func<int> getter )
{
    setter(42);
    yield return 7;
}

//...

int local = 0;
IEnumerable<int> items = GetItems((x)=>{local = x;}, ()=>local);
Console.WriteLine(local); // 0
items.GetEnumerator().MoveNext();
Console.WriteLine(local); // 42

높은 수준에서 REF 변수는 스택에있는 가치 유형을 포함하여 많은 위치를 가리킬 수 있습니다. 반복자 메소드를 호출하여 반복자가 처음 만들어지는 시간과 REF 변수가 할당 된 시점은 매우 다른 두 시간입니다. 원래 참조로 전달 된 변수가 반복자가 실제로 실행될 때 여전히 주변에 있음을 보장 할 수 없습니다. 따라서 허용되지 않습니다 (또는 확인 가능)

다른 사람들은 반복기에 Ref 매개 변수를 가질 수없는 이유를 설명했습니다. 간단한 대안은 다음과 같습니다.

public interface IFoo
{
    IEnumerable<int> GetItems( int[] box );
    ...
}

public class Bar : IFoo
{
    public IEnumerable<int> GetItems( int[] box )
    {
        int value = box[0];
        // use and change value and yield to your heart's content
        box[0] = value;
    }
}

들어오고 나갈 몇 가지 항목이있는 경우 클래스를 정의하여 고정시킵니다.

반복 해야하는 값이 반복 된 항목에서 파생 될 때 기능을 사용 하여이 문제를 해결했습니다.

// One of the problems with Enumerable.Count() is
// that it is a 'terminator', meaning that it will
// execute the expression it is given, and discard
// the resulting sequence. To count the number of
// items in a sequence without discarding it, we 
// can use this variant that takes an Action<int>
// (or Action<long>), invokes it and passes it the
// number of items that were yielded.
//
// Example: This example allows us to find out
//          how many items were in the original
//          source sequence 'items', as well as
//          the number of items consumed by the
//          call to Sum(), without causing any 
//          LINQ expressions involved to execute
//          multiple times.
// 
//   int start = 0;    // the number of items from the original source
//   int finished = 0; // the number of items in the resulting sequence
//
//   IEnumerable<KeyValuePair<string, double>> items = // assumed to be an iterator
//
//   var result = items.Count( i => start = i )
//                   .Where( p => p.Key = "Banana" )
//                      .Select( p => p.Value )
//                         .Count( i => finished = i )
//                            .Sum();
//
//   // by getting the count of items operated 
//   // on by Sum(), we can calculate an average:
// 
//   double average = result / (double) finished; 
//
//   Console.WriteLine( "started with {0} items", start );
//   Console.WriteLine( "finished with {0} items", finished );
//

public static IEnumerable<T> Count<T>( 
    this IEnumerable<T> source, 
    Action<int> receiver )
{
  int i = 0;
  foreach( T item in source )
  {
    yield return item;
    ++i ;
  }
  receiver( i );
}

public static IEnumerable<T> Count<T>( 
    this IEnumerable<T> source, 
    Action<long> receiver )
{
  long i = 0;
  foreach( T item in source )
  {
    yield return item;
    ++i ;
  }
  receiver( i );
}
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top