Отказ от ответственности: Этот пост больше не похож на мой первоначальный ответ, а скорее включает в себя около семи лет опыта, который я получил с тех пор. Я сделал правку, потому что это очень популярный вопрос, и ни один из существующих ответов не охватил все аспекты. Если вы хотите увидеть мой оригинальный ответ, он доступен в истории изменений для этого поста .
Первое, что нужно понять, - это такие операции C # linq, как Select()
, All()
, Where()
и т. Д., Корни которых лежат в функциональном программировании . Идея заключалась в том, чтобы перенести некоторые из наиболее полезных и доступных частей функционального программирования в мир .Net. Это важно, потому что ключевым принципом функционального программирования является то, что эти операции не должны иметь побочных эффектов. Трудно преуменьшить это. Добавление операций each()
или ForEach()
не только выходит за рамки функционального программирования других операторов linq, но и прямо противоположно им.
Но я понимаю, что это неудовлетворительно. У вас есть реальная проблема, которую вам нужно решить. Почему вся эта философия башни из слоновой кости должна мешать чему-то, что может быть действительно полезным?
Эрик Липперт, который в то время был в команде разработчиков C #, может помочь нам здесь. Он рекомендует просто использовать традиционную петлю foreach
:
[ForEach ()] добавляет ноль новых представительных возможностей в язык. Это позволяет вам переписать этот совершенно чистый код:
foreach(Foo foo in foos){ statement involving foo; }
в этот код:
foos.ForEach(foo=>{ statement involving foo; });
Суть в том, что если вы внимательно посмотрите на параметры синтаксиса, вы не получите ничего нового от расширения ForEach()
по сравнению с традиционным циклом foreach
. Я частично не согласен. Представьте, что у вас есть это:
foreach(var item in Some.Long(and => possibly)
.Complicated(set => ofLINQ)
.Expression(to => evaluate)
{
// now do something
}
Этот код скрывает значение, потому что он отделяет ключевое слово foreach
от операций в цикле. В нем также перечислены команды цикла before для операций, которые определяют последовательность, в которой работает цикл. Гораздо более естественно хотеть, чтобы сначала выполнялись эти операции, а затем команда loop в end определения запроса. Кроме того, код просто ужасен. Кажется, что было бы намного лучше написать это:
Some.Long(and => possibly)
.Complicated(set => ofLINQ)
.Expression(to => evaluate)
.ForEach(item =>
{
// now do something
});
Однако даже здесь я в конце концов пришел к точке зрения Эрика. Я понял, что код, который вы видите выше, вызывает дополнительную переменную. Если у вас есть сложный набор выражений LINQ, подобных этому, вы можете добавить некоторую ценную информацию в свой код, сначала присвоив результат выражения LINQ новой переменной:
var queryForSomeThing = Some.Long(and => possibly)
.Complicated(set => ofLINQ)
.Expressions(to => evaluate);
foreach(var item in queryForSomeThing)
{
// now do something
}
Этот код кажется более естественным. Ключевое слово foreach
помещается рядом с остальной частью цикла и после определения запроса. Более того, имя переменной может добавить новую информацию, которая будет полезна будущим программистам, пытающимся понять цель запроса LINQ. Опять же, мы видим, что требуемый оператор ForEach()
действительно не добавил новой выразительной силы в язык.
Однако нам все еще не хватает двух возможностей гипотетического метода расширения ForEach()
:
- Это не составно. Я не могу добавить еще
.Where()
или GroupBy()
или OrderBy()
после цикла foreach
, встроенного в остальную часть кода, без создания нового оператора.
- Это не ленивый. Эти операции происходят немедленно. Это не позволяет мне, скажем, иметь форму, где пользователь выбирает операцию в качестве одного поля на большом экране, на который не воздействуют, пока пользователь не нажмет командную кнопку. Эта форма может позволить пользователю передумать перед выполнением команды. Это совершенно нормально ( easy даже) с запросом LINQ, но не так просто с
foreach
.
Конечно, вы можете создать свой собственный ForEach()
метод расширения. Несколько других ответов уже имеют реализации этого метода; это не все так сложно. Тем не менее, я чувствую, что это не нужно. Уже существует существующий метод, который соответствует тому, что мы хотим сделать как с семантической, так и с операционной точек зрения. Обе вышеуказанные недостающие функции могут быть решены с помощью существующего оператора Select()
.
Select()
соответствует виду преобразования или проекции, описанному в обоих приведенных выше примерах. Имейте в виду, однако, что я все равно избегал бы создания побочных эффектов. Вызов Select()
должен вернуть либо новые объекты, либо проекции оригиналов. Иногда этому может помочь использование анонимного типа или динамического объекта (если и только если это необходимо). Если вам нужно, чтобы результаты сохранялись, скажем, в исходной переменной списка, вы всегда можете вызвать .ToList()
и присвоить ее исходной переменной. Здесь я добавлю, что мне больше нравится работать с переменными IEnumerable<T>
, чем с более конкретными типами.
myList = myList.Select(item => new SomeType(item.value1, item.value2 *4)).ToList();
В итоге:
- Просто придерживайтесь
foreach
большую часть времени.
- Когда
foreach
действительно не пойдет (что, вероятно, не так часто, как вы думаете), используйте Select()
- Когда вам нужно использовать
Select()
, вы все равно можете избежать (видимых программой) побочных эффектов, возможно, проецируя на анонимный тип.