Thứ Ba, 24 tháng 12, 2024

IEnumerable (Generator)

Chào mọi người, hôm nay chúng ta sẽ xem xét về IEnumerable và trước tiên chúng ta sẽ có một chút giải thích về cách nó hoạt động, sau đó chúng ta sẽ đi sâu một chút vì nó không quá phức tạp, chủ đề cũng không quá lớn. Tuy nhiên, hãy bắt đầu ngay thôi.


Vậy một cách đơn giản để tạo một IEnumerable là bạn sẽ khai báo một public enumerable và hãy chọn một số. Vậy chúng ta muốn IEnumerable sinh ra các số và điều đặc biệt về IEnumerable trong các ngôn ngữ khác nó có thể được gọi là một generator (trình tạo). Vậy bạn sử dụng nó để tạo ra các thứ và ở đây chúng ta sẽ chỉ gọi nó là get, điều này không cần tên phức tạp nào, chúng ta chỉ gọi nó là get và điều chúng ta muốn làm là hiểu rõ những gì thực sự xảy ra khi bạn sử dụng một IEnumerable. Vậy tất cả những gì chúng ta sẽ làm là chúng ta sẽ yield một giá trị và bạn sẽ phải sử dụng yield return, rồi sau đó chúng ta sẽ yield return tiếp. Vậy tại sao dùng yield return mà không chỉ dùng return? Một giải thích ngắn gọn bằng lời là khi bạn chỉ dùng return, bạn sẽ thoát khỏi hàm ngay tại đó, còn nếu bạn dùng yield return, bạn như thể ném ra một giá trị nhưng sau đó bạn vẫn tiếp tục thực thi hàm và chúng ta sẽ thấy điều này xảy ra như thế nào trong giây lát nữa.


Điều tôi sẽ làm là tôi cũng sẽ đặt các bước vào đây để chúng ta có thể thấy thứ tự thực thi của chúng như thế nào. Vậy hãy tiến hành dump giá trị một, dump giá trị hai ở đây và giá trị ba ở cuối. Vậy hãy nhanh chóng thay đổi những cái này. Này, nếu tôi gọi get, thì tôi sẽ nhận được enumerable của mình. Ở thời điểm này, nếu tôi hover qua đây, đây là một IEnumerable. Tôi có thể chọn các phần từ ở đây. Bây giờ điều tôi thực sự muốn làm là tôi chỉ muốn dump nó và điều này sẽ thực hiện việc liệt kê danh sách, vậy nếu tôi chạy nó, tôi có 1, 2, 3 và sau đó tôi cũng có một danh sách. Này, vậy điều gì sẽ xảy ra nếu tôi chạy nó lại trong khi tôi thực thi danh sách một lần nữa, giống như tôi đang thực thi một hàm rồi sau đó thực thi lại. Vậy hãy tiến hành làm như sau vì thường thì khi chúng ta thực thi một hàm, chúng ta nhận được một giá trị mới mỗi lần, vậy hãy sao chép nó và lưu nó vào một biến, chúng ta sẽ gọi nó là e cho enumerable. Chúng ta sẽ lưu nó và sau đó gọi get, chúng ta sẽ dump nó và chúng ta sẽ thay thế get này bằng e. Được rồi, vậy bây giờ chúng ta cơ bản đang lấy cùng một giá trị, đúng không? Chúng ta chạy nó và vẫn nhận được cùng một kết quả. Đây chính là toàn bộ mục đích của một generator, khi bạn kích hoạt generator, nó sẽ phát ra một giá trị một cách tuần tự. Và mỗi lần bạn gọi một generator, bạn đang kích hoạt việc tạo ra các giá trị.


