Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Задачи применения IEnumerable

Anna | 17.06.2014 | нет комментариев
В этой статье я хочу рассказать о загвоздках применения интерфейса IEnumerable. Мы разглядим, какие задачи может принести применение этого интерфейса, когда его на самом деле необходимо применять, и чем его заменить.

А начать статью я хотел с пары примеров кода, а вернее с пары багов, встречавшихся мне в реальных планах.

Примеры задач

Вот 1-й пример — код из реального плана, изменены только имена.

private IEnumerable<Account> GetAccountsByOrder(IEnumerable<Account> accounts, IEnumerable<OrderItem> orderItems)
{
     var orderItemsWithQuotaOwners =  _restsProvider.GetQuotaOwner(orderItems);

    return accounts.Where(
                q => orderItemsWithSourceQuotaOwners.Any(s => 
                    s.QuotaOwner == q.QuotaOwner
                    && ...
                   ));
}

Данный с виду не трудный кусок кода принес нам достаточно много неприятностей. Все дело в способе GetQuotaOwner. Внутри него выполняется LINQ to SQL запрос, потом строится проекция на LINQ to entities и возвращается IEnumerable. В результате на всякую строку quotedAccounts мы получаем новое выполнение внутренностей способа GetQuotaOwner. Что увлекательно, решарпер в этом случае нас не предупредил об угрозы.

Это 2-й пример. Тут, правда, не код реального плана, но идея кода и задача были взяты из реального плана.

class Foo
{
    public string Value;
}

class Bar
{
    public string Value;
    public int ACount;
}

static void Main()
{
    Foo[] foo = new[] 
       { 
           new Foo { Value = "Abba" }, 
           new Foo { Value = "Deep Purple" }, 
           new Foo { Value = "Metallica" }
       };

    var bar = foo.Select(x => new Bar 
       { 
                   Value = x.Value, 
                   ACount = x.Value.Count(c => c == 'a' || c == 'A') 
       });

    Censure(bar);

    foreach (var one in bar)
    {
        Console.WriteLine(one.Value);
    }
}

private static void Censure(IEnumerable<Bar> bar)
{
    foreach (var one in bar)
    {
        if (one.ACount > 1)
        {
            one.Value = "<censored>";
        }
    }
}

Тут мы получаем какие-то данные, строим их проекцию и дальше подвергаем цензуре. И с огромным изумлением видим, что на экран попадают данные, не подвергнутые цензуре…

Повод задачи достаточно примитивна — мы двукратно итерируем по коллекции, а значит мы получим две самостоятельных коллекции инстансов класса Bar.

?сно, что поправить эти два куска кода не представляет никакой трудности, довольно добавить ToArray. Вопрос в ином — что мы фундаментально делали не так и как правильно трудиться с IEnumerable.

Что абстрагирует IEnumerable

Для начала разглядим IEnumerable как таковой. Если не вдаваться в технические подробности, данный интерфейс абстрагирует последовательность элементов. Причем об этой последовательности неведомо бесстрашно ничего: финальная она либо безграничная, какова стоимость операций над ней.

Вот легкой пример — var lines = File.ReadLines(«data.txt»);

Что мы сейчас можем делать с lines? Ну если мы не хотим убить эффективность нашей программы, мы не можем двукратно итерировать по этой коллекции. Значит, что девственный код

    var lines = File.ReadLines("data.txt");
    string lastLine = lines.ElementAt(lines.Count());

должен быть для нас табу.

Может быть ещё дрянней:

class RandomStrings : IEnumerable<int>
{
    Random _rnd = new Random();

    public IEnumerator<int> GetEnumerator()
    {
        while (true)
            yield return _rnd.Next();
    }
}

Сейчас даже один девственный одиночный Count() вешает наше приложение.

Отсель следует один легкой итог: трудиться с IEnumerable не имея предположений, что там сидит внутри, дюже трудно.

Безоговорочно, для нашего примера с чтением файла дозволено реализовать результативное приобретение последней строки (либо, скажем, какую-то потоковую обработку строк), но для этого необходимо перестать думать о IEnumerable как о коллекции и начать писать больше трудный код.

Но в реальных программах, Почаще каждого, программист думает об IEnumerable как о коллекции. К примеру, даже возник такой паттерн — защитная копия IEnumerable. Т.е. вызов ToArray() в начале способа, когда туда приходит IEnumerable.

