Является ли плохой практикой намеренно полагаться на побочные эффекты Linq?

Подобный шаблон программирования появляется время от времени:

int staleCount = 0;

fileUpdatesGridView.DataSource = MultiMerger.TargetIds
    .Select(id =>
    {
        FileDatabaseMerger merger = MultiMerger.GetMerger(id);

        if (merger.TargetIsStale)
            staleCount++;

        return new
        {
            Id = id,
            IsStale = merger.TargetIsStale,
            // ...
        };
    })
    .ToList();

fileUpdatesGridView.DataBind();
fileUpdatesMergeButton.Enabled = staleCount > 0;

Я не уверен, что есть более краткий способ закодировать это?

Даже если это так, это плохая практика?


person crokusek    schedule 14.08.2015    source источник
comment
О каком побочном эффекте вы говорите?   -  person Jeroen Vannevel    schedule 14.08.2015
comment
Вы потребляете перечисление, гарантируя, что оно выполнено и не будет выполняться снова, поэтому, если перед этим вы сбросите staleCount, в этом нет ничего плохого.   -  person SimpleVar    schedule 14.08.2015
comment
Побочным эффектом является изменение staleCount, которое находится за пределами конвейера linq, и делает это в Select(), а не в Foreach().   -  person crokusek    schedule 14.08.2015
comment
Это пример/заглушка/гипотетический код, и он будет закрыт как таковой, если будет опубликован в Code Review. Для Code Review требуется конкретный работающий код. Я рекомендую вам ознакомиться с справочным центром, прежде чем давать дополнительные рекомендации, @SamAxe.   -  person nhgrif    schedule 14.08.2015
comment
Все испортится, если вы добавите в смесь .AsParallel().   -  person spender    schedule 14.08.2015
comment
Я голосую за то, чтобы закрыть этот вопрос как не по теме, потому что это не конкретный вопрос программирования, а опрос.   -  person nhgrif    schedule 14.08.2015
comment
Итак, тонны этого кода начинают накапливаться годами, потому что нам приходится искать в чьем-то личном блоге, чтобы оценить настроение? Просто нужно переформулировать?   -  person crokusek    schedule 14.08.2015
comment
@crokusek Если вы написали реальный конкретный код и были заинтересованы в отзывах о нем, вы потенциально могли бы превратить это в Code Reviewпо теме а> вопрос. Но Stack Overflow не подходит для такого рода вопросов. Это не то, о чем Stack Overflow. И мы здесь не для того, чтобы рекомендовать вам, где получить информацию, которую вы ищете. Просто говорю, что этот вопрос как есть не подходит для формата Stack Overflow.   -  person nhgrif    schedule 14.08.2015
comment
Если я скажу, что это плохая практика, а не по шкале от 1 до 10, будет ли это нормально?   -  person crokusek    schedule 14.08.2015
comment
@crokusek, вероятно, нет, это все еще полностью основано на мнении. Если определенный стиль кода не является полностью неприемлемым (не ваш случай), на такие вопросы редко можно ответить определенным утверждением. Вы можете легко превратить этот вопрос в SO-совместимый, если у вас действительно есть конкретная цель: как избежать побочных эффектов в... или этот код безопасен для параллельного/сделать параллельным.   -  person Alexei Levenkov    schedule 14.08.2015
comment
Я действительно хочу знать, следует ли мне избегать побочных эффектов. Может быть, мне просто нужно добавить комикс, поскольку этот похожий случай живет. Есть и другие случаи плохой практики, которые были закрыты, но, по крайней мере, они получили несколько хороших ответов до того, как были закрыты.   -  person crokusek    schedule 14.08.2015
comment
Недавние разногласия и почему это важный вопрос: убедитесь, что селектор запускается во время подсчета   -  person crokusek    schedule 26.05.2017


Ответы (2)


Нет, это не является строго "плохой практикой" (например, построение SQL-запросов с конкатенацией строк пользовательского ввода или использованием goto).

Иногда такой код более читабелен, чем несколько запросов/foreach или вызов Aggregate без побочных эффектов. Также неплохо хотя бы попытаться написать версии foreach и версии без побочных эффектов, чтобы увидеть, какая из них более читабельна/легче доказать правильность.

Обратите внимание, что:

  • часто очень трудно понять, что/когда произойдет с таким кодом. т.е. вы пробуете хаки вокруг факта ленивого выполнения запросов LINQ с вызовом .ToList(), иначе это значение не будет вычислено.
  • чистые функции могут выполняться параллельно, если есть побочные эффекты, для этого нужно быть очень осторожным
  • если вам когда-нибудь понадобится преобразовать LINQ-to-Object в LINQ-to-SQL, вам придется переписать такие запросы
  • обычно запросы LINQ отдают предпочтение функциональному стилю программирования без побочных эффектов (и, следовательно, по соглашению читатели не ожидают побочных эффектов в коде).
person Alexei Levenkov    schedule 14.08.2015
comment
Я бы сказал, что это ужасная практика, потому что реализация LINQ не гарантирует, что поведение перечисления не будет изменено или оптимизировано в будущем. В то время как .ToList(), вероятно, продолжит принудительно перечислять все элементы, .NET Core уже оптимизирует .Count() и .Skip(). Это уже уменьшит количество элементов, итерируемых из исходной коллекции, и внесет критическое изменение, если будут полагаться на побочные эффекты. - person Ryan; 18.05.2021

