Какие методы могут защитить от неожиданного отложенного выполнения с IEnumerable‹T› в качестве аргумента?

Есть несколько вопросов, подобных этому, которые касаются правильных типов ввода и вывода вот так. Мой вопрос заключается в том, какие передовые методы, именование методов, выбор типа параметра или подобное могут защитить от аварий с отложенным выполнением?

Они наиболее распространены с IEnumerable, который является очень распространенным типом аргумента, потому что:

  • Следует принципу надежности "Будьте консервативны в своих действиях, будьте либеральны в своих действиях". принимать от других"

  • Широко используется с Linq

  • IEnumerable находится выше в иерархии коллекций и предшествует новые типы коллекций

Однако он также вводит отложенное выполнение. Теперь мы могли ошибиться при разработке наших методов (особенно методов расширения), когда мы думали, что лучшая идея — взять самый простой тип. Итак, наши методы выглядели так:

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> lstObject)
{
    foreach (T t in lstObject)
       //some fisher-yates may be
}

Опасность, очевидно, заключается в том, что мы смешиваем вышеуказанную функцию с ленивой Linq, и она очень восприимчива.

var query = foos.Select(p => p).Where(p => p).OrderBy(p => p); //doesn't execute
//but
var query = foos.Select(p => p).Where(p => p).Shuffle().OrderBy(p => p);
//the second line executes up to a point.

Большая редакция:

Повторное открытие: критика функциональности языка неконструктивна, однако спрашивать о передовом опыте — вот где сияет StackOverflow. Обновил вопрос, чтобы отразить это.

Большое редактирование здесь:

Чтобы уточнить приведенную выше строку. Мой вопрос не о том, что второе выражение не оценивается, серьезно. Программисты это знают. Меня беспокоит метод Shuffle, фактически выполняющий запрос до этого момента. См. первый запрос, в котором ничего не выполняется. Теперь аналогичным образом при построении другого выражения Linq (которое должно быть выполнено позже) наша пользовательская функция играет в спойлер. Другими словами, как сообщить вызывающему абоненту, что Shuffle — это не та функция, которую он хотел бы получить в этой точке выражения Linq. Я надеюсь, что дело дошло до сути. Извинения! :) Хотя это так же просто, как пойти и проверить метод, я спрашиваю, как вы, ребята, обычно программируете в обороне..

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

Но такая функция творит чудеса с Linq:

public static IEnumerable<S> DistinctBy<S, T>(this IEnumerable<S> source, 
                                              Func<S, T> keySelector)
{
    HashSet<T> seenKeys = new HashSet<T>(); //credits Jon Skeet
    foreach (var element in source)
        if (seenKeys.Add(keySelector(element)))
            yield return element;
}

Как видите, обе функции принимают IEnumerable<>, но вызывающая сторона не знает, как реагируют функции. Итак, какие общие меры предосторожности вы, ребята, принимаете здесь?

  1. Назовите наши пользовательские методы соответствующим образом, чтобы у вызывающей стороны было представление о том, хорошо это или нет с Linq?

  2. Переместить ленивые методы в другое пространство имен и оставить Linq-ish в другом, чтобы это хоть как-то дало идею?

  3. Не принимать IEnumerable в качестве параметра для immediately исполняемых методов, а вместо этого использовать более производный тип или конкретный тип, который, таким образом, оставляет IEnumerable только для ленивых методов? Это возлагает на вызывающего бремя выполнения возможных невыполненных выражений? Для нас это вполне возможно, так как за пределами Linq мира мы почти не имеем дело с IEnumerable, а большинство базовых классов коллекций реализуют как минимум до ICollection.

Или что-то еще? Мне особенно нравится 3-й вариант, и это то, с чем я собирался, но подумал заранее узнать ваши идеи. Я видел много кода (приятных маленьких Linq вроде методов расширения!) даже от хороших программистов, которые принимают IEnumerable и делают ToList() или что-то подобное внутри метода. Я не знаю, как они справляются с побочными эффектами.

Редактировать: После отрицательного голосования и ответа я хотел бы уточнить, что дело не в том, что программисты не знают о том, как работает Linq (наше мастерство может быть на каком-то уровне, но это другое дело), ​​а в том, что тогда многие функции писались без учета Linq. Теперь цепочка немедленно выполняющегося метода вместе с методами расширения Linq делает его опасным. Итак, мой вопрос: есть ли общее руководство, которому следуют программисты, чтобы сообщить вызывающему абоненту, что использовать со стороны Linq, а что нет? Это больше относится к оборонительному программированию, чем если-вы-не-знаете-использовать-это-то-мы-не-можем-помочь! (по крайней мере, я так думаю)..