Vì vậy, để hình dung rõ hơn, hãy đưa vào một ví dụ thực tế hơn. Giả sử chúng ta có IEnumerable này và cho mỗi phần tử là 1 và E, hãy thực sự đổi tên e thành IEnumerable để dễ hiểu hơn một chút và E đầu tiên sẽ là e và tôi sẽ thực hiện một vòng foreach và điều chúng ta sẽ làm là chỉ dump E như là giá trị. Được rồi, và điều tôi sẽ làm trước đây là thực sự chạy cái này trước. Được rồi, vậy chúng ta có thể thấy điều xảy ra là chúng ta thực thi giá trị một trước, chúng ta đưa ra giá trị trả về và sau đó chúng ta vào vòng for loop. Được rồi, và chúng ta dump nó, sau đó chúng ta yêu cầu enumerable giá trị tiếp theo và nó cơ bản nói rằng hãy tiếp tục từ đây vào phần tiếp theo, vậy nó cũng phải thực thi và nó chuyển sang hai, nó trả về hai và sau đó chúng ta dump hai và rồi nó lại đi tiếp, hỏi bạn có giá trị tiếp theo không và sau đó nó cơ bản phải nói, tôi không nhận được gì và sau đó nó sẽ thoát. Được rồi, và bạn sẽ thấy thêm một chút về cách hoạt động của nó khi chúng ta thực hiện một triển khai tùy chỉnh.


Nhưng về cơ bản, những gì bạn có thể làm và đây là một lỗi rất thực tế mà bạn có thể mắc phải là bạn có thể có một IEnumerable, hãy lấy giá trị này vì nó không thể đánh vần và những gì bạn có thể làm là bạn có thể lấy count, đúng rồi, và những gì có thể xảy ra là bạn có thể sử dụng count và sau đó bạn sẽ tái sử dụng IEnumerable, được rồi, và chúng ta sẽ dump count ở đây, được rồi, vậy điều sẽ xảy ra là khi chúng ta kích hoạt count để tìm biết có bao nhiêu phần tử trong IEnumerable này, chúng ta sẽ cần phải liệt kê IEnumerable. Được rồi, hành động liệt kê cơ bản là bạn kích hoạt generator, đúng không? Bạn sẽ bắt đầu generator, nó sẽ tạo ra các giá trị và sau đó nó sẽ dừng lại. Lần tiếp theo bạn cần một giá trị từ nó, bạn sẽ kích hoạt lại và nó sẽ tạo ra cùng một giá trị. Vậy cách bạn sử dụng generator này là bạn không lấy tất cả các giá trị cùng một lúc, bạn làm là nó phát ra một giá trị bạn xử lý nó và sau đó nó phát ra giá trị tiếp theo. Điều này nhằm giúp bạn làm việc với bộ nhớ hiệu quả hơn vì bạn không phân bổ hàng ngàn mục, bạn đang xử lý một mục tại một thời điểm, vì vậy mối quan tâm duy nhất của bạn là bạn không liệt kê danh sách hơn một lần. Được rồi, vì vậy chúng ta chạy mã ở đây, những gì bạn sẽ thấy là chúng ta đã liệt kê lần đầu tiên để xuất count và sau đó chúng ta liệt kê lần thứ hai nhưng lại sử dụng giá trị một cách tuần tự.


Những gì bạn có thể làm là thay vì sử dụng enumerable, điều tôi sẽ làm là tôi sẽ bỏ chú thích dòng này và tôi sẽ đặt một enumerable được xây dựng bằng số ở đây và chúng ta sẽ gọi từ a đến nhỏ hơn này và thay vì enumerable tôi sẽ nói rằng đây là một list bây giờ và chúng ta không cần gọi hàm count trên list, chúng ta có thể chỉ gọi count và những gì sẽ xảy ra bây giờ là bạn sẽ thấy khi chúng ta gọi hai danh sách, chúng ta liệt kê ngay tại đó và sau đó chúng ta chuyển danh sách đó, danh sách đã liệt kê, thành một list và sau đó chúng ta cơ bản có thể kiểm tra count, chúng ta có thể lặp qua nó. Vì vậy, nếu bạn phải thực hiện nhiều hơn một thao tác trên các giá trị được tạo ra bởi IEnumerable, bạn muốn chuyển chúng thành list nhưng hãy cẩn thận rằng bạn đang phân bổ bộ nhớ, nhưng đây thực sự không phải là vấn đề trong thời đại ngày nay, chúng ta có rất nhiều bộ nhớ dư thừa, nhưng hãy nhớ rằng nếu bạn làm việc với gigabytes bộ nhớ, bạn có thể muốn xem xét điều này.


