Кога трябва отделно да внедря IEnumerator‹T›?

В рамковите класове на колекции често съм виждал IEnumerator<T> отделно имплементиран като вътрешен клас и негов екземпляр се връща в метода GetEnumerator.

Сега да предположим, че пиша свои собствени класове за колекция, които ще имат вградена колекция като List<T> или T[], действаща като притежател вътрешно, да кажем така:

public class SpecialCollection<T> : IEnumerable<T>
{
    List<T> list;

    public SpecialCollection<T>()
    {

    }

    public IEnumerator<T> GetEnumerator()
    {
        return list.GetEnumerator();

        //or
        return list.Where(x => some logic).GetEnumerator(); 

        //or directly rely on yield keyword
        yield return x; //etc
    }
}

Трябва ли да пиша собствен клас изброител или е добре да върна изброител на класа List<T>? Има ли някакво обстоятелство, при което трябва да напиша собствен клас за преброяване?


Имам и свързан въпрос. Ако не е толкова важно или не прави голяма разлика, защо всеки клас за събиране в BCL пише свой собствен IEnumerator?

Например, клас List<T> има нещо подобно

T[] items;
public IEnumerator<T> GetEnumerator()
{
    return new List<T>.Enumerator(items);
}

person nawfal    schedule 10.06.2014    source източник
comment
За разлика от какво? Много от тези класове са написани преди да съществуват итераторите.   -  person SLaks    schedule 10.06.2014
comment
Генериците бяха въведени едновременно с ключовата дума yield, ако не греша   -  person Dennis_E    schedule 10.06.2014
comment
Не използвайте разточителното array.Select(x => x).GetEnumerator();, можете да използвате ((IEnumerable<T>)array).GetEnumerator(). Едномерните масиви имплементират генеричните интерфейси по магически начин, който изглежда като изрично имплементиране на интерфейс отвън.   -  person Jeppe Stig Nielsen    schedule 10.06.2014
comment
Много от тези изброители са структури. Може би това има нещо общо.   -  person Dennis_E    schedule 10.06.2014
comment
@JeppeStigNielsen Не съм сигурен, че разбирам мнението ви. Това беше някакъв пример.   -  person nawfal    schedule 10.06.2014
comment
@JeppeStigNielsen Мисля, че // some logic имаше предвид, че може да се добави някаква допълнителна логика към изброителя.   -  person Maarten    schedule 10.06.2014
comment
Не е дубликат, но може да изясни някои точки: stackoverflow.com/questions/3737997/   -  person Yuval Itzchakov    schedule 10.06.2014
comment
Мога да разбера дали това q е дубликат или неясно. Въз основа на мнение? Какво не е наред?   -  person nawfal    schedule 10.06.2014
comment
Вижте дали ericlippert.com/2011/06/30/following-the-pattern ви помага   -  person Jon Skeet    schedule 10.06.2014
comment
@Maarten Да предположим, че пиша собствен тип class MyColl : IEnumerable<int>. Мога да запазя стойностите си в масив вътрешно, така че имам поле private int[] array;. Сега, когато трябва да внедря генеричния интерфейс, не мога просто да кажа return array.GetEnumerator(); (грешка по време на компилиране) поради не съвсем генеричния характер на масивите (тук int[]). Можех да реша това до return array.Select(x => x).GetEnumerator();. Но това е прахосничество. Вместо това използвайте return ((IEnumerable<int>)array).GetEnumerator(); или еквивалентно return array.AsEnumerable().GetEnumerator();. Виж какво имам предвид?   -  person Jeppe Stig Nielsen    schedule 10.06.2014
comment
@JeppeStigNielsen добра гледна точка. Само да повторя, примерите не бяха внимателно изработени. Исках само да предам намерението.   -  person nawfal    schedule 10.06.2014
comment
@JonSkeet Благодаря. Но тази връзка говори за това какво е наложило необходимостта от патешко въвеждане в случай на foreach и защо структурата е предпочитана пред референтен тип. Не говори нищо за повторно използване на съществуващите реализации на изброителя.   -  person nawfal    schedule 10.06.2014
comment
@nawfal Това предполага, че имате съществуваща реализация на изброител за повторно използване. Ако го направите, чудесно, ако не го направите, тогава трябва да направите нещо друго. В тази публикация блокът на итератора ще обхване почти всички, всички, освен тези с най-строгите изисквания за производителност.   -  person Servy    schedule 10.06.2014
comment
@nawfal: Говори защо List<T> има свой собствен тип (struct) изброител.   -  person Jon Skeet    schedule 10.06.2014
comment
@Servy да, за това е въпросът ми. Тоест, ако вече съществуват реализации на изброител за повторна употреба, има ли смисъл да се пишат нови. Попитах това, защото го виждам през цялото време в рамкови класове. Но както Ерик отговаря, представянето е единствената причина.   -  person nawfal    schedule 10.06.2014
comment
@JonSkeet, доколкото разбирам, не говори за защо List‹T› има свой собствен (struct) тип изброител. Той говори за защо List‹T› има свой собствен тип (struct) изброител.   -  person nawfal    schedule 10.06.2014
comment
@nawfal: Изобщо не съм сигурен каква разлика се опитвате да посочите там, освен ако не е по отношение на подчертаването. Като се има предвид, че изброителят се нуждае от достъп до частните членове на списъка, той трябва да бъде вложена структура... как бихте предложили да го приложите иначе?   -  person Jon Skeet    schedule 10.06.2014
comment
@JonSkeet В статията не се говори за неизползване на GetEnumerator на резервния масив (за което е въпросът ми). Той говори за това защо се изисква да бъде struct на първо място. Въпросът ми е, ако имате нужда от структура, тогава защо не използвате отново вече внедрената структура на резервен масив. Надявам се да е ясно. Не за да се заяждам, но честно казано връзката изобщо не изяснява объркването ми. Отговорът на Ерик го прави.   -  person nawfal    schedule 10.06.2014
comment
@nawfal Изглежда, че пропускате факта, че foreach над масив се разпознава от компилатора и вместо това се трансформира в for цикъл, тъй като това е много по-бързо. Това означава, че всъщност се използват само изброители на масиви в кутии и актът на боксиране също елиминира възможността за оптимизации на стила на списъка, така че няма нужда масивите да използват трика на списъка да имат структура като изброител (така че те недей; техният изброител е class), защото имат нещо още по-добро, което е специфично за тях.   -  person Servy    schedule 10.06.2014
comment
@nawfal: Ако върнете IEnumerator<T> на масива, няма да можете да откриете промяната на списъка между извикванията, което трябва да задейства изключение.   -  person Jon Skeet    schedule 10.06.2014
comment
@JonSkeet Това е отлична точка и отговаря на въпроса ми!. Ако можете да го направите като отговор, ще го оценя (vcsjones е отговорил, но по някаква причина го е изтрил). Но също така мисля, че използването на изброители на всичко над масив (като List<T>, Queue<T>) в рамката не е лош избор, като се има предвид, че те улавят модификации.   -  person nawfal    schedule 10.06.2014


