Khái niệm IEnumerable thì chắc cũng có kha khá người biết, khi ta muốn duyệt tất cả các phần tử trong 1 danh sách, ta thường dùng hàm foreach như sau.

foreach(Student student in students) {}

Các kiểu Collection trong C# như List, ArrayList, Dictionary v…v đều implement interface IEnumerable, do đó ta có thể sử dụng foreach để duyệt.

Khái niệm Yield lại được ít người biết tới hơn (Mình thấy rất ít người biết yield là gì, chưa nói đến việc sử dụng). Yield là một keyword thường hay được dùng với IENumerable. Sử dụng yield sẽ làm code của bạn ngắn gọn, hiệu suất cao hơn rất nhiều. Bài viết này sẽ giải thích cũng như hướng dẫn cách áp dụng từ khóa yield.

1. Nhắc lại về IEnumerable

1 mảng IEnumerable có những thuộc tính sau:

  • Là một mảng read-only, chỉ có thể đọc, không thể thêm hay bớt phần tử.
  • Chỉ duyệt theo một chiều, từ đầu tới cuối mảng.

Hãy xét trường hợp sau, nếu ta muốn đọc 1 danh sách học sinh từ file, ta thường viết

public List<Student> ReadStudentsFromFile(string fileName)
{
  string[] lines = File.ReadAllLines(fileName);
  //Tạo một list trống
  List<Student> result = new List<Student>(); 

  foreach (var line in lines)
  {
    Student student = ParseTextToStudent(line);
    result.Add(student); //Thêm student vào list
  }
  return result; // Trả list ra
}

var students = ReadStudentsFromFile("students.txt");
foreach(var student in students) {};

Đoạn code này không có gì sai. Tuy nhiên ta thấy việc tạo list, thêm phần tử vào list, trả list ra có thể được rút gọn với từ khóa yield như sau

//Đổi kiểu trả về là IEnumerable
public IEnumerable<Student> ReadStudentsFromFile(string fileName)
 {
   string[] lines = File.ReadAllLines(fileName);
   foreach (var line in lines)
   {
      Student student = ParseTextToStudent(line);
      yield return student; //YIELD NÈ
   }
 }

//Dùng như cũ
var students = ReadStudentsFromFile("students.txt");
foreach(var student in students) {};

Bạn sẽ thắc mắc: Ừ, thì rút gọn được 2 dòng code, nhưng mà code có vẻ khó hiểu hơn. Ngày xưa minh cũng nghĩ thế. Ở phần sau, mình sẽ giải thích cơ chế hoạt động của yield, cũng như lý do chúng ta nên dùng yield trong code.

why1

2. Phân biệt return và yield return

Chúng ta đều biết điều cơ bản nhất khi viết 1 method: Từ khóa return sẽ kết thúc method, trả ra kết quả, ko chạy thêm bất kì câu lệnh gì phía sau:

public int GetNumber() { return 5; }
Console.WriteLine(GetNumber());

Thế trong trường hợp này, khi chúng ta yield 3 lần thì sao?

public IEnumerable<int> GetNumber()
 {
   yield return 5;
   yield return 10;
   yield return 15;
 }
foreach (int i in GetNumber()) Console.WriteLine(i);  //5 10 15

Sao lạ vậy ta, tại sao ta lấy được cả 3 kết quả? Ta có thể hiểu luồng chạy của chương trình như sau:

  1. Khi gọi method GetNumber, lấy phần từ đầu tiên, chương trình chạy tới dòng lệnh số 3, lấy ra kết quả là 5, in ra console.
  2. Duyệt tiếp phần từ tiếp theo, chương trình chạy vào dòng lệnh số 4, lấy kết quả 10, in ra màn hình.
  3. Tương tự với phần tử cuối cùng, sau khi in ra, chương trình kết thúc.

Ta hãy quay lại so sánh 2 method đã viết ở đầu chương trình:

public List<Student> ReadStudentsFromFile(string fileName)
{
  string[] lines = File.ReadAllLines(fileName);
  List<Student> result = new List<Student>(); //Tạo một list trống

  foreach (var line in lines)
  {
    Student student = ParseTextToStudent(line);
    result.Add(student); //Thêm student vào list
  }
  return result; // Trả list ra
}

public IEnumerable<Student> YieldReadStudentsFromFile(string fileName)
{
 string[] lines = File.ReadAllLines(fileName);
 foreach (var line in lines)
 {
   Student student = ParseTextToStudent(line);
   yield return student;
 }
}
  • Ở method đầu, ta trả về kết quả sau khi đã chạy hết hàm for, đưa kết quả vào trong 1 list mới, hàm ReadStudentsFromFile kết thúc.
  • Ở method thứ 2, kết quả được ngay sau khi parse được student đầu tiên, với mỗi vòng lặp tiếp theo, chương trình sẽ chạy tiếp vào method YieldReadStudentsFromFile, lấy kết quả ra dần dần.

Sau khi đã hiểu bản chất, ta có thể ứng dụng yield vào những trường hợp sau:

  • Cần method trả về một danh sách read-only, chỉ đọc, không được thêm bớt xóa sửa.
  • Như trường hợp trên, giả sử ta có 50 dòng, hàm ParseTextToStudent tốn 1s 1 lần. Với cách cũ, khi gọi hàm ReadStudentsFromFile, ta phảo đợi 50s. Với hàm YieldReadStudentsFromFile, hàm ParseTextToStudent chỉ được chạy mỗi khi ta đọc thông tin của học sinh, đó đó tăng performance lên rất nhiều (Nếu ta chỉ lấy 5 học sinh đầu chỉ cần đợi 5s).
  • Trong một số trường hợp, danh sách trả về có vô hạn phần tử, hoăc lấy toàn bộ phần tử rất mất thời gian, ta phải sử dụng yield để giải quyết.

Bài viết vừa rồi chỉ hướng dẫn cho bạn khái niệm yield cơ bản. Khi dùng hàm yield, thật ra C# sẽ compile method đã viết lại thành 1 state machine, implement các method Next, Current, … của IEnumrator. Bạn nào muốn tìm hiểu thêm có thể đọc thêm ở đây: http://coding.abel.nu/2011/12/return-ienumerable-with-yield-return/

Yield là một câu hỏi khá khó nhằn, có thể bạn sẽ bị hỏi khi phỏng vấn vào vị trí Senior Developer nhé. Yield cũng là 1 trong “5 anh em siêu nhân” tạo nên sự bá đạo của LINQ (4 người còn lại là: Extension method, Delegate, Lambda expression, Generic). Nếu bạn thường xuyên theo dõi blog, có lẽ bạn đã viết về “5 anh em” này.

pr-2

Ở bài sau, mình sẽ giới thiệu về LINQ, cũng như lật mặt nạ sự bá đạo nằm sau những method đơn giản như Where, First, … của nó.