Warum können Iterator -Methoden entweder "Ref" oder "Out" -Parameter einnehmen?
-
14-09-2019 - |
Frage
Ich habe das heute früher versucht:
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
}
}
Was ist die Begründung dahinter?
Lösung
C# Iteratoren sind intern staatliche Maschinen. Jedes Mal, wenn Sie yield return
Etwas, der Ort, an dem Sie aufgehört haben, sollte zusammen mit dem Zustand der lokalen Variablen gerettet werden, damit Sie zurückkehren und von dort aus fortfahren können.
Um diesen Zustand zu halten, erstellt C# Compiler eine Klasse für lokale Variablen und den Ort, von dem er fortgesetzt werden sollte. Es ist nicht möglich, eine zu haben ref
oder out
Wert als Feld in einer Klasse. Folglich, wenn Sie einen Parameter als deklarieren durften ref
oder out
, Es gäbe keine Möglichkeit, den vollständigen Schnappschuss der Funktion zu halten, als wir aufgehört hatten.
BEARBEITEN: Technisch gesehen nicht alle Methoden, die zurückkehren IEnumerable<T>
werden als Iteratoren angesehen. Nur diejenigen, die verwenden yield
Um eine Sequenz direkt zu erzeugen, werden Iteratoren betrachtet. Während die Aufteilung des Iterators in zwei Methoden eine schöne und gemeinsame Problemumgehung ist, widerspricht dies nicht dem, was ich gerade gesagt habe. Die äußere Methode (die nicht verwendet yield
direkt) ist nicht als Iterator betrachtet.
Andere Tipps
Wenn Sie sowohl einen Iterator als auch einen INT aus Ihrer Methode zurückgeben möchten, lautet eine Problemumgehung:
public class Bar : IFoo
{
public IEnumerable<int> GetItems( ref int somethingElse )
{
somethingElse = 42;
return GetItemsCore();
}
private IEnumerable<int> GetItemsCore();
{
yield return 7;
}
}
Sie sollten beachten, dass keiner der Code in einer Iteratormethode (dh im Grunde genommen eine Methode enthält yield return
oder yield break
) wird ausgeführt, bis die MoveNext()
Die Methode im Enumerator wird aufgerufen. Also, wenn Sie verwenden konnten out
oder ref
In Ihrer Iterator -Methode erhalten Sie überraschendes Verhalten wie folgt:
// 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
Dies ist eine häufige Gefahr, ein verwandtes Problem ist Folgendes:
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
Ein gutes Muster besteht also darin, Iteratormethoden in zwei Teile zu trennen: eine, die sofort ausgeführt werden kann und eine, die den Code enthält, der träge ausgeführt werden sollte.
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
BEARBEITEN:Wenn Sie wirklich das Verhalten haben möchten, bei dem das Bewegen des Iterators das ändern würde ref
-Parameter, Sie könnten so etwas tun:
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
Auf hohem Niveau kann eine REF -Variable auf viele Standorte hinweisen, einschließlich auf Werttypen, die sich auf dem Stapel befinden. Die Zeit, zu der der Iterator ursprünglich durch Aufrufen der Iteratormethode erstellt wurde und wenn die Ref -Variable zugewiesen wird, sind zwei sehr unterschiedliche Zeiten. Es ist nicht möglich zu garantieren, dass die Variable, die ursprünglich von Referenz übergeben wurde, immer noch gibt, wenn der Iterator tatsächlich ausgeführt wird. Daher ist es nicht erlaubt (oder überprüfbar)
Andere haben erklärt, warum Ihr Iterator keinen Ref -Parameter haben kann. Hier ist eine einfache Alternative:
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;
}
}
Wenn Sie mehrere Gegenstände zum Ein- und Ausgabereien haben, definieren Sie eine Klasse, um sie zu halten.
Ich habe dieses Problem mit Funktionen umgegangen, wenn der Wert, den ich zurückgeben muss, aus den iterierten Elementen abgeleitet wird:
// 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 );
}