person nawfal    schedule 25.11.2012    source источник
comment
Если это cw, пожалуйста, сделайте так, я не могу найти вариант   -  person nawfal    schedule 26.11.2012
comment
Отложенное выполнение стало возможным, начиная с .Net 1.0, и простым, начиная с C# 2.0. LINQ просто делает его более распространенным.   -  person SLaks    schedule 26.11.2012
comment
@nawful: А? LINQ — это не волшебство; это просто набор довольно простых методов расширения. Я настоятельно рекомендую вам прочитать серию статей Джона Скита EduLINQ, чтобы понять как это работает.   -  person SLaks    schedule 26.11.2012
comment
@SLaks Хорошо, я прочитаю это, но есть ли в нем что-нибудь, связанное с отложенным выполнением со времен, предшествующих Linq? Я не мог увидеть это в ссылке, которую вы разместили с одного взгляда   -  person nawfal    schedule 26.11.2012
comment
Отложенное выполнение просто означает, что перечислитель работает в MoveNext(). Всегда можно было написать это вручную, и компилятор C# сделает это за вас с помощью ключевого слова yield.   -  person SLaks    schedule 26.11.2012
comment
Что касается № 3, вы должны принять параметр, который вам нужен, если вам нужно только перебрать список, используйте IEnumerable, если вам нужны методы ICollection, используйте его. Использование определенного типа параметра в качестве объяснения того, что делает функция, вместо того, чтобы иметь конкретное имя метода, кажется странным.   -  person Patrick    schedule 26.11.2012
comment
@SLaks Хорошо, я понял. Плохо разбирался в yield. Хм, так что я считаю, что этот вопрос все еще актуален, учитывая, что Linq сделал его таким распространенным. Просто пытаюсь увидеть, как люди пишут собственные методы, которые должны будут иметь дело с Linq.   -  person nawfal    schedule 26.11.2012
comment
Реальный вопрос заключается в том, следует ли вам проводить регрессионное тестирование при переходе на новую версию языка. Ответ положительный. :-) Серьезно, в тех немногих случаях, когда вам нужно повторить все перечисление, вы можете вызвать ToList() или ToArray(), чтобы заставить это поведение - я признаю, что это не идеально.   -  person SAJ14SAJ    schedule 26.11.2012
comment
Слишком много текста для вопроса Используете ли вы какую-либо разметку для методов, использующих отложенное выполнение. :) В любом случае это немного неконструктивно, поскольку многие люди могут делать разные вещи, и поэтому на этот вопрос нет правильного ответа.   -  person Patrick    schedule 26.11.2012
comment
Однозначного ответа нет, но в StackOverflow есть много примеров вопросов, на которые есть несколько хороших ответов, и эти вопросы часто являются наиболее ценными для сообщества. Пожалуйста, откройте этот вопрос повторно   -  person KCD    schedule 10.10.2019


Ответы (3)


Как видите, обе функции принимают IEnumerable<>, но вызывающая сторона не знает, как реагируют функции.

Это просто вопрос документации. Ознакомьтесь с документацией по DistinctBy в MoreLINQ, которая включает:

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

Да, важно знать, что делает участник, прежде чем использовать его, и для вещей, принимающих/возвращающих любые коллекции, есть несколько важных вещей, которые нужно знать:

  • Сборник будет прочитан сразу или отложен?
  • Будет ли коллекция передаваться в потоковом режиме во время возврата результатов?
  • Если объявленный принятый тип коллекции является изменяемым, попытается ли метод изменить его?
  • Если объявленный возвращаемый тип коллекции является изменяемым, будет ли он действительно изменяемой реализацией?
  • Будет ли возвращенная коллекция изменена другими действиями (например, это доступное только для чтения представление коллекции, которое может быть изменено внутри класса)
  • Является ли null приемлемым входным значением?
  • Является ли null допустимым значением элемента?
  • Будет ли метод когда-либо возвращать null?

Все эти вещи заслуживают внимания, и большинство из них стоило рассмотреть задолго до появления LINQ.

Мораль на самом деле такова: «Убедитесь, что вы знаете, как что-то ведет себя, прежде чем называть это». Так было до LINQ, и LINQ не изменил этого. Он просто представил две возможности (отложенное выполнение и потоковую передачу результатов), которые редко присутствовали раньше.

person Jon Skeet    schedule 26.11.2012
comment
Хорошо, я понимаю. Спасибо. Можете ли вы предоставить мне хорошую ссылку, которая дает четкое объяснение разницы между отложенным чтением и возвратом в потоковом режиме. Разве они не являются следствием друг друга? - person nawfal; 26.11.2012
comment
@nawfal: Ну, вы не можете выполнять потоковую передачу без использования отложенного выполнения, но некоторые операторы откладываются без потоковой передачи: как только вы запрашиваете первый элемент, они считывают все входные данные, прежде чем выдавать какие-либо выходные данные. . OrderBy и Reverse являются хорошими примерами этого. См. msmvps.com/blogs /jon_skeet/archive/2010/03/25/ еще немного моих мыслей по этому поводу. - person Jon Skeet; 26.11.2012
comment
Ок, Джон принял. Но я должен сказать, что незнание того, что null является приемлемым в качестве ввода или метода, изменяющего переданный тип коллекции и т. д., не так серьезно, как незнание того, выполняется ли метод немедленно или откладывается, поэтому я задал этот вопрос. Кстати, спасибо! :) - person nawfal; 27.11.2012