Hãy tiến hành và lấy một ví dụ cuối cùng về IEnumerable. Tôi sẽ bỏ chú thích tất cả những thứ này, tôi sẽ lấy IEnumerable của mình và đi xuống đây và những gì tôi sẽ làm là tôi chỉ muốn một lần nữa hình dung mental về các mục đi qua từng cái một, đặc biệt khi bạn đang sử dụng liên kết. Được rồi, vậy chúng ta sẽ kiểm tra xem số đó, vậy số là nhỏ hơn 10 không và vì chúng ta chỉ có hai số, đó là tất cả những gì sẽ xảy ra. Được rồi, nhưng rồi tôi sẽ làm gì là mỗi khi tôi tiêu thụ số này, tôi cũng sẽ dump nó và tôi sẽ nói rằng chúng tôi ổn. Vậy điều này sẽ xảy ra trong mệnh đề where và chúng ta sẽ có một giá trị số và ở trên nó sẽ nói where và những gì tôi sẽ làm tiếp theo là tôi chỉ chạy select và tôi chỉ chọn chính số đó. Được rồi, vậy tôi chỉ trả về số đó, tôi lấy số đó như là số, vì vậy đây là một câu lệnh select vô nghĩa trừ việc tôi muốn giải thích những gì đã xảy ra, những gì thực sự đang xảy ra, đúng không? Vậy nên chúng ta sẽ dump và chúng ta sẽ chọn ở đây và những gì tôi sẽ làm ở cuối là bởi vì nếu tôi chạy nó, không có lý do thực sự để biết về các giá trị, những gì chúng ta đã tạo ở đây là một IEnumerable, chúng sẽ cũng trả về các số, vì vậy chúng ta cơ bản đã lấy generator hoặc một IEnumerable và C# vốn về cơ bản là một generator và IEnumerable là cùng một thứ và chúng ta đã tiến hành mở rộng một lần nhưng cơ bản là lấy generator và chúng ta đã nói rằng bây giờ bạn cũng làm điều này và sau đó chúng ta đặt câu lệnh select lên nó và nói rằng bạn cũng làm điều này nữa, vậy nên những gì chúng ta thực sự cần làm là chúng ta cần phải liệt kê nó, kích hoạt nó. Chúng ta cơ bản cần phải yêu cầu một giá trị trở lại, đúng không? Khi chúng ta yêu cầu một giá trị, đó là khi chúng ta bắt đầu liệt kê generator hoặc kích hoạt generator và đây là lý do tại sao khi bạn gọi ToList hoặc FirstOrDefault hoặc Count, bạn cần biết một số thứ về các mục trong generator và đó là khi liệt kê kích hoạt. Vậy hãy tiến hành và chỉ kích hoạt count ở đây để chúng ta kích hoạt việc liệt kê và những gì tôi muốn thấy là luồng của mục. Được rồi, vậy số một là một, xuất nó ở đây, một khi chúng ta trả về một từ phương thức get hoặc generator, nó sẽ đi đến mệnh đề where trước và sau đó nó sẽ đi đến mệnh đề Select và đó là tất cả rồi, và sau đó nó cơ bản trả về generator và yêu cầu giá trị tiếp theo và sau đó nó lại đi đến where và sau đó đi đến Select và rồi nó hỏi bạn có giá trị tiếp theo không và sau đó cơ bản nói output ba, không còn gì nữa. Được rồi, và chúng ta cơ bản chỉ nhận ra rằng một generator hoặc IEnumerable phát ra một giá trị một cách tuần tự. Vậy đó là đủ cho một cái nhìn tổng quan nhanh về IEnumerable, hãy tiếp tục và thực hiện một cuộc đi sâu vào một triển khai đơn giản tùy chỉnh của đối tượng IEnumerable mà chức năng này thực sự tạo ra, chúng ta sẽ xem xét kỹ hơn sau. Vậy những gì chúng ta muốn đạt được là việc tạo ra các giá trị và cơ bản là có một hàm thông qua đó chúng ta đưa giá trị đầu tiên trước khi lấy giá trị tiếp theo. Vậy tất cả những gì chúng ta thực sự có là một vòng lặp với một máy trạng thái từ đó chúng ta lấy giá trị tùy thuộc vào nơi chúng ta đang ở. Được rồi, đó là tất cả những gì generator là. Vậy hãy tiếp tục và tạo một public cùng với generator đơn giản của tôi, generator gen4 đúng rồi và chúng ta sẽ có một public khác, hãy chỉ có một giá trị integer mà chúng ta sẽ cố gắng công khai và chúng ta sẽ chỉ nói và chúng ta lấy giá trị tiếp theo. Được rồi, vậy public và giá trị sẽ tuân theo cùng một điều kiện, nơi điều kiện của chúng ta thực sự là số của chúng ta dưới hai và chúng ta bắt đầu từ một. Vậy hãy tiếp tục làm điều đó, chúng ta sẽ bắt đầu từ một và chúng ta sẽ nói nếu giá trị lớn hơn hai và sau đó chúng ta không còn giá trị nữa, nếu không, chúng ta muốn trả về true. Được rồi, và sau đó chúng ta muốn có thể lấy giá trị này, vậy hãy tiến hành và nói một public và get value hoặc có lẽ nên làm nó là big volume và trả về _value. Được rồi, vậy bây giờ điều gì có thể xảy ra? Bây giờ chúng ta có thể có một vòng while nơi chúng ta có thể cơ bản sử dụng generator của mình, hoặc hãy gọi nó là simpleGen = new MySimpleGen và chúng ta sẽ sử dụng simpleGen để kiểm tra nếu có một giá trị, đúng rồi, bạn có thể có một giá trị, nếu vậy, thực thi. Được rồi, hãy tiến hành lấy giá trị từ simpleGen.Value và hãy tiến hành dump volume, đúng rồi, chúng ta sẽ dump volume và bạn sẽ nhận thấy ở đây nó là một vòng lặp vô hạn. Được rồi, 


