Pourquoi les méthodes Iterator ne peuvent-elles pas prendre les paramètres «ref» ou «out»?

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

  •  14-09-2019
  •  | 
  •  

Question

J'ai essayé ceci plus tôt dans la journée:

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            

    }
}

Quelle est la justification derrière cela?

Était-ce utile?

La solution

Les itérateurs C # sont des machines d'État en interne. Chaque fois que vous yield return Quelque chose, l'endroit où vous vous êtes arrêté devrait être sauvé avec l'état des variables locales afin que vous puissiez revenir et continuer à partir de là.

Pour tenir cet état, C # Compiler crée une classe pour contenir des variables locales et l'endroit où il devrait continuer. Il n'est pas possible d'avoir un ref ou out valeur comme champ dans une classe. Par conséquent, si vous étiez autorisé à déclarer un paramètre comme ref ou out, il n'y aurait aucun moyen de garder l'instantané complet de la fonction au moment où nous nous étions arrêtés.

ÉDITER: Techniquement, toutes les méthodes qui reviennent IEnumerable<T> sont considérés comme des itérateurs. Juste ceux qui utilisent yield Pour produire une séquence directement, sont considérés comme des itérateurs. Par conséquent, bien que la division de l'itérateur en deux méthodes soit une solution de contournement agréable et courante, elle ne contredit pas ce que je viens de dire. La méthode extérieure (qui n'utilise pas yield directement) est ne pas considéré comme un itérateur.

Autres conseils

Si vous souhaitez retourner à la fois un itérateur et un int à partir de votre méthode, une solution de contournement est la suivante:

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

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

Vous devez noter qu'aucun du code dans une méthode Iterator (c'est-à-dire essentiellement une méthode qui contient yield return ou yield break) est exécuté jusqu'à ce que le MoveNext() La méthode dans l'énumérateur est appelée. Donc, si vous avez pu utiliser out ou ref Dans votre méthode Iterator, vous obtiendrez un comportement surprenant comme ceci:

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

Il s'agit d'un piège commun, un problème connexe est le suivant:

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

Ainsi, un bon modèle consiste à séparer les méthodes d'itérateur en deux parties: une à exécuter immédiatement et une qui contient le code qui doit être exécuté paresseusement.

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

ÉDITER:Si vous voulez vraiment le comportement où le déplacement de l'itérateur modifierait le ref-Paramètre, vous pouvez faire quelque chose comme ceci:

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

À un niveau élevé, une variable REF peut pointer vers de nombreux emplacements, y compris des types de valeur qui sont sur la pile. L'heure à laquelle l'itérateur est initialement créé en appelant la méthode Iterator et lorsque la variable REF serait attribuée, il y a deux moments très différents. Il n'est pas possible de garantir que la variable qui a été transmise par référence est toujours là lorsque l'itérateur s'exécute réellement. Par conséquent, il n'est pas autorisé (ou vérifiable)

D'autres ont expliqué pourquoi votre itérateur ne peut pas avoir de paramètre REF. Voici une alternative 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 vous avez plusieurs articles à passer et à sortir, définissez une classe pour les tenir.

J'ai contourné ce problème en utilisant des fonctions, lorsque la valeur que je dois retourner est dérivée des éléments itérés:

// 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 );
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top