Какие методы могут защитить от неожиданного отложенного выполнения с 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); //does not execute
//but
var query = foos.Select(p => p).Where(p => p).Shuffle().OrderBy(p => p);
//the second line executes up to a point.

Большая правка:

Повторное открытие этого: критика языковой функциональности не конструктивна - однако спрос на передовую практику - вот где сияет Кару. Обновлен вопрос, чтобы отразить это.

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

Чтобы прояснить вышеприведенную строку - мой вопрос не о том, что второе выражение не оценивается, серьезно нет. Программисты это знают. Меня беспокоит метод TG46, который на самом деле выполняет запрос до этого момента. Смотрите первый запрос, где ничего не выполняется. Теперь аналогичным образом при создании другого выражения 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, а что нет? Это больше о программировании для защиты, чем если вы не знаете, как использовать то, тогда мы не можем помочь! (или, по крайней мере, я верю)..

Ответ 1

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

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

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

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

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

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

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

Ответ 2

Использовать IEnumerable везде, где это имеет смысл, и защищать код.

Как отмечал SLaks в комментарии, отсроченное выполнение возможно с IEnumerable с самого начала, и поскольку С# 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 или какой вуду он делает, чтобы придумать каждый элемент! В очень редком случае, когда вы действительно не можете разрешить запуск какого-либо неожиданного кода во время итерации, вы должны принять массив , а не какой-либо интерфейс.

Ответ 3

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

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

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

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

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