IENUMERABLE C# LÀ GÌ

  -  
*

The Programmable Me


Ở phần 1, chúng ta đã bàn về Functional programing (FP). Nếu như đây là lần đầu bạn tiếp xúc với khái niệm này và cảm thấy nó khó hiểu thì cũng đừng sợ, vì ai cũng giống bạn cả thôi. FP là một khái niệm quan trọng và cũng là một trong những cơ sở để Linq trong .NET được xây dựng. Vì vậy trong quá trình tìm hiểu về Linq, bạn cũng sẽ hiểu rõ được hơn về FP.

Bạn đang xem: Ienumerable c# là gì

Trở lại với Linq, tính năng của C# hỗ trợ bạn thao tác với các tập hợp dữ liệu. Một trong những điều mà mình thấy nhiều lập trình viên mắc phải, đó là việc sử dụng Linq sai quy cách do không hiểu rõ bản chất của thư viện này. Chúng ta hãy thử nhìn vào định nghĩa của một method Linq điển hình như Where():

public static IEnumerable Where(this IEnumerable source,Func predicate)Như các bạn thấy, method này được viết dưới dạng “extionsion method”. Đối với những ai không biết, “extension method” là một tính năng của C# nhằm giúp mở rộng thêm các method cho class hoặc interface của bạn bằng việc viết nó dưới dạng “static” ở một class khác và sử dụng từ khóa “this”. Ở method trên, chúng ta có thể thấy rằng, interface được mở rộng là IEnumerable. Nếu như bạn đã đọc kĩ phần trước, bạn sẽ biết rằng IEnumerable là interface chủ yếu được Linq hướng đến.Vì vậy để hiểu rõ Linq thì chúng ta cần nắm vững về interface này.

IEnumerable và từ khóa yield return

IEnumerable được định nghĩa trong System.Collection.Generics và là một interface vô cùng đơn giản:

public interface IEnumerable{ IEnumerator GetEnumerator();}Như bạn thấy, interface này chỉ có 1 method duy nhất, GetEnumerator(), trả về một giá trị có dạng IEnumerator. IEnumerator được định nghĩa như sau:

public interface IEnumerator{ T Current{get;} void Dispose(); bool MoveNext(); void Reset();}Nhìn vào hai interface trên, chắc hẳn các bạn cũng đoán được ý nghĩa của chúng. IEnumerator cho phép bạn thực hiện đọc qua tất cả các giá trị trong một tập hợp. Hãy tưởng tượng bạn đang muốn đọc và xử lý các dòng trong một bảng biểu, khi bạn muốn đọc sang dữ liệu tiếp theo, bạn gọi MoveNext() và đọc dữ liệu đó qua thuộc tính Current. Khi không còn dữ liệu nữa, MoveNext() sẽ trả về false. Bạn cũng có thể đọc lại từ đầu bằng method Reset(), hoặc xóa bỏ cả tập hợp bằng Dispose()

IEnumerable là interface cơ bản nhất đại diện cho các kiểu tập hợp dữ liệu trong C#, vì nó định nghĩa những tính năng cơ bản nhất mà một tập hợp phải có: khả năng đọc các phần tử một cách lần lượt. Tất cả các class dùng để chứa các tập hợp dữ liệu (Dictionary, Collection, ArrayList, List…) đều kế thừa từ interface này. IEnumerable cũng là interface cho phép bạn gọi từ khóa foreach , về cơ bản chính là một vòng lặp mà tự động gọi MoveNext() và Current

Việc thực hiện interface này khá đơn giản: Bạn có thể viết một linked-list để chứa dữ liệu, vì linked-list cũng cho phép truy cập phần tử một cách lần lượt như vậy. Tuy nhiên trong C#, chúng ta có một cách viết khác để tự động xây dựng một cấu trúc dữ liệu kiểu IEnumerable, đó là việc sử dụng từ khóa yield return.

Cách tốt nhất để hiểu được từ khóa này là dùng ví dụ:

IEnumerable generateString(){ yield return "one"; yield return "two"; yield return "three";}Hàm generateString() không gọi bất cứ class nào kế thừa từ IEnumerable cả, mà dùng từ khóa yield return. Khi một hàm sử dụng từ khóa này, hàm đó được gọi là “Generator”. Khi hàm này gặp từ khóa “yield return“, nó sẽ ngừng hoạt động và đưa giá trị được yield đó vào chuỗi IEnumerable mà nó trả về. Lúc này tại hàm gọi Generator đó, bạn có thể đọc được ngay giá trị đầu tiên đó. Khi bạn muốn đọc giá trị tiếp theo(bằng cách dùng foreach hoặc gọi MoveNext()), Generator sẽ tiếp tục chạy từ vị trí yield return cũ cho đến từ khóa yield return tiếp theo. Các lần gọi giá trị sau cũng diễn ra y hệt như vậy. Đến khi hàm này không gặp từ khóa yield return nào nữa(hoặc khi nó gặp từ khóa yield break) thì chuỗi IEnumerable trả về sẽ được hoàn tất (đây là lúc mà MoveNext() trả về false)

Để dễ hiểu hơn, hãy đọc ví dụ sau đây.

Xem thêm: Game Bắn Xe Tăng - Mua Trò Chơi Xe Tăng Zoo War: Bắn Súng 3V3

