2009-06-15 11 views
28

próbowałem tego wcześniej dzisiaj:Dlaczego metody iteratora nie mogą przyjmować parametrów "ref" lub "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    

    } 
} 

Jakie jest uzasadnienie tego?

+0

Czy coś się stało, gdy próbowaliście tego, czy też prosiliście nas o uzasadnienie wypróbowania? –

+2

Omawiam niektóre z tych zagadnień dotyczących projektowania: http://blogs.msdn.com/ericlippert/archive/2009/05/04/the-stack-is-an-implementation-detail-part-two.aspx –

+0

nowoczesne rozwiązanie : http://answers.unity3d.com/answers/551381/view.html – Fattie

Odpowiedz

36

C# iteratory są wewnętrznymi maszynami stanów. Za każdym razem, gdy coś zostanie przerwane, należy zapisać miejsce, w którym zostało przerwane, wraz ze stanem zmiennych lokalnych, aby móc wrócić i kontynuować.

Aby utrzymać ten stan, kompilator C# tworzy klasę przechowującą zmienne lokalne i miejsce, z którego powinien kontynuować. Nie można mieć wartości ref ani out jako pola w klasie. W związku z tym, gdyby można było zadeklarować parametr jako ref lub out, nie byłoby sposobu, aby zachować pełną migawkę funkcji w momencie, kiedy skończyliśmy.

EDYCJA: Nie wszystkie metody zwracające IEnumerable<T> są traktowane jako iteratory. Tylko te, które używają yield do bezpośredniego tworzenia sekwencji, są uważane za iteratory. Dlatego też, podczas gdy dzielenie iteratora na dwie metody jest miłym i powszechnym rozwiązaniem, nie jest ono sprzeczne z tym, co właśnie powiedziałem. Metoda zewnętrzna (która nie używa bezpośrednio metody yield) jest uważana za iterator.

+0

Z pewnością ma to wiele sensu, dzięki :) – Trap

+2

"Nie można mieć wartości ref lub out jako pola w klasie." - Kompilator może łatwo zaimplementować parametry ref do iteratorów poprzez przydzielenie tablicy pojedynczego elementu do wywołującego, wstawienie do niej argumentu i przekazanie tablicy do iteratora, a także wykonanie iteratora w tablicy [0]. Byłoby to bardzo małą ilością pracy ze strony kompilatora w porównaniu z przekształceniem iteratora w maszynę stanów. –

+0

@JimBalter Byłoby to prawdą, gdyby kompilator kontrolował każdy fragment kodu, który działał. Niestety, ten plan wymagałby wygenerowania innego podpisu API w systemie binarnym - tj. dzwoniący ze świata zewnętrznego przechodzący przez zmienne "ref" nie byliby w stanie zobaczyć, jak się zmieniają. –

5

Na wysokim poziomie zmienna A ref może wskazywać na wiele lokalizacji, w tym na typy wartości na stosie. Czas, w którym iterator jest początkowo tworzony przez wywołanie metody iteratora i kiedy zostanie przypisana zmienna ref, to dwa bardzo różne czasy. Nie można zagwarantować, że zmienna, która została pierwotnie przekazana przez odniesienie, nadal znajduje się w pobliżu, gdy iterator faktycznie jest wykonywany. Stąd nie jest dozwolone (lub sprawdzalne)

15

Jeśli chcesz powrócić zarówno iterator i int od wybranej metody, obejście tego problemu jest taka:

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

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

Należy pamiętać, że żaden z kodem wewnątrz metoda iteratora (tzn. zasadniczo metoda zawierająca yield return lub yield break) jest wykonywana do momentu wywołania metody MoveNext() w module wyliczającym. Więc jeśli jesteś w stanie wykorzystać out lub ref w metodzie iteracyjnej, co można uzyskać zaskakujące zachowanie takiego:

// 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 typowy pułapka, powiązany kwestia jest taka:

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 

tak dobrym sposobem jest oddzielenie metod iteratora na dwie części: jedną do natychmiastowego wykonania i jedną zawierającą kod, który powinien być leniwie wykonany.

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 zachowanie gdzie przesuwając iterator byłoby zmodyfikować ref -parameter, można 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 
+0

Bardzo ciekawa lektura, dzięki. – Trap

+3

Odp: Edycja za pomocą getter/setter lambdas, jest to sposób na symulowanie wskaźników do typów wartości (oczywiście bez manipulacji adresami), więcej tutaj: http://incrediblejourneysintotheknown.blogspot.com/2008/05/pointers- to-value-types-in-c.html –

+0

@Earwicker: Bardzo interesujące. –

1

stałam się wokół tego problemu za pomocą funkcji, gdy wartość, którą muszę zwrócić, pochodzi z elementów powtórzonych:

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

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 przedmiotów do przekazania i wyjazdu, zdefiniuj klasę, która je pomieści.

Powiązane problemy