Dlaczego metody iteratora nie mogą pobierać parametrów' ref 'lub' out'?
Próbowałem tego wcześniej dzisiaj:
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
}
}
Jakie jest uzasadnienie tego?
5 answers
Iteratory C# są wewnętrznie maszynami stanu. Za każdym razem, gdy yield return
coś, miejsce, w którym przerwałeś, powinno zostać zapisane wraz ze stanem zmiennych lokalnych, abyś mógł wrócić i kontynuować stamtąd.
Aby utrzymać ten stan, kompilator C# tworzy klasę przechowującą zmienne lokalne i miejsce, z którego powinna być kontynuowana. Nie jest możliwe posiadanie wartości ref
lub out
jako pola w klasie. W związku z tym, jeśli możesz zadeklarować parametr jako ref
lub out
, tam nie byłoby sposobu, aby zachować pełną migawkę funkcji w czasie, który przerwaliśmy.
Edit: technicznie nie wszystkie metody zwracające IEnumerable<T>
są uważane za Iteratory. Tylko te, które używają yield
do wytworzenia sekwencji bezpośrednio, są uważane za Iteratory. W związku z tym, chociaż podział iteratora na dwie metody jest miłym i powszechnym obejściem, nie stoi to w sprzeczności z tym, co właśnie powiedziałem. Metoda zewnętrzna (która nie używa yield
bezpośrednio) jest , a nie uważana za iterator.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-16 23:53:22
Jeśli chcesz zwrócić zarówno iterator, jak i int z metody, obejście jest takie:
public class Bar : IFoo
{
public IEnumerable<int> GetItems( ref int somethingElse )
{
somethingElse = 42;
return GetItemsCore();
}
private IEnumerable<int> GetItemsCore();
{
yield return 7;
}
}
Należy zauważyć, że żaden z kodu wewnątrz metody iteratora (tj. zasadniczo metody zawierającej yield return
lub yield break
) nie jest wykonywany, dopóki nie zostanie wywołana metoda MoveNext()
W Enumeratorze. Więc gdybyś był w stanie użyć out
lub ref
w swojej metodzie iterator, otrzymałbyś zaskakujące zachowanie takie jak:
// 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
Jest to częsta pułapka, związany z tym problem jest to:
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
Więc dobrym wzorcem jest rozdzielenie metod iteratora na dwie części: jedną do natychmiastowego wykonania i jedną zawierającą kod, który powinien być leniwie wykonywany.
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:
Jeśli naprawdę chcesz, aby przeniesienie iteratora zmieniało parametr ref
-, możesz zrobić coś takiego:
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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-17 08:20:05
Na wysokim poziomie zmienna ref może wskazywać na wiele lokalizacji, w tym na typy wartości znajdujące się na stosie. Czas, w którym iterator jest początkowo tworzony przez wywołanie metody iterator i kiedy zmienna ref zostanie przypisana są dwa bardzo różne czasy. Nie można zagwarantować, że zmienna, która pierwotnie została przekazana przez referencję, jest nadal w pobliżu, gdy iterator faktycznie wykonuje. Dlatego nie jest dozwolone (lub weryfikowalne)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-16 00:03:45
Inni wyjaśnili, dlaczego twój iterator nie może mieć parametru ref. Oto prosta alternatywa:
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;
}
}
Jeśli masz kilka elementów do przekazania, zdefiniuj klasę, która je przechowuje.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-12 23:54:54
Obejrzałem ten problem używając funkcji, gdy wartość, którą muszę zwrócić pochodzi z iteracyjnych elementów:
// 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 );
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-06 10:21:15