Lập trình C#

Từ khóa yield trong C#

Từ khóa yield trong C#
Được viết bởi Minh Hoàng

Series lập trình C#, ngôn ngữ lập trình hiện đại và mạnh mẽ.

Khái niệm IEnumerable thì chắc nhiều bạn biết. Các kiểu collection trong C# như List, ArrayList, Dictionary,… đều implement interface IEnumerable, do đó chúng ta có thể sử dụng foreach để duyệt. Chẳng hạn khi muốn duyệt tất cả các phần tử trong một danh sách, chúng ta sẽ dùng foreach như sau:

foreach(Student student in listStudent) {}

Nội dung chính:

  1. Nhắc lại về IEnumerable
  2. Yield là gì? Và cách sử dụng yield
    • Cách sử dụng yield return
    • Cách sử dụng yield break
    • Phân biệt return và yield return
    • Sử dụng yield khi nào?
#1/2. Nhắc lại về IEnumerable

Một tập hợp IEnumerable có những thuộc tính sau:

  • Là một tập hợp 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 tập hợp.
// Đây là một List collection mà chúng ta sẽ sử dụng một Enumertor để lặp qua
// chứ không phải như bình thường dùng vòng lặp for() để duyệt theo chỉ mục.
List<string> listData = new List<string>();

listData.Add("Minh Hoàng Blog");
listData.Add("www.minhhn.com");
listData.Add("Chia sẻ");
listData.Add("Kiến thức lập trình");
listData.Add("Cuộc sống Nhật bản");

// Ở đây sẽ dùng foreach() để duyệt kiểu liệt kê enumerator
// được lấy từ đối tượng List<string>
foreach (string item in listData)
{
	Console.WriteLine(item);
}

// Hoặc sử dụng for() lặp theo chỉ mục
for (int i = 0; i < listData.Count; i++)
{
	string item = listData[i];

	Console.WriteLine(item);
}

#2/2. Yield là gì? Và cách sử dụng yield

Từ khóa yield báo hiệu cho trình biên dịch rằng phương thức mà nó xuất hiện là một khối lặp (iterator block). Trình biên dịch tạo ra một lớp để implement hành vi được thể hiện trong khối lặp. Trong khối lặp, từ khóa yield được sử dụng cùng với từ khóa return để cung cấp giá trị cho đối tượng liệt kê (enumerator object). Đây là giá trị được trả về. Từ khóa yield cũng được sử dụng break để báo hiệu kết thúc lặp (iteration).

Phương thức có sử dụng từ khóa yield bắt buộc phải trả về kiểu dữ liệu là IEnumerable.

Cách sử dụng yield return

– Có thể hiểu đơn giản là yield sẽ kết hợp với từ khóa return cho phép trả về các giá trị từ trong vòng lặp, và sau đó nó có thể trở lại vòng lặp và tiếp tục cho phần tử tiếp theo.

– Chúng ta cùng xét ví dụ sau: lấy ra tất cả index của số “7” được tìm thấy trong collection “listData“:

using System;
using System.Collections.Generic;

namespace MinhHoangBlog
{
	class Program
	{
		static void Main(string[] args)
		{
			// Tạo đối tượng của collection List<string>
			List<int> listData = new List<int>() { 6, 7, 3, 3, 1, 7, 4, 3, 7, 2, 8, 5, 7, 4 };

			// In ra màn hình tất cả vị trí xuất hiện của số 7
			foreach (var index in GetListIdx1(listData, 7))
				Console.Write(index + " ");

			foreach (var index in GetListIdx2(listData, 7))
				Console.Write(index + " ");

			// Output: 1 5 8 12
		}

		// Dùng cách thông thường
		private static List<int> GetListIdx1(List<int> listData, int valueFind)
		{
			List<int> listIdx = new List<int>();
			for (int ii = 0; ii < listData.Count; ii++)
			{
				if (listData[ii] == valueFind)
					listIdx.Add(ii);
			}
			return listIdx;
		}

		// Dùng từ khóa yield
		private static IEnumerable<int> GetListIdx2(List<int> listData, int valueFind)
		{
			for (int ii = 0; ii < listData.Count; ii++)
			{
				if (listData[ii] == valueFind)
					yield return ii;
			}
		}
	}
}

