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