for
vs. 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:
Кожен раз, коли cities.Count()
телефонує (47 разів),
Кожен раз cities.ElementAt(i)
викликається (47 разів).
Запит на базу даних 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;
}
}
}
Обидва виробляють однаковий ІР. Обидва мають однаковий результат. Обидва мають однакові побічні ефекти. Звичайно, це 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 позицій.
--- передбачати ---
Абінгдон Олбані Олександрія Алгамбра [...] Бон Бордо Бостон
Дані називались 1 раз (ів) і давали 47 позицій.
LINQ порівняно з традиційним способом
Що стосується LINQ, то, можливо, ви захочете вивчити функціональне програмування (FP) - не речі C # FP, а справжня мова FP, як Haskell. Функціональні мови мають специфічний спосіб вираження та подання коду. У деяких ситуаціях вона перевершує нефункціональні парадигми.
FP, як відомо, набагато перевершує, коли йдеться про маніпулювання списками ( список як загальний термін, не пов'язаний з List<T>
). З огляду на цей факт, можливість виражати код C # більш функціональним чином, коли мова йде про списки - це досить гарна річ.
Якщо ви не впевнені, порівняйте читабельність коду, написаного як функціональним, так і нефункціональним способом у моїй попередній відповіді на цю тему.