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 # более функциональным способом, когда дело доходит до списков, является довольно хорошей вещью.
Если вы не уверены, сравните читабельность кода, написанного как функциональными, так и нефункциональными способами, в моем предыдущем ответе на эту тему.