Để tôi tạm dừng việc đó và mỗi khi chúng ta lấy giá trị, chúng ta sẽ tăng nó lên, được rồi. Vậy chúng ta sẽ thực sự chạy vào câu lệnh if này, được rồi, vậy chúng ta nhận được giá trị một và hai. Có lẽ để có một cái nhìn rõ ràng hơn về điều này, tôi cũng sẽ dump những số này. Vậy mỗi khi chúng ta cố gắng kiểm tra xem có giá trị hay không, những gì tôi sẽ làm là tôi sẽ xuất giá trị này ở đây. Hãy tiến hành chạy chương trình này để bạn có thể thấy rằng kết quả gần như giống hệt nhau: chúng ta có một generator, chúng ta thực hiện một vòng lặp qua nó và có một số thứ mà chúng ta làm bên trong nó. Đúng vậy, chỉ là sự khác biệt trong cách chúng ta viết mã: liệu chúng ta muốn sử dụng yield return để trả về cùng một kiểu từ một hàm và sau đó nó sẽ tạo ra lớp này cho chúng ta, hay chúng ta muốn viết một đối tượng mới mỗi lần chúng ta cần một generator. Được rồi, và còn có cả sự chồng lấn liên kết mà bạn có thể gặp phải dựa trên các generator, điều này khá hay. Vậy đây là lý do tại sao các generator tồn tại dưới dạng này, chúng là một cấu trúc rất phổ biến mà chúng ta sử dụng trong lập trình.