– Với 2 cách viết theo cách thông thường hay sử dụng từ khóa yield đều cho một kết quả giống nhau là 1 5 8 12, tuy nhiên có sự khác nhau đó là:

  • Thứ nhất, số lượng dòng code khác nhau rõ rệt.
  • Thứ hai, hiệu suất của hàm có sử dụng yield được cải thiện. Vì trong quá trình runtime, chương trình sẽ nhảy qua lại giữa 2 phương thức “Main” và “GetListIdx2” để lấy giá trị, mà không phải tốn bộ nhớ để tạo một biến “listIdx” để lưu trữ như làm theo cách thông thường. Do đó, đối với dữ liệu lớn thì điều này là một sự cải thiện hiệu năng đáng kể.

(*) Cách thức hoạt động của yield là:

  • Mỗi khi tìm thấy giá trị số “7” trong mảng “listData” thì nó sẽ return giá trị đó, nhưng không thoát khỏi methodGetListIdx2” ngay lập tức, mà sẽ đánh dấu lại vị trí lặp đó.
  • Sau khi return giá trị ra, thì nó sẽ tiếp tục quay lại vào bên trong vòng lặp, tiếp tục lặp đến vị trí tiếp theo từ vị trí đã đánh dấu trước đó để tiếp tục tìm kiếm,…
Cách sử dụng yield break

– Nếu muốn kết thúc ở một trạng thái nào đó, ta có thể sử dụng từ khóa yield break. Ví dụ: khi lặp đến vị trí “độ dài – 6” thì break vòng lặp, dừng lại, không tìm kiếm nữa. Khi đó kết quả chỉ là 1 5

// Dùng từ khóa yield
private static IEnumerable<int> GetListIdx2(List<int> listData, int valueFind)
{
	for (int ii = 0; ii < listData.Count; ii++)
	{
		if (ii == listData.Count - 6)
			yield break;

		if (listData[ii] == valueFind)
			yield return ii;
	}
	Console.WriteLine("Done");	// Dòng này để so sánh giữa "yield break" và "break"
}

・Trong method trên mình đã đưa vào lệnh in ra chữ “Done” khi kết thúc nhưng chữ này không được in rayield break ở đây sẽ đóng vai trò như lệnh return, tức ngừng luôn hàm tại vị trí yield break.

・Còn nếu dùng break thay cho yield break thì kết quả sẽ có chữ “Done“, vì break sẽ chỉ kết thúc vòng lặp và tiếp tục chạy các dòng code còn lại của hàm.

Phân biệt return và yield return

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

public List<int> GetNumber()
{
	var list = new List<int>();
	list.Add(5);
	return list;

	list.Add(10);
	return list;

	list.Add(15);
	return list;
}
foreach (int i in GetNumber())
	Console.WriteLine(i);	// Output: 5

– Thế trong trường hợp này, khi chúng ta yield return 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);	// Output: 5 10 15

Với yield return, ta thấy có 3 giá trị được output!? Nguyên nhân là ở cách hoạt động của yield return như đã trình bày ở (*) :

  • 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.
  • Rồi vào vòng lặp, duyệt tiếp phần tử tiếp theo ở dòng lệnh số 4, lấy kết quả 10, in ra màn hình.
  • Tương tự như thế, chương trình lấy kết quả 15, in ra màn hình.
Sử dụng yield khi nào?

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

  • Khi 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.
  • 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, chúng ta phải sử dụng yield giải quyết để tăng performance cho chương trình.
Cảm ơn bạn đã theo dõi. Đừng ngần ngại hãy cùng thảo luận với chúng tôi!

Giới thiệu

Minh Hoàng

Xin chào, tôi là Hoàng Ngọc Minh, hiện đang làm BrSE, tại công ty Toyota, Nhật Bản. Những gì tôi viết trên blog này là những trải nghiệm thực tế tôi đã đúc rút ra được trong cuộc sống, quá trình học tập và làm việc. Các bài viết được biên tập một cách chi tiết, linh hoạt để giúp bạn đọc có thể tiếp cận một cách dễ dàng nhất. Hi vọng nó sẽ có ích hoặc mang lại một góc nhìn khác cho bạn[...]

Translate »