Возвращаемый метод IEnumerable <T> должен ToList () или нет

Я не могу решить, должен ли мой метод, возвращающий IEnumerable<T>, быть ленивым или должен ли он создать список и вернуть его. Я часто выбираю последнее, поэтому я могу быть уверен, что перечисление каждого значения не выполняется несколько раз. Например:

public IEnumerable<UserProfile> GetUsers()
{
    var allDepartments = GetAllDepartments(active: true); // Returns IEnumerable<Department>
    var allUsers = GetAllUserDepartments(active: true); // Returns IEnumerable<User>
    var users
        = allDepartments
            .Join(allUsers, x => x.Department, y => y.Department, (x, y) => y, StringComparer.OrdinalIgnoreCase)
            .Distinct()
            .Select(x => GetUserProfile(x))
            .ToList(); // Is this recommended or not

    return users;
}

Ключевой частью является то, что каждая перечисление делает что-то (GetUserProfile) нетривиально, это может быть дорого, это может быть не так, но какова рекомендация для метода, который возвращает IEnumerable<T>? Должен ли он заботиться о том, может ли вызывающий абонент перечислить его несколько раз или нет?

Предположим, что вызывающий объект требует только IEnumerable<T>, и мой вопрос может быть изменен на:

Как я могу указать вызывающему, что каждое перечисление может быть дорогостоящим?

Насколько дороги могут измениться их решения по внедрению, если они перечисляют всю партию несколько раз, тогда имеет смысл их выполнять, если они не перечисляют всю партию, тогда ToList() будет пустой тратой для меня (или вызывающего) для выполнения?

30 голосов | спросил user2000095-tim 15 +04002014-10-15T16:08:31+04:00312014bEurope/MoscowWed, 15 Oct 2014 16:08:31 +0400 2014, 16:08:31

3 ответа


18

Всегда ли код, вызывающий метод, всегда ожидает функциональность List (доступ по индексу и т. д.)? Верните List. Использует ли код, вызывающий метод, только итерацию по нему? Верните IEnumerable.

Вам не следует заботиться о том, что делает вызывающий абонент, потому что тип возврата четко указывает, что может сделать возвращаемое значение. Любой вызывающий, который получает результат IEnumerable, знает, что если они хотят индексированный доступ к результату, им придется преобразовать в List, потому что IEnumerable simple не способен на него, пока он не был перечислен и не помещен в индексированную структуру. Не предполагайте, что вызывающие лица глупы, в противном случае вы получите от них away . Например, возвращая IEnumerable, вы убрали возможность передавать результаты, которые могут иметь свои преимущества по производительности. Ваша реализация может измениться, но вызывающий может всегда превращать List в IEnumerable, если это необходимо.

Теперь, когда вы решили, какой тип возврата должен быть, вы должны решить, что использовать реализация . Отсроченное исполнение имеет преимущества, но также может вводить в заблуждение. Возьмите этот пример:

List

Это может быть «исправлено» с помощью public static IEnumerable<int> GetRecordIds() { return dbContext.Records.Select(r => r.Id); } IEnumerable<int> currentRecordIds = GetRecordIds(); dbContext.Add(new Record { Id = 7 }); // Includes "7", despite GetRecordIds() being called before the Add(): currentRecordIds.Dump(); , вызывающего GetRecordIds перед возвратом. «Правильное» использование здесь просто зависит от ожидаемого от класса («живых» результатов или результатов времени вызова).

Опять же, не предполагайте, что вызывающий является глупым и не отвлекает функциональность от вызывающего, делая предположения о том, как он будет использоваться. Помните, что интерфейс сообщает вам , что, как ожидается, будет возвращено. ToList only означает, что вы получаете что-то, что можно повторить (потенциально потоковые результаты и использование отложенного исполнения), и IEnumerable только означает, что вы получаете коллекцию в памяти, которую можно добавить, удалить, получить доступ по индексу и т. д.

Изменить - Чтобы обратиться к вашему «Обновить 1»:

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

ответил Ocelot20 15 +04002014-10-15T18:28:41+04:00312014bEurope/MoscowWed, 15 Oct 2014 18:28:41 +0400 2014, 18:28:41
11

Я думаю, что реальный вопрос здесь может ли лениво оцениваться возвращаемое значение ? Это, по сути, то, к чему это сводится.

IEnumerable лениво оценивается тем, что любые действия, которые вы применяете к нему (через Linq или любой разумный автор), будут отложены до последнего момента, т. е. пока вы не попытаетесь наблюдать за IEnumerable путем итерации или с помощью fold (First(), FirstOrDefault()). В большинстве случаев это хорошая вещь , потому что это означает, что вы можете полностью избежать любой работы, пока вам это не понадобится, - что иногда означает, что потенциально дорогостоящая операция никогда не происходит. Отлично!

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

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

Существует инфраструктура, называемая Rx ( R активная E x ) для .NET, которая на самом деле имеет функцию в этом встроенном Observable.Using(). Наблюдаемый - это класс, который может быть отмечен , и лениво оценивается, что очень похоже на IEnumerable. На самом деле IObservable реализует IEnumerable. Используя Observable.Using(), вы можете привязать создание объекта с помощью фабричного метода к времени жизни IObservable. При первом обнаружении IObservable создается ресурс. Когда завершается IObservable, ресурс Dispose() d. Чистый и аккуратный.

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

ответил Dan Pantry 15 +04002014-10-15T20:58:21+04:00312014bEurope/MoscowWed, 15 Oct 2014 20:58:21 +0400 2014, 20:58:21
5

Вы не должны использовать var, если нужно поместить комментарий с указанием типа возврата, если он неясен без комментария, поместите реальный тип вместо var, это более читаемо таким образом.

Чтобы ответить на ваш вопрос, я думаю, что да, вы должны называть ToList перед возвратом перечисления главным образом потому, что ваш IEnumerable<UserProfile> может быть запросом EF, и нет способа для пользователя вашего класса, чтобы знать это, то есть человек может использовать это перечисление несколько раз и запускать тонны SQL-запросов, не зная его (пока не возникнут проблемы с производительностью, а диагностика приведет к запуску IEnumerable запросы).

Стоимость использования ToList такая же, как при использовании конструктора new List<>([your enumerable]), который является операцией O (n), поскольку вам нужно скопировать весь массив. Если у вас есть очень большой список, который вы не хотите копировать все время, возможно, вам стоит рассмотреть возможность добавления параметра к вашему методу, который позволит вам решить, выполняется ли IEnumerable или нет. :

public IEnumerable<UserProfile> GetUsers(bool deferExecution)
{
    IEnumerable<Department> allDepartments = GetAllDepartments(active: true);
    IEnumerable<User> allUsers = GetAllUserDepartments(active: true);
    IEnumerable<UserProfile> users
        = allDepartments
            .Join(allUsers, x => x.Department, y => y.Department, (x, y) => y, StringComparer.OrdinalIgnoreCase)
            .Distinct()
            .Select(x => GetUserProfile(x));

    if(!deferExecution) 
    {
        users = users.ToList();
    }

    return users;
}

Хотя честно, я никогда не пробовал это с логическим в реальной жизни, поэтому я не могу сказать, действительно ли это хорошая идея, но я чувствую, что это будет не так!

Если вам нужна дополнительная информация о стоимости исполнения ToList, проверьте это пост.

ответил IEatBagels 15 +04002014-10-15T16:19:30+04:00312014bEurope/MoscowWed, 15 Oct 2014 16:19:30 +0400 2014, 16:19:30

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132