private static IEnumerable generateStrings(){ Console.WriteLine("yield one"); yield return "one"; Console.WriteLine("yield two"); yield return "two"; Console.WriteLine("yield three"); yield return "three";}//codevar seq = generateStrings();foreach(string s in seq){ Console.WriteLine($"loop {i++}"); Console.WriteLine(s);}//output//yield one//loop 1//one//yield two//loop 2//two//yield three//loop 3//threeBạn có thể thấy là hàm generateStrings() chỉ tiếp tục chạy khi vòng lặp foreach chuyển sang giá trị tiếp theo của chuỗi IEnumerable. Đây là điểm làm nên điểm hay nhất của IEnumerable: lazy evaluation, tức là chỉ thực hiện tính toán và trả về dữ liệu khi được gọi. Ứng dụng của tính năng này là rất lớn: Hãy tưởng tượng bạn đang làm việc với một nguồn dữ liệu khổng lồ, được gọi từ một nguồn dữ liệu nào đó (database, network…). Bạn không muốn gọi tất cả các dữ liệu đó cùng lúc, vì nó quá lớn và quá mất thời gian, mà thay vào đó là gọi chúng một cách lần lượt. Khi bạn không muốn tiếp tục gọi dữ liệu nữa, bạn có thể ngừng Generator này lại bằng các gọi yield break ở bên trong hàm generator, hoặc gọi Dispose() ở IEnumerator của IEnumerable mà bạn nhận được. Kiểu như sau:

IEnumerable getRecords(){ int i=0; while(true) { Record record = ReadRecord(i++); yield return record; }}void Process(){ var records = getRecords(); var enumerator = records.GetEnumerator(); while(enumerator.MoveNext()) { ProcessResult result = ProcessRecord(enumerator.Current); if (result == ProcessResult.Enough) { enumerator.Dispose(); return; } }}Nếu bạn thấy những điều trên là khó hiểu thì cũng không có gì đáng lo ngại cả, vì generator là một khái niệm tương đối khó nắm bắt. Cách tốt nhất để hiểu được nó là hãy mở Visual Studio lên và bỏ thời gian nghịch ngợm một chút.

Hàm Where trong Linq

Giờ thì chắc bạn cũng nắm bắt được tương đối về IEnumerable rồi (nếu như chưa thì lời khuyên của mình là nên ngừng đọc và dành chút thời gian để thử nghiệm). Chúng ta hãy thử dùng điều này để viết lại một hàm đơn giản nhất trong Linq: Where (xem định nghĩa hàm này ở đầu bài)

Hàm Where về cơ bản có chức năng giống như một filter(tương tự như câu “WHERE” trong sql). Nó giúp bạn chọn lọc ra những phần tử trong một tập hợp mà thỏa mãn một yêu cầu nào đó. Chúng ta hãy trở lại ví dụ với hàm generateStrings() đã viết ở trên(đã loại bỏ các đoạn Console.WriteLine()). Giả sử bạn muốn chọn lọc ra những string có độ dài nhỏ hơn 4 (trong trường hợp trên là “one” và “two”). Bạn hoàn toàn có thể viết như sau

public static class MyLinq{ public static IEnumerable Where(this IEnumerable source) { foreach(string s in source) { if(s.LengthĐiều này có nghĩa rằng chỉ những đoạn giá trị string nào có độ dài nhỏ hơn 4 mới được hàm này tiếp nhận và đưa vào giá trị IEnumerable đầu ra.

Xem thêm: Nghị Luận Xã Hội Tình Yêu Thương Là Gì? Biểu Hiện Trong Cuộc Sống Như Thế Nào? ?

Đoạn code trên hoạt động hoàn toàn đúng, ngoài trừ việc nó không có tính tái sử dụng cho lắm: bạn không điều khiển được logic lựa chọn của hàm Where, và hàm Where hiện tại chỉ hoạt động với kiểu string mà thôi. Bạn có nhớ rằng trong phần trước, chúng ta có nói đến việc C# hỗ trợ kiểu dữ liệu Func. Chúng ta hãy viết lại hàm Where để tận dụng điều này:

public static class MyLinq{ public static IEnumerable Where(this IEnumerable source, Func predicate) { foreach(T item in source) { if (predicate(item)) {yield return s;} } }}//codevar source = generateStrings();var seq = source.Where(s=> s.Length Giờ thì hàm Where của chúng ta đã tương đối giống với hàm Where trong Linq rồi. Nó cho phép bạn gọi hàm này với bất kì kiểu dữ liệu nào, và chỉ ra logic lựa chọn tại thời điểm gọi hàm. Trên thực tế, đoạn code này về cơ bản là giống với hàm Where thực sự trong Linq(ngoại trừ một vài logic kiểm tra điều kiện). Tuyệt!

Nếu tinh ý thì bạn cũng sẽ nhận ra rằng hàm Where của chúng ta là một hàm “thuần” (pure function), có nghĩa là nó không làm thay đổi dữ liệu. Điều này đúng cho tất cả các hàm trong Linq (tất nhiên, để nó thực sự “thuần”, thì bản thân logic trong hàm “predicate” cũng không được làm biến đổi gì về dữ liệu). Một điểm thú vị nữa của hàm Where là nó là một hàm “lười” (lazy function), có nghĩa là tại thời điểm bạn gọi Where(), không có bất cứ logic lựa chọn nào được thực hiện. Chỉ khi bạn tìm cách đọc phần tử của chuỗi IEnumerable mà Where() trả về, lúc đó logic “predicate” của bạn mới bắt đầu hoạt động.