Используйте IEnumerable везде, где это имеет смысл, и безопасно кодируйте.

Как указал SLaks в комментарии, отложенное выполнение было возможно с IEnumerable с самого начала, а поскольку в C# 2.0 появился оператор yield, было очень легко реализовать отложенное выполнение самостоятельно. Например, этот метод возвращает IEnumerable, который использует отложенное выполнение для возврата некоторых случайных чисел:

public static IEnumerable<int> RandomSequence(int length)
{
    Random rng = new Random();
    for (int i = 0; i < length; i++) {
        Console.WriteLine("deferred execution!");
        yield return rng.Next();
    }
}

Поэтому всякий раз, когда вы используете foreach для перебора IEnumerable, вы должны предполагать, что между итерациями может произойти что угодно. Это может даже вызвать исключение, поэтому вы можете поместить цикл foreach внутрь try/finally.

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

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

person Jesse McGrew    schedule 26.11.2012

Отложенное выполнение не имеет ничего общего с типами. Любой метод linq, использующий итераторы, потенциально может отложить выполнение, если вы напишете свой код таким образом. Select(), Where(), OrderByDescending() например. все используют итераторы и, следовательно, откладывают выполнение. Да, эти методы ожидают IEnumerable<T>, но это не значит, что проблема IEnumerable<T>.

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

Итак, какие общие меры предосторожности вы, ребята, принимаете здесь?

Никто. Честно говоря, мы используем IEnumerable везде, и у нас нет проблем с тем, что люди не понимают «побочных эффектов». «Идея отложенного выполнения Linq» играет центральную роль в ее полезности в таких вещах, как Linq-to-SQL. Мне кажется, что дизайн пользовательских функций не так ясен, как мог бы быть. Если люди пишут код для использования LINQ и не понимают, что он делает, то проблема в этом, а не в том, что IEnumerable является базовым типом.

Все ваши идеи — это просто обертки вокруг того факта, что у вас есть программисты, которые просто не понимают linq-запросы. Если вам не нужно ленивое выполнение, которое, похоже, вам не нужно, просто заставьте все оценивать до выхода из функций. Вызовите ToList() для ваших результатов и верните их в согласованный API, с которым потребитель хотел бы работать — списки, массивы, коллекции или IEnumerables.

person womp    schedule 26.11.2012
comment
Я не имел в виду, что IEnumerable тоже проблема. А во-вторых, дело не в том, что программисты не знают, как работает Linq, а в том, что многие функции тогда писались без учета Linq. Теперь цепочка немедленно выполняющегося метода вместе с методами расширения Linq делает его опасным. Итак, мой вопрос: есть ли общее руководство, которому следуют программисты, чтобы сообщить вызывающему абоненту, что использовать со стороны Linq, а что нет? Я спрашиваю больше о защитном кодировании. - person nawfal; 26.11.2012
comment
Я не думаю, что понимаю. Если функции писались без учета Linq, почему им грозит отложенное выполнение? И если ваши методы расширения опасны, потому что они возвращают необработанные запросы, почему бы вам не применить предложенное мной исправление и не заставить их оценивать перед возвратом? - person womp; 26.11.2012
comment
То есть см. эту часть моего вопроса var query = foos.Select(p => p).Where(p => p).Shuffle().OrderBy(p => p); опасность (или, другими словами, нежелательность) заключается в том, что я смешиваю немедленно выполняющуюся функцию с методом Linq. И нет никаких сомнений в том, что мои методы возвращают необработанные запросы! Они это исполняют! Это проблема. Я не думаю, что вы получили мой вопрос .. - person nawfal; 26.11.2012
comment
пожалуйста, посмотрите мое редактирование, вы, должно быть, неправильно поняли мои слова .. Извините .. - person nawfal; 26.11.2012
comment
@nawfal: Насколько опасно вызывать Shuffle, а затем OrderBy? Что в этом утверждении нежелательно, кроме того факта, что они немного противодействуют...? - person Patrick; 26.11.2012
comment
@Patrick Патрик Мне бы потребовался невыполнимый (или отложенный) запрос. Первое выражение обеспечивает это. Но во втором случае, поскольку я выполняю foreach внутри Shuffle, выполняется первая половина запроса. Это не то, что обычно, когда я кодирую Linq так, как хочу. Таким образом, мой Shuffle отличается от других методов Linq, с которыми я работал. Итак, как защищаться по этому поводу. Общая практика, советы и т.д.. - person nawfal; 26.11.2012
comment
@nawfal: Хорошо, спасибо за объяснение, я не понимал, чего вы добивались, пока не прочитал ваше редактирование в вашем вопросе. - person Patrick; 26.11.2012