Почему бы просто не закодировать это так:

var result=MultiMerger.TargetIds
    .Select(id =>
    {
        FileDatabaseMerger merger = MultiMerger.GetMerger(id);

        return new
        {
            Id = id,
            IsStale = merger.TargetIsStale,
            // ...
        };
    })
    .ToList();
fileUpdatesGridView.DataSource = result;
fileUpdatesGridView.DataBind();
fileUpdatesMergeButton.Enabled = result.Any(r=>r.IsStale);

Я бы посчитал это плохой практикой. Вы делаете предположение, что лямбда-выражение принудительно выполняется, потому что вы вызвали ToList. Это деталь реализации текущей версии ToList. Что, если изменить ToList в .NET 7.x, чтобы он возвращал объект, который полулениво преобразует IQueryable? Что, если он изменился для параллельного запуска лямбды? Внезапно у вас возникли проблемы с параллелизмом на вашем staleCount. Насколько я знаю, обе эти возможности могут сломать ваш код из-за неправильных предположений, которые делает ваш код.

Теперь, что касается многократного вызова MultiMerger.GetMerger с одним идентификатором, это действительно должно быть переработано, чтобы быть соединением, поскольку логика выполнения соединения (w|c) будет намного эффективнее, чем то, что вы там закодировали, и будет масштабировать намного лучше, особенно если реализация MultiMerger фактически извлекает данные из базы данных (или может быть изменена для этого).

Что касается вызова ToList() перед передачей его в источник данных, если источник данных не использует все поля в вашем новом объекте, вы будете (намного) быстрее и займете меньше памяти, чтобы пропустить ToList и позволить источнику данных только извлекать нужные ему поля. То, что вы сделали, — это строгое связывание данных с точными требованиями представления, чего следует избегать, где это возможно. Например, что делать, если вам вдруг нужно отобразить поле, которое существует в FileDatabaseMerger, но не находится в вашем текущем анонимном объекте? Теперь вам нужно внести изменения как в контроллер, так и в представление, чтобы добавить его, где, если бы вы просто передали IQueryable, вам нужно было бы изменить только представление. Опять же, быстрее, меньше памяти, более гибкое и более удобное в сопровождении.

Надеюсь, это поможет .. И этот вопрос действительно должен быть опубликован для проверки кода, а не для stackoverflow.

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

var result=MultiMerger.GetMergersByIds(MultiMerger.TargetIds);
fileUpdatesGridView.DataSource = result;
fileUpdatesGridView.DataBind();
fileUpdatesMergeButton.Enabled = result.Any(r=>r.TargetIsStale);

or

var result=MultiMerger.GetMergers().Where(m=>MultiMerger.TargetIds.Contains(m.Id));
fileUpdatesGridView.DataSource = result;
fileUpdatesGridView.DataBind();
fileUpdatesMergeButton.Enabled = result.Any(r=>r.TargetIsStale);
person Robert McKee    schedule 14.08.2015
comment
Any() включает неустаревшие записи, поэтому это не сработает. - person crokusek; 14.08.2015
comment
Хотя, похоже, вы должны использовать левое соединение, а не свою функцию. - person Robert McKee; 14.08.2015
comment
Мне это нравится. Так вы говорите, что это была плохая практика? - person crokusek; 14.08.2015
comment
Это плохая практика, да. Если вы пропустите свой код через хороший анализатор кода, он, скорее всего, тоже пометит его по ряду причин... Выбор выполняется лениво, но затем вы принудительно запускаете его с помощью ToList. Я бы тоже не стал вызывать ToList перед передачей в источник данных. Позвольте ему получить запрос на случай, если он захочет выполнить такие действия, как сортировка, разбиение по страницам и т. д. Вы победили лень, вручную реализовали левое соединение (вероятно, не столь эффективное), а затем у вас есть переменная, которая вычисляется в ленивой функции и использует ее вне области запроса. - person Robert McKee; 14.08.2015
comment
Да, известно о задержке выполнения, но есть другие причины. Разве ваш пример не сломался бы без ToList(), потому что его нужно было бы выполнить дважды? - person crokusek; 14.08.2015
comment
Давайте продолжим обсуждение в чате. - person crokusek; 14.08.2015
comment
Основываясь на заголовке вопроса, становится ясно, что шаблон программирования использования побочных эффектов Linq в целом не обязательно должен включать использование DataSource. Я извиняюсь за пример, который вызывает критику, расходящуюся с заголовком. Но спасибо, что не упускаете проблемы из виду. - person crokusek; 14.08.2015
comment
Linq будет выполняться дважды с вашими последними двумя примерами (DataBind + Any()), поскольку нет никакой материализации с ToList(). Нет функций GetMerges() или GetMergesByIds(). Даже если они были созданы, вы предполагали, что идентификатор доступен в самом слиянии, но это не так. Вы предположили, что есть представление с сопоставлением столбцов, но его нет. Первый пример был отличным и был сосредоточен на заголовке вопроса. - person crokusek; 14.08.2015
comment
Давайте продолжим обсуждение в чате. - person Robert McKee; 15.08.2015