for
so với foreach
Có một sự nhầm lẫn phổ biến là hai cấu trúc đó rất giống nhau và cả hai đều có thể hoán đổi cho nhau như thế này:
foreach (var c in collection)
{
DoSomething(c);
}
và:
for (var i = 0; i < collection.Count; i++)
{
DoSomething(collection[i]);
}
Thực tế là cả hai từ khóa bắt đầu bằng ba chữ cái giống nhau không có nghĩa là về mặt ngữ nghĩa, chúng giống nhau. Sự nhầm lẫn này rất dễ bị lỗi, đặc biệt là cho người mới bắt đầu. Lặp lại thông qua một bộ sưu tập và làm một cái gì đó với các yếu tố được thực hiện với foreach
; for
không phải và không nên được sử dụng cho mục đích này , trừ khi bạn thực sự biết những gì bạn đang làm.
Hãy xem điều gì sai với nó bằng một ví dụ. Cuối cùng, bạn sẽ tìm thấy mã đầy đủ của một ứng dụng demo được sử dụng để thu thập kết quả.
Trong ví dụ này, chúng tôi đang tải một số dữ liệu từ cơ sở dữ liệu, chính xác hơn là các thành phố từ Adventure Works, được sắp xếp theo tên, trước khi gặp "Boston". Truy vấn SQL sau được sử dụng:
select distinct [City] from [Person].[Address] order by [City]
Dữ liệu được tải theo ListCities()
phương thức trả về một IEnumerable<string>
. Đây là những gì foreach
trông giống như:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Hãy viết lại bằng một for
, giả sử rằng cả hai đều có thể hoán đổi cho nhau:
var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
var city = cities.ElementAt(i);
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Cả hai trở về cùng một thành phố, nhưng có một sự khác biệt rất lớn.
- Khi sử dụng
foreach
, ListCities()
được gọi một lần và mang lại 47 mục.
- Khi sử dụng
for
, ListCities()
được gọi là 94 lần và mang lại 28153 mặt hàng tổng thể.
Chuyện gì đã xảy ra?
IEnumerable
là lười biếng . Nó có nghĩa là nó sẽ thực hiện công việc chỉ vào lúc này khi cần kết quả. Đánh giá lười biếng là một khái niệm rất hữu ích, nhưng có một số lưu ý, bao gồm thực tế là rất dễ bỏ lỡ khoảnh khắc mà kết quả sẽ cần thiết, đặc biệt là trong trường hợp kết quả được sử dụng nhiều lần.
Trong trường hợp a foreach
, kết quả chỉ được yêu cầu một lần. Trong trường hợp for
như được thực hiện trong mã được viết không chính xác ở trên , kết quả được yêu cầu 94 lần , tức là 47 × 2:
Truy vấn cơ sở dữ liệu 94 lần thay vì một cơ sở dữ liệu là khủng khiếp, nhưng không phải là điều tồi tệ hơn có thể xảy ra. Ví dụ, hãy tưởng tượng điều gì sẽ xảy ra nếu select
truy vấn được đi trước bởi một truy vấn cũng chèn một hàng trong bảng. Đúng vậy, chúng ta sẽ có for
cơ sở dữ liệu sẽ gọi cơ sở dữ liệu 2.147.483.647 lần, trừ khi nó hy vọng gặp sự cố trước đó.
Tất nhiên, mã của tôi là thiên vị. Tôi cố tình sử dụng sự lười biếng IEnumerable
và viết nó theo cách để liên tục gọi ListCities()
. Người ta có thể lưu ý rằng một người mới bắt đầu sẽ không bao giờ làm điều đó, bởi vì:
Các IEnumerable<T>
không có tài sản Count
, nhưng chỉ có phương pháp Count()
. Gọi một phương thức là đáng sợ, và người ta có thể mong đợi kết quả của nó không được lưu vào bộ nhớ cache và không phù hợp trong một for (; ...; )
khối.
Việc lập chỉ mục không khả dụng IEnumerable<T>
và không rõ ràng để tìm ElementAt
phương thức mở rộng LINQ.
Có lẽ hầu hết những người mới bắt đầu sẽ chuyển đổi kết quả của ListCities()
một thứ mà họ quen thuộc, như a 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;
}
}
Tuy nhiên, mã này rất khác với foreach
thay thế. Một lần nữa, nó cho kết quả tương tự, và lần này ListCities()
phương thức chỉ được gọi một lần, nhưng mang lại 575 mục, trong khi với foreach
, nó chỉ mang lại 47 mục.
Sự khác biệt đến từ thực tế là ToList()
khiến tất cả dữ liệu được tải từ cơ sở dữ liệu. Mặc dù foreach
chỉ yêu cầu các thành phố trước "Boston", nhưng for
thành phố mới yêu cầu tất cả các thành phố phải được truy xuất và lưu trữ trong bộ nhớ. Với 575 chuỗi ngắn, có lẽ nó không tạo ra nhiều khác biệt, nhưng nếu chúng ta chỉ lấy được vài hàng từ một bảng chứa hàng tỷ bản ghi thì sao?
Vì vậy foreach
, thực sự là gì?
foreach
gần hơn với một vòng lặp while. Mã tôi đã sử dụng trước đây:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
có thể được thay thế đơn giản bằng:
using (var enumerator = Program.ListCities().GetEnumerator())
{
while (enumerator.MoveNext())
{
var city = enumerator.Current;
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
Cả hai đều sản xuất cùng một IL. Cả hai đều có kết quả như nhau. Cả hai đều có tác dụng phụ như nhau. Tất nhiên, điều này while
có thể được viết lại trong một vô hạn tương tự for
, nhưng nó sẽ còn dài hơn và dễ bị lỗi. Bạn được tự do chọn một trong những bạn thấy dễ đọc hơn.
Bạn muốn tự kiểm tra nó? Đây là mã đầy đủ:
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();
}
}
}
}
}
}
Và kết quả:
--- cho ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Dữ liệu được gọi là 94 lần và mang lại 28153 mục.
--- với danh sách ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Dữ liệu được gọi là 1 lần và mang lại 575 mục.
--- trong khi ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Dữ liệu được gọi là 1 lần và mang lại 47 mục.
--- foreach ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Dữ liệu được gọi là 1 lần và mang lại 47 mục.
LINQ so với cách truyền thống
Đối với LINQ, bạn có thể muốn học lập trình chức năng (FP) - không phải công cụ C # FP, mà là ngôn ngữ FP thực như Haskell. Các ngôn ngữ chức năng có một cách cụ thể để thể hiện và trình bày mã. Trong một số tình huống, nó vượt trội hơn các mô hình phi chức năng.
FP được biết là vượt trội hơn nhiều khi nói đến việc thao túng danh sách ( danh sách là một thuật ngữ chung, không liên quan đến List<T>
). Với thực tế này, khả năng thể hiện mã C # theo cách có chức năng hơn khi nói đến danh sách là một điều tốt.
Nếu bạn không bị thuyết phục, hãy so sánh khả năng đọc mã được viết theo cả hai cách chức năng và phi chức năng trong câu trả lời trước của tôi về chủ đề này.