Pregunta

Intenté esto hoy:

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 justificación detrás de esto?

¿Fue útil?

Solución

C# iteradores son máquinas de estado internamente. Cada vez que usted yield return Algo, el lugar donde dejaste debe guardar junto con el estado de las variables locales para que puedas regresar y continuar desde allí.

Para mantener este estado, el compilador C# crea una clase para mantener variables locales y el lugar donde debe continuar. No es posible tener un ref o out valor como campo en una clase. En consecuencia, si se le permitió 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 lo dejamos.

EDITAR: Técnicamente, no todos los métodos que regresan 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 contradice con lo que acabo de decir. El método exterior (que no se usa yield directamente) es no considerado un iterador.

Otros consejos

Si desea devolver un iterador y un INT de su método, una solución es esta:

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 ninguno de los códigos dentro de un método de iterador (es decir, básicamente un método que contiene yield return o yield break) se ejecuta hasta el MoveNext() Se llama al método en el enumerador. Entonces, si pudieras usar out o ref En su método de iterador, obtendrá un comportamiento sorprendente como este:

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

Esta es una trampa común, un problema relacionado es este:

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 tanto, un buen patrón es separar los métodos de iterador en dos partes: uno para ejecutar de inmediato y otro que contenga el código que debe ejecutarse perezosamente.

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

EDITAR:Si realmente quieres el comportamiento donde mover el iterador modificaría el ref-Parameter, podrías 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

En un nivel alto, una variable de referencia puede apuntar a muchas ubicaciones, incluidos los tipos de valor que están en la pila. El momento en que el iterador se crea inicialmente llamando al método de iterador y cuando se asignaría la variable REF son dos veces muy diferentes. No es posible garantizar que la variable que originalmente se pasó por referencia todavía esté cerca cuando el iterador realmente se ejecuta. Por lo tanto, no está permitido (o verificable)

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 entrar y salir, defina una clase para sostenerlos.

He entendido 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 );
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top