Được rồi, chúng ta cũng có thể xem xét hàm get này. Một trong những người xem của tôi đã gợi ý cho tôi rằng các tuỳ chọn được kết hợp để sử dụng IL Inspector, vì vậy tôi sẽ sử dụng nó để xem trình biên dịch đang làm gì với IEnumerable. Vậy đây là hàm get của tôi và về cơ bản, nếu chúng ta xem xét nó, chúng ta có thể thấy rằng nó đang kế thừa từ IEnumerable<int>. IEnumerable này chỉ là một IEnumerable, IEnumerator và Enumerator. Vậy nên Enumerator cũng là generic. Điều đang xảy ra ở đây là chúng ta có một giá trị hiện tại (current value), về cơ bản là những gì chúng ta đã triển khai dưới dạng một giá trị, và trong đây chúng ta cũng có current, là giá trị hiện tại đối với chúng ta, nó sẽ là trường _value. Chúng ta sau đó cũng có hàm MoveNext nổi bật, đây là cách mà chúng ta có hàm HasValue hoặc xin lỗi, hàm HasValue để kiểm tra xem chúng ta có giá trị tiếp theo hay không. Và về cơ bản, điểm chính ở đây là bạn có thể đi qua những hàm này, nó sẽ không nói nhiều với bạn ngoài việc cơ bản là hàm mà bạn viết ở đây sẽ trả về một đối tượng. Được rồi, và bạn thực sự có thể triển khai những loại đối tượng này một cách thủ công.


