2009-06-15 12 views
28

yo probamos este día de hoy:¿Por qué los métodos del iterador no pueden tomar los parámetros '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    

    } 
} 

¿Cuál es la razón detrás de esto?

+0

¿Ocurrió algo cuando intentaste esto o nos pides tu razón para probarlo? –

+2

Discuto algunas de estas consideraciones de diseño aquí: http://blogs.msdn.com/ericlippert/archive/2009/05/04/the-stack-is-an-implementation-detail-part-two.aspx –

+0

solución moderna : http://answers.unity3d.com/answers/551381/view.html – Fattie

Respuesta

36

Los iteradores C# son máquinas de estado internas. Cada vez que yield return algo, el lugar donde lo dejó se debe guardar junto con el estado de las variables locales para que pueda volver y continuar desde allí.

Para mantener este estado, el compilador de C# crea una clase para contener las variables locales y el lugar desde donde debe continuar. No es posible tener un valor ref o out como un campo en una clase. En consecuencia, si se le permitiera declarar un parámetro como ref o out, no habría forma de mantener la instantánea completa de la función en el momento en que la habíamos dejado.

EDITAR: Técnicamente, no todos los métodos que devuelven IEnumerable<T> se consideran iteradores. Solo aquellos que usan yield para producir una secuencia directamente se consideran iteradores. Por lo tanto, si bien la división del iterador en dos métodos es una solución agradable y común, no está en contradicción con lo que acabo de decir. El método externo (que no usa yield directamente) es no considerado un iterador.

+0

Ciertamente tiene mucho sentido, gracias :) – Trap

+2

"No es posible tener un valor de referencia o de salida como campo en una clase". --El compilador podría implementar fácilmente los parámetros de ref a los iteradores asignando una única matriz de elementos en la persona que llama, colocando el argumento en eso, y pasando la matriz al iterador, y haciendo que el iterador opere en la matriz [0]. Esta sería una cantidad muy pequeña de trabajo por parte del compilador en comparación con convertir el iterador en una máquina de estado. –

+0

@JimBalter Eso sería cierto si el compilador controlaba cada parte del código que se ejecutó. Desafortunadamente, ese plan requeriría una firma API diferente para ser generada en el binario, es decir. los llamantes del mundo exterior que pasan variables "' ref' "no podrán verlos cambiar. –

5

En un nivel elevado, una variable de referencia puede apuntar a muchos lugares, incluidos los tipos de valores que están en la pila. La hora en la que se crea inicialmente el iterador llamando al método del iterador y cuándo se le asignará la variable de referencia son dos tiempos muy diferentes. No es posible garantizar que la variable que originalmente se pasó por referencia aún esté presente cuando el iterador realmente se ejecute. Por lo tanto, no está permitido (o verificable)

15

Si desea volver a la vez un repetidor y un int de su método, una solución es la siguiente:

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

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

Debe tener en cuenta que ninguna de código dentro de una el método del iterador (es decir, básicamente un método que contiene yield return o yield break) se ejecuta hasta que se llame al método MoveNext() en el Enumerator. Así que si usted fuera capaz de utilizar out o ref en su método iterador, se llega a un comportamiento sorprendente de esta manera:

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

Este es un error común, un tema relacionado es la siguiente:

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 

por lo Un buen patrón es separar los métodos del iterador en dos partes: una para ejecutar inmediatamente y otra que contiene el código que debe ejecutarse de forma perezosa.

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: Si realmente desea el comportamiento cuando se mueve el iterador modificaría la -parámetro ref, se podría hacer algo como esto:

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 lectura muy interesante, gracias. – Trap

+3

Re: la edición con getter/setter lambdas, esto como una manera de simular punteros a tipos de valor (aunque sin manipulación de direcciones, por supuesto), más aquí: http://incrediblejourneysintotheknown.blogspot.com/2008/05/pointers- to-value-types-in-c.html –

+0

@Earwicker: Muy interesante. –

1

he conseguido solucionar este problema usando funciones, cuando el valor que necesito devolver se deriva de los elementos iterados:

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

Otros han explicado por qué su iterador no puede tener un parámetro ref. Aquí hay una alternativa simple:

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

Si tiene varios elementos para ingresar y salir, defina una clase para guardarlos.

Cuestiones relacionadas