2009-06-15 3 views
28

Ho provato questo oggi in precedenza:Perché i metodi di iteratore non possono prendere i parametri "ref" o "out"?

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?

+0

È successo qualcosa quando hai provato questo, o ci stai chiedendo il tuo fondamento logico per provarlo? –

+2

Discuto di alcune di queste considerazioni sulla progettazione qui: http://blogs.msdn.com/ericlippert/archive/2009/05/04/the-stack-is-an-implementation-detail-part-two.aspx –

+0

soluzione moderna : http://answers.unity3d.com/answers/551381/view.html – Fattie

risposta

36

Gli iteratori C# sono macchine di stato interne. Ogni volta che si esegue il comando yield return, il luogo in cui si era interrotto deve essere salvato insieme allo stato delle variabili locali in modo da poter tornare indietro e continuare da lì.

Per mantenere questo stato, il compilatore C# crea una classe per contenere le variabili locali e il luogo da cui deve continuare. Non è possibile avere un valore ref o out come campo in una classe. Di conseguenza, se si fosse autorizzati a dichiarare un parametro come ref o out, non sarebbe stato possibile mantenere l'istantanea completa della funzione al momento della disattivazione.

MODIFICA: Tecnicamente, non tutti i metodi che restituiscono IEnumerable<T> sono considerati iteratori. Solo quelli che usano yield per produrre direttamente una sequenza sono considerati iteratori. Pertanto, mentre la suddivisione dell'iteratore in due metodi è una soluzione semplice e comune, non è in contraddizione con ciò che ho appena detto. Il metodo esterno (che non utilizza direttamente yield) è non considerato un iteratore.

+0

Ha sicuramente molto senso, grazie :) – Trap

+2

"Non è possibile avere un valore di riferimento o di uscita come campo in una classe." - Il compilatore può facilmente implementare i parametri di ref in iteratori allocando un singolo array di elementi nel chiamante, inserendo l'argomento in questo e passando l'array all'iteratore e facendo funzionare l'iteratore sull'array [0]. Questa sarebbe una piccola quantità di lavoro sulla parte del compilatore rispetto a quando si trasforma l'iteratore in una macchina a stati. –

+0

@JimBalter Questo sarebbe vero se il compilatore controllasse ogni parte del codice che girava. Sfortunatamente, quel piano richiederebbe una firma API diversa da generare nel file binario - cioè. i chiamanti dal mondo esterno che passano in variabili "ref" non sarebbero in grado di vederli cambiare. –

5

Ad un livello elevato, una variabile ref può puntare a molte posizioni, compresi i tipi di valore che sono in pila. Il momento in cui l'iteratore viene inizialmente creato chiamando il metodo iteratore e quando la variabile ref viene assegnata sono due volte molto diverse. Non è possibile garantire che la variabile originariamente passata per riferimento sia ancora presente quando l'iteratore viene effettivamente eseguito. Quindi non è consentito (o verificabile)

15

Se si desidera restituire sia un iteratore e un int dal tuo metodo, una soluzione è questa:

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

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

Si dovrebbe notare che nessuno di codice all'interno di un Il metodo iteratore (ovvero un metodo che contiene yield return o yield break) viene eseguito finché non viene chiamato il metodo MoveNext() nell'enumeratore. Quindi, se tu fossi in grado di utilizzare out o ref nel metodo iteratore, si otterrebbe 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 

Questo è un errore 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 

Così un buon modello è quello di separare i metodi di iteratore in due parti: una da eseguire immediatamente e una che contiene il codice che deve 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 

EDIT: Se si vuole veramente il comportamento in cui si muove l'iteratore modificherebbe il ref -parameter, si potrebbe fare qualcosa di simile:

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 
+0

Una lettura molto interessante, grazie. – Trap

+3

Re: la modifica con lambdas getter/setter, questo come un modo per simulare i puntatori ai tipi di valore (anche se senza la manipolazione dell'indirizzo, ovviamente), più qui: http://incrediblejourneysintotheknown.blogspot.com/2008/05/pointers- to-value-types-in-c.html –

+0

@Earwicker: molto interessante. –

1

ho ottenuto intorno a questo problema utilizzando le funzioni, quando il valore che ho bisogno di restituire deriva 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); 
} 
1

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 si dispone di più elementi da passare dentro e fuori, definire una classe per mantenerli.