Vậy nếu chúng ta quay lại phần đi sâu nhất, chúng ta sẽ bỏ chú thích generator đơn giản của mình và dưới đây, thực tế, hãy quay lại đây, hãy lấy một chút không gian và những gì chúng ta sẽ làm là chúng ta sẽ tạo một vòng lặp foreach, chúng ta sẽ tạo một biến value trong generator thực tế, đúng rồi. Vậy nên chúng ta sẽ tiến hành tạo một generator thực tế và bạn, generator thực tế của tôi, đúng không? Nó sẽ không tạo ra điều này cho tôi à? Đúng rồi, hãy tiến hành tạo lớp này: public class MyActualGen. Tôi sẽ kéo nó xuống dưới generator đơn giản của mình để dễ nhìn thấy tất cả mọi thứ này một chút hơn. Và vâng, tôi muốn loại bỏ những dấu ngoặc này, chúng không nên ở đó. Vậy nên điều chính mà chúng ta sẽ nhận được ở đây là trước tiên sẽ có một in ở đó và câu lệnh này sẽ nói rằng câu lệnh foreach không thể hoạt động trên các biến kiểu MyActualGen vì MyActualGen không chứa định nghĩa instance công khai cho IEnumerator. Về cơ bản, tất cả những gì nó nói là chúng ta cần một hàm được đặt tên là GetEnumerator, nhưng hãy tiến hành và giả lập điều đó, cuối cùng chúng ta sẽ đi đến lớp thực tế mà chúng ta đã thấy trong IL Inspector. Vậy nên public IEnumerator GetEnumerator(), tôi nghĩ đó là gì nó đang yêu cầu. Đúng rồi, và void không chứa định nghĩa cho Current là lỗi tiếp theo của chúng ta. Và về cơ bản, điều này sẽ dẫn đến việc bạn cần các lớp cụ thể với các trường cụ thể để làm cho cú pháp này hoạt động. Vậy hãy tiến hành và kế thừa nó từ cùng những thứ mà lớp đó đang kế thừa và chúng ta sẽ dần xây dựng lớp generator này. Vậy hãy tiến hành kế thừa từ IEnumerable, sự kiện sẽ tạo ra một vài hàm. Vì chúng ta có một Enumerator ở đây, Enumerator này đã được tạo ra một cách hơi kỳ lạ, nên hãy tiến hành loại bỏ phần này và ở đây chúng ta sẽ loại bỏ GetEnumerator(), nhưng ở phần đầu tiên chúng ta sẽ phải khai báo nó là public. Được rồi, đối tượng này sẽ triển khai giao diện IEnumerable và bây giờ chúng ta muốn trả về một giao diện IEnumerator. Được rồi, và trong triển khai thực tế mà được tạo ra từ hàm IEnumerable này, nó triển khai cả hai giao diện này, vì vậy hãy tiến hành để chúng ta có thể trả về điều này từ đây. Được rồi, vậy hãy tiến hành và triển khai nó nữa. Oh, một Enumerator triển khai giao diện, ẩn các kết quả để dễ nhìn thấy hơn một chút. Chúng ta sẽ có Current, ý tôi là chúng ta có thể lấy một vài giá trị từ generator đơn giản của mình và bắt đầu điền chúng vào đây. Vậy chúng ta sẽ lấy giá trị, chúng ta sẽ dán nó ở đây, lấy tên thực tế của giá trị. Tôi sẽ đặt nó ở đây hoặc Current, điều này về cơ bản là khẳng định lại điều này. Tôi không biết chính xác lý do tại sao nó làm như vậy, nếu ai biết thì hãy để lại bình luận hoặc Dispose, chúng ta không làm gì đáng để dispose như một luồng hoặc cái gì đó như vậy. Vậy hãy nhìn vào Empty Next, là hàm GetEnumerator() nơi chúng ta trả về this, điều này cũng ổn. Sau đó chúng ta có hàm MoveNext, vậy những gì tôi sẽ làm là tôi sẽ sao chép những gì chúng ta đã làm trong hàm HasValue và để tránh gặp lại lỗi tương tự mà chúng ta đã gặp trước đây với vòng lặp vô hạn nơi tôi thấy bạn thấy tôi đang tăng giá trị này, tôi sẽ làm điều tương tự cho Parent ở đây. Được rồi, và nếu vì lý do nào đó chúng ta lấy giá trị cho Current, giá trị sẽ được tăng lên. Được rồi, vậy nên đi xuống tiếp theo là hàm Reset, chúng ta muốn làm gì? Chúng ta muốn đặt lại trong quá trình liệt kê qua MyActualGen của chúng ta. Vậy chúng ta phải đặt giá trị về 0 hoặc về 1 một lần nữa, về cơ bản là trạng thái ban đầu, đúng không? Vậy hãy chỉ đặt nó về 1. Không, không khó để triển khai điều này, nhưng chúng ta có một trường hợp rất đơn giản và GetEnumerator(), nó giống như kịch bản mà chúng ta có với Card. Một lần nữa, tôi không hoàn toàn chắc chắn tại sao nó lại ở đây, nhưng dù sao đi nữa, tất cả những gì bạn có thể làm là cơ bản là trả về IEnumerator và chỉ trả về this nữa. Được rồi, và đó là tất cả cho việc triển khai Enumerator thực tế. Tất cả những gì điều này cho phép bạn làm là về cơ bản nó là một vòng lặp qua vòng for này và so sánh nó với vòng lặp của chúng ta. Về cơ bản, sự khác biệt duy nhất là một dòng mà chúng ta phải cơ bản nói rằng chúng ta lấy giá trị ở đây một cách tự động, ở đây chúng ta phải trích xuất giá trị một cách thủ công cho chính mình. Nhưng về cơ bản, hai triển khai này hoàn toàn giống nhau. Vậy hãy tiến hành chạy chương trình này và lo và behold, tôi không biết dấu ngoặc đó đến từ đâu nhưng không, không chắc chính xác nó có nghĩa là gì. Hãy nhìn vào vẻ huy hoàng của nó, kết quả hoàn toàn giống nhau, đúng không? Và vâng, đó cũng là generator, một cách giải thích ngắn gọn và một cuộc đi sâu vào chi tiết. Tất cả mọi thứ nếu bạn thích tập này, hãy để lại một like, đăng ký kênh. Nếu bạn có bất kỳ câu hỏi nào, hãy chắc chắn để lại chúng trong phần bình luận. Tôi cũng phát trực tiếp vào các ngày thứ Tư và Chủ nhật bắt đầu khoảng sáu, bảy giờ theo giờ London, vì vậy hãy chắc chắn theo dõi để tham gia. Tôi cũng thông báo trên Discord, vì vậy hãy chắc chắn tham gia. Hy vọng tôi sẽ gặp lại bạn trong các tập tiếp theo.


---

Không có nhận xét nào:

Đăng nhận xét