Domanda

L'ho provato prima oggi:

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            

    }
}

Qual è la logica dietro questo?

È stato utile?

Soluzione

I iteratori C# sono macchine statali internamente. Ogni volta che tu yield return Qualcosa, il luogo in cui ti sei interrotto dovrebbe essere salvato insieme allo stato delle variabili locali in modo da poter tornare e continuare da lì.

Per tenere questo stato, il compilatore C# crea una classe per contenere variabili locali e il luogo da cui dovrebbe continuare. Non è possibile avere un ref o out valore come campo in una classe. Di conseguenza, se ti è stato permesso di dichiarare un parametro come ref o out, non ci sarebbe modo di mantenere la completa istantanea della funzione nel momento in cui avevamo interrotto.

MODIFICARE: Tecnicamente, non tutti i metodi che ritornano IEnumerable<T> sono considerati iteratori. Solo quelli che usano yield Per produrre una sequenza direttamente sono considerati iteratori. Pertanto, mentre la divisione dell'iteratore in due metodi è una soluzione alternativa piacevole e comune, non contraddice con quello che ho appena detto. Il metodo esterno (che non usa yield direttamente) è non considerato un iteratore.

Altri suggerimenti

Se vuoi restituire sia un iteratore che un INT dal tuo metodo, una soluzione alternativa è questa:

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

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

Dovresti notare che nessuno del codice all'interno di un metodo iteratore (cioè fondamentalmente un metodo che contiene yield return o yield break) viene eseguito fino al MoveNext() Il metodo nell'enumeratore viene chiamato. Quindi, se sei stato in grado di usare out o ref Nel tuo metodo iteratore, otterresti un comportamento sorprendente come questo:

// 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

Questa è una trappola comune, un problema correlato è questo:

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

Quindi un buon modello è separare i metodi di iteratore in due parti: uno da eseguire immediatamente e uno che contiene il codice che dovrebbe essere eseguito pigramente.

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

MODIFICARE:Se vuoi davvero il comportamento in cui lo spostamento dell'iteratore modificherebbe il ref-Parameter, potresti fare qualcosa del genere:

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

Ad un livello elevato, una variabile REF può indicare molte posizioni tra cui i tipi di valori che sono sullo stack. Il tempo in cui l'iteratore viene inizialmente creato chiamando il metodo iteratore e quando la variabile di riferimento verrebbe assegnata sono due volte molto diverse. Non è possibile garantire che la variabile che originariamente è stata approvata per riferimento è ancora in circolazione quando l'iteratore effettivamente esegue. Quindi non è consentito (o verificabile)

Altri hanno spiegato perché il tuo iteratore non può avere un parametro Ref. Ecco una semplice alternativa:

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;
    }
}

Se hai diversi elementi da passare dentro e fuori, definisci una classe per tenerli.

Ho superato questo problema usando le funzioni, quando il valore che devo restituire è derivato dagli elementi iterati:

// 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 );
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top