То есть мы сразу говорим — к нам пришла финальная последовательность, которая легко влезает в память.Но для чего мы тогда используем IEnumerable, когда имеем ввиду коллекцию?

Тут, правда, дотошный читатель может спросить — а что значит верно трудиться с коллекцией? Коллекции бывают различные — связный список тоже коллекция, и приобретение последней строки для связного списка в том жанре, как это делалось выше, тоже вообще-то весьма не результативно (правда безоговорочно и не так жутко, как в случае с IEnumerable, где повторная итерация по коллекции может быть связана с громадным объёмом работ).

Следственно стоит уточнить представления и говорить о векторе(в .NET List, дальше я буду назвать эту коллекцию листом) либо массиве.

Тогда мы подлинно сумеем программировать по контракту — если на вход способа передается IList трудимся как с листом, зная, что доступ к произвольному элементу и приобретение числа элементов это O(1), а если уж нам пришел IEnumerable — значит придется попотеть, реализовывая правильную и результативную работу с ним.

Аналогичная обстановка с возвращаемым значением — вернув IEnumerable мы принуждаем пользователя писать гораздо больше трудный код, работающий с последовательностью, а не листом.

LINQ

Обстановка в .NET с засильем IEnumerable обострилась с вступлением LINQ. Если прежде прикладник мог видеть данный интерфейс пару раз в жизни, то сейчас всякий LINQ запрос порождает IEnumerable.

Появляется вопрос — что делать с такими IEnumerable? Дозволено впасть в одну крайность — сразу преобразовать в массив либо List. Такой подход имеет право на жизнь. Он гарантирует неимение задач с повторной итерацией. С иной стороны может быть порождено много лишних массивов, которые потом придется собирать сборщику мусора.

Дозволено придерживаться компромиссного подхода: трудиться с IEnumerable внутри способа, отдавая наружу только массивы либо листы. Минус этого подхода в том, что придется больше осмотрительно относится к переменным типа IEnumerable (var в реальных исходниках…), чураясь повторных итераций по ним, в том случае, если это может негативно повлиять на эффективность. Концептуально данный подход тоже возможен — внутри одного способа мы абсолютно можем знать природу данного определенного инстанса IEnumerable и не усердствовать обрабатывать его как сферический IEnumerable в вакууме.

Выбор типа коллекции

Как теснее говорилось, для передачи коллекций между способами ICollection не самый успешный тип, т.к. результативно трудиться с произвольной реализацией ICollection только чуть проще, чем с IEnumerable.

Дозволено предпочесть IList, но данный интерфейс имеет один большой минус по сопоставлению с IEnumerable — он разрешает редактировать коллекции, тогда как в 95% случаев сами коллекции подразумеваются как объекты только для чтения.

Взамен IList дозволено воспользоваться ветхим добросердечным массивом. Он, правда, разрешает присваивать элементы. Но, на мой взор, особенно частые операции над коллекциями — это удаление и добавление элементов. В то время как присваивание элемента по индексу для бизнес-приложений — это экзотика. По этому в качестве коллекции только для чтения абсолютно дозволено применять массивы.

Ещё одна вероятность — применять ReadOnlyCollection. Сразу хочу сказать, что данный класс имеет не вовсе правильное имя. Его исключительный конструктор имеет следующую сигнатуру public ReadOnlyCollection (IList list). То есть правильнее было бы его назвать ReadOnlyList. На 1-й взор применение этого класса повсюду может быть не дюже комфортно, но если написать экстеншн

public ReadOnlyCollection<T> ToReadOnly(this IEnumerable<T> data)

, это может быть рабочим вариантом.

Ну а фреймворк 4.5 теснее решил эту задачу: он вводит интерфейс IReadOnlyCollection и IReadOnlyList. Причем List реализует IReadOnlyList, т.е. дозволено писать

IReadOnlyList<Foo> Do(IReadOnlyList<Bar> bar)
{
    return bar.Where(x => IsGood(x)).ToList();
}

Итоги

Глобальное применение в сигнатурах способов IEnumerable нарушает тезисы программирования по контракту и ведет к ошибкам.

Для передачи между способами применять IEnumerable дозволено только в том случае, когда на самом деле нужна работа с IEnumerable, и работа с обыкновенными коллекциями будет не результативна либо немыслима.

Для передачи коллекций только для чтения между способами дозволено пользоваться массивами, классом ReadOnlyCollection и интерфейсом IReadOnlyList.

P.S.
На ту же тему есть ещё одна статья на прогре.

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB