for против foreach
Существует распространенное заблуждение, что эти две конструкции очень похожи и что они взаимозаменяемы, как это:
foreach (var c in collection)
{
DoSomething(c);
}
а также:
for (var i = 0; i < collection.Count; i++)
{
DoSomething(collection[i]);
}
Тот факт, что оба ключевых слова начинаются с одинаковых трех букв, не означает, что семантически они похожи. Эта путаница чрезвычайно подвержена ошибкам, особенно для начинающих. Перебор коллекции и выполнение чего-либо с элементами сделано с помощью foreach; forне должен и не должен использоваться для этой цели , если вы действительно не знаете, что делаете.
Давайте посмотрим, что не так с этим на примере. В конце вы найдете полный код демонстрационного приложения, используемого для сбора результатов.
В этом примере мы загружаем некоторые данные из базы данных, точнее, города из Adventure Works, упорядоченные по имени, прежде чем встретить «Бостон». Используется следующий запрос SQL:
select distinct [City] from [Person].[Address] order by [City]
Данные загружаются ListCities()методом, который возвращает IEnumerable<string>. Вот как это foreachвыглядит:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Давайте перепишем его for, предполагая, что оба они взаимозаменяемы:
var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
var city = cities.ElementAt(i);
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Оба возвращают одни и те же города, но есть огромная разница.
- При использовании
foreach, ListCities()вызывается один раз и дает 47 пунктов.
- При использовании
for, ListCities()называется 94 раз и дает 28153 пунктов в целом.
Что случилось?
IEnumerableэто ленивый . Это означает, что он будет выполнять работу только в тот момент, когда нужен результат. Ленивая оценка - это очень полезная концепция, но она имеет некоторые предостережения, в том числе тот факт, что легко упустить момент (ы), когда потребуется результат, особенно в тех случаях, когда результат используется несколько раз.
В случае a foreachрезультат запрашивается только один раз. В случае, for как это реализовано в неправильно написанном коде выше , результат запрашивается 94 раза , то есть 47 × 2:
Запрашивать базу данных 94 раза вместо одной ужасно, но не хуже, что может случиться. Представьте, например, что произойдет, если selectзапросу предшествует запрос, который также вставляет строку в таблицу. Правильно, у нас было бы то, forчто будет вызывать базу данных 2 147 483 647 раз, если мы не надеемся, что она завершится раньше.
Конечно, мой код необъективен. Я сознательно воспользовался своей ленью IEnumerableи написал это так, чтобы неоднократно звонить ListCities(). Можно заметить, что новичок никогда этого не сделает, потому что:
Свойство IEnumerable<T>не имеет Count, а только метод Count(). Вызов метода является пугающим, и можно ожидать, что его результат не будет кэширован и непригоден для for (; ...; )блока.
Индексирование недоступно для IEnumerable<T>и не очевидно, чтобы найти ElementAtметод расширения LINQ.
Вероятно, большинство новичков просто преобразуют результат ListCities()в нечто, с чем они знакомы, например List<T>.
var cities = Program.ListCities();
var flushedCities = cities.ToList();
for (var i = 0; i < flushedCities.Count; i++)
{
var city = flushedCities[i];
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Тем не менее, этот код сильно отличается от foreachальтернативного. Опять же, он дает те же результаты, и на этот раз ListCities()метод вызывается только один раз, но дает 575 элементов, в то время как с помощью foreachон дает только 47 элементов.
Разница исходит из того , что ToList()вызывает все данные должны быть загружены из базы данных. В то время как foreachзапрошены только города до «Бостона», новый forтребует, чтобы все города были извлечены и сохранены в памяти. С 575 короткими строками это, вероятно, не имеет большого значения, но что если мы извлекаем только несколько строк из таблицы, содержащей миллиарды записей?
Так что же на foreachсамом деле?
foreachближе к циклу времени. Код, который я ранее использовал:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
можно просто заменить на:
using (var enumerator = Program.ListCities().GetEnumerator())
{
while (enumerator.MoveNext())
{
var city = enumerator.Current;
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
Оба производят один и тот же IL. Оба имеют одинаковый результат. Оба имеют одинаковые побочные эффекты. Конечно, это whileможно переписать аналогичным образом for, но оно будет еще длиннее и подвержено ошибкам. Вы можете выбрать тот, который вы найдете более читабельным.
Хотите сами это проверить? Вот полный код:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
public class Program
{
private static int countCalls;
private static int countYieldReturns;
public static void Main()
{
Program.DisplayStatistics("for", Program.UseFor);
Program.DisplayStatistics("for with list", Program.UseForWithList);
Program.DisplayStatistics("while", Program.UseWhile);
Program.DisplayStatistics("foreach", Program.UseForEach);
Console.WriteLine("Press any key to continue...");
Console.ReadKey(true);
}
private static void DisplayStatistics(string name, Action action)
{
Console.WriteLine("--- " + name + " ---");
Program.countCalls = 0;
Program.countYieldReturns = 0;
var measureTime = Stopwatch.StartNew();
action();
measureTime.Stop();
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("The data was called {0} time(s) and yielded {1} item(s) in {2} ms.", Program.countCalls, Program.countYieldReturns, measureTime.ElapsedMilliseconds);
Console.WriteLine();
}
private static void UseFor()
{
var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
var city = cities.ElementAt(i);
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
private static void UseForWithList()
{
var cities = Program.ListCities();
var flushedCities = cities.ToList();
for (var i = 0; i < flushedCities.Count; i++)
{
var city = flushedCities[i];
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
private static void UseForEach()
{
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
private static void UseWhile()
{
using (var enumerator = Program.ListCities().GetEnumerator())
{
while (enumerator.MoveNext())
{
var city = enumerator.Current;
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
}
private static IEnumerable<string> ListCities()
{
Program.countCalls++;
using (var connection = new SqlConnection("Data Source=mframe;Initial Catalog=AdventureWorks;Integrated Security=True"))
{
connection.Open();
using (var command = new SqlCommand("select distinct [City] from [Person].[Address] order by [City]", connection))
{
using (var reader = command.ExecuteReader(CommandBehavior.SingleResult))
{
while (reader.Read())
{
Program.countYieldReturns++;
yield return reader["City"].ToString();
}
}
}
}
}
}
И результаты:
--- для ---
Абингдон Олбани Александрия Альгамбра [...] Бонн Бордо Бостон
Данные были названы 94 раз (а) и дали 28153 пункта (ов).
--- со списком ---
Абингдон Олбани Александрия Альгамбра [...] Бонн Бордо Бостон
Данные были вызваны 1 раз (а) и дали 575 пунктов.
--- в то время ---
Абингдон Олбани Александрия Альгамбра [...] Бонн Бордо Бостон
Данные были вызваны 1 раз (а) и дали 47 пунктов.
--- foreach ---
Абингдон Олбани Александрия Альгамбра [...] Бонн Бордо Бостон
Данные были вызваны 1 раз (а) и дали 47 пунктов.
LINQ против традиционного способа
Что касается LINQ, вы можете изучить функциональное программирование (FP) - не материал C # FP, а настоящий язык FP, такой как Haskell. Функциональные языки имеют особый способ выражения и представления кода. В некоторых ситуациях он превосходит нефункциональные парадигмы.
Известно, что FP намного лучше, когда дело доходит до манипулирования списками ( список как общий термин, не связанный с List<T>). Учитывая этот факт, возможность выражать код C # более функциональным способом, когда дело доходит до списков, является довольно хорошей вещью.
Если вы не уверены, сравните читабельность кода, написанного как функциональными, так и нефункциональными способами, в моем предыдущем ответе на эту тему.