LINQ và IEnumerable<T>
là pull. Điều này có nghĩa là các biến vị ngữ và hành động là một phần của câu lệnh LINQ nói chung không được thực thi cho đến khi các giá trị được kéo. Ngoài ra, các biến vị ngữ và hành động sẽ thực hiện mỗi giá trị thời gian được kéo (ví dụ: không có bộ nhớ đệm bí mật nào xảy ra).
kéo từ một IEnumerable<T>
được thực hiện bởi các tuyên bố foreach
mà thực sự là cú pháp đường để nhận một Enumerator bằng cách gọi IEnumerable<T>.GetEnumerator()
và liên tục gọi IEnumerator<T>.MoveNext()
để kéo các giá trị.
khai thác LINQ như ToList()
, ToArray()
, ToDictionary()
và ToLookup()
kết thúc tốt đẹp một tuyên bố foreach
do đó, những phương pháp này sẽ làm một kéo. Điều tương tự cũng có thể được nói về các toán tử như Aggregate()
, Count()
và First()
. Những phương pháp này có điểm chung là chúng tạo ra một kết quả duy nhất mà phải được tạo ra bằng cách thực hiện câu lệnh foreach
.
Nhiều toán tử LINQ tạo một chuỗi IEnumerable<T>
mới. Khi một phần tử được kéo ra khỏi chuỗi kết quả, toán tử sẽ lấy một hoặc nhiều phần tử từ chuỗi nguồn. Toán tử Select()
là ví dụ rõ ràng nhất nhưng các ví dụ khác là SelectMany()
, Where()
, Concat()
, Union()
, Distinct()
, Skip()
và Take()
. Các toán tử này không thực hiện bất kỳ bộ nhớ đệm nào. Khi phần tử N'th được kéo từ Select()
, nó kéo phần tử thứ N từ trình tự nguồn, áp dụng phép chiếu bằng cách sử dụng hành động được cung cấp và trả về nó. Không có gì bí mật xảy ra ở đây.
Các toán tử LINQ khác cũng tạo ra các chuỗi IEnumerable<T>
mới nhưng chúng được thực hiện bằng cách thực sự kéo toàn bộ chuỗi nguồn, thực hiện công việc của chúng và sau đó tạo chuỗi mới. Các phương pháp này bao gồm Reverse()
, OrderBy()
và GroupBy()
. Tuy nhiên, thao tác kéo được thực hiện bởi toán tử chỉ được thực hiện khi chính toán tử được kéo có nghĩa là bạn vẫn cần một vòng lặp foreach
"ở cuối" của câu lệnh LINQ trước khi bất cứ điều gì được thực thi. Bạn có thể lập luận rằng các toán tử này sử dụng bộ nhớ cache vì chúng ngay lập tức kéo toàn bộ chuỗi nguồn. Tuy nhiên, cache này được xây dựng mỗi khi toán tử được lặp lại vì vậy nó thực sự là một chi tiết thực hiện và không phải là một cái gì đó kỳ diệu sẽ phát hiện rằng bạn đang áp dụng cùng một thao tác OrderBy()
nhiều lần cho cùng một trình tự.
Trong ví dụ của bạn, ToList()
sẽ thực hiện thao tác kéo. Hành động ở bên ngoài Select
sẽ thực hiện 100 lần. Mỗi khi hành động này được thực thi, Aggregate()
sẽ thực hiện một thao tác kéo khác sẽ phân tích các thuộc tính XML. Trong tổng số mã của bạn sẽ gọi Int32.Parse()
200 lần.
Bạn có thể cải thiện điều này bằng cách kéo các thuộc tính một lần thay vì trên mỗi lần lặp:
var X = XElement.Parse (@"
<ROOT>
<MUL v='2' />
<MUL v='3' />
</ROOT>
")
.Elements()
.Select (t => Int32.Parse (t.Attribute ("v").Value))
.ToList();
Enumerable.Range (1, 100)
.Select (s => x.Aggregate (s, (t, u) => t * u))
.ToList()
.ForEach (s => Console.WriteLine (s));
Bây giờ Int32.Parse()
chỉ được gọi 2 lần. Tuy nhiên, chi phí là một danh sách các giá trị thuộc tính phải được cấp phát, lưu trữ và cuối cùng là rác được thu thập. (Không phải là mối quan tâm lớn khi danh sách chứa hai phần tử.)
Lưu ý rằng nếu bạn quên ToList()
đầu tiên sẽ kéo các thuộc tính, mã sẽ chạy nhưng với các đặc tính hiệu suất giống hệt như mã gốc. Không có không gian được sử dụng để lưu trữ các thuộc tính nhưng chúng được phân tích cú pháp trên mỗi lần lặp.
"làm thế nào tôi có thể tự mình tìm ra một thứ như thế này" - cách tốt nhất là nghiên cứu IL được tạo ra từ mã này. – Andrey
Bạn có thể đặt điểm ngắt trình gỡ rối trên phương thức Parse() và xem tần suất truy cập. –