Отговори (2)


Отговорът на първия ви въпрос е: когато възвръщаемостта на дохода не отговаря на вашите нужди.

Отговорът на втория ви въпрос е: тези често използвани типове имат изисквания за производителност, които са необичайно строги, така че изброителите са създадени по поръчка. Написах няколко статии за това наскоро; виж:

http://ericlippert.com/2014/05/21/enumerator-advance/

http://ericlippert.com/2014/06/04/enumerator-bounds/

person Eric Lippert    schedule 10.06.2014

Само да отговоря на една част:

List<T> има своя собствена реализация на изброител по две причини:

  • It can't just return the iterator of its backing array, because:
    • It needs to detect structural changes in the list (additions and removals) in order to invalidate the enumerator
    • Масивът може да е по-голям от списъка. (Използването на Take ще поправи това, с цената на друго ниво на индиректност)
  • Горното може да се извърши с итераторен блок (въпреки че първо трябва да има метод без итератор, за да се улови „структурната версия“ на списъка по време на извикване, а не по време на първата итерация), но това е относително неефективно в сравнение с действителното силно оптимизирано внедряване на променлива структура

Използването на променлива структура тук има определени проблеми, но когато използван по очаквания начин, той избягва разпределянето на купчина, извиквания на виртуални методи чрез препратки и т.н.

person Jon Skeet    schedule 10.06.2014
comment
Дължината на масива е друга отлична точка, лесна за пропускане. Но мисля, че връщането на изброителя на вградени типове от рамката като речник, списък, стек, опашка, набор и т.н. (различно от масив, разбира се) е добре, като се има предвид, че техният изброител се справя с този проблем с модификацията на колекцията. не е ли - person nawfal; 10.06.2014
comment
@nawfal: Връщане на изброителя в какъв контекст? Често е добре, но понякога не е така. Тук няма еднозначен отговор. - person Jon Skeet; 10.06.2014
comment
Имах предвид в същия случай като в примера, който дадох във въпроса си. Ако типът на резервната колекция на моя клас SpecialCollection<T> е List<T>, тогава връщането на List<T>.GetEnumerator от метода SpecialCollection<T>.GetEnumerator е без грешки, предполагам. - person nawfal; 10.06.2014
comment
@nawfal: Да, ако нямате други специални изисквания, мисля, че това трябва да е наред. - person Jon Skeet; 10.06.2014
comment
Хм разбирам. Благодаря. - person nawfal; 10.06.2014