Translate

Thứ Hai, 12 tháng 8, 2013

C# - Cơ chế yielding và ‘yield’ keyword trong Iteration

“yield” – Một từ khóa có vẻ lạ lẫm với bạn nhưng đã được ứng dụng từ lâu trong lĩnh vực lập trình và trong khá nhiều ngôn ngữ. Từ khóa này gắn liền với các kĩ thuật như Generator, Coroutine và các đối tượng iterator. Ta sẽ khám phá cơ chế và cách sử dụng của từ khóa này như thế nào.

Giới thiệu

Theo MSDN:
The yield keyword signals to the compiler that the method in which it appears is an iterator block. The compiler generates a class to implement the behavior that is expressed in the iterator block. In the iterator block, the yield keyword is used together with the return keyword to provide a value to the enumerator object. This is the value that is returned, for example, in each loop of a foreach statement. The yield keyword is also used with break to signal the end of iteration.
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.
Điều này khá kì lạ nhưng nếu bạn đã đọc các link từ Wikimedia trong lời mở đầu thì bạn sẽ hiểu được một cách rõ ràng. Điều mà tôi muốn nhắc tới ở đây là “yield return” sẽ không làm cho phương thức chứa nó kết thúc mà vẫn tiếp tục chạy cho đến khi kết thúc vòng lặp và thực thi xong câu lệnh cuối cùng. Muốn kết thúc phương thức bạn phải dùng một cặp từ khóa khác là “yield break”.
Quy định
-       Không đặt “yield” trong các khối unsafe.
-       Các tham số của phương thức, toán tử, accessor (getter / setter) không được dùng các từ khóa ref hoặcout.
-       “yield return” chỉ có thể được đặt trong khối try nếu như nó được theo sau bởi khối finally.
-       “yield break” có thể đặt trong các khối try và catch nhưng không được đặt trong khối finally.
-       Không dùng “yield” trong anonymous method.

Cách dùng

Để sử dụng “yield return”, bạn chỉ cần tạo một phương thức với kiểu trả về là một IEnumerable (mảng và collection trong .Net đều implement interface IEnumerable)  với vòng lặp và dùng “yield return” để trả về các giá trị cần thiết trong thân vòng lặp.
Ví dụ thay vì viết một phương thức Foo1() để lấy một mảng int từ 0 đến một số nào đó như dưới đây:
?
1
2
3
4
5
6
7
8
9
public static IEnumerable Foo1(int number)
{
int[] numbers = new int[number];
for (int i = 0; i < number; i++)
{
numbers[i] = i;
}
return numbers;
}
Bạn có thể thay thế phương thức trên bằng phương thức sau:
?
1
2
3
4
5
6
7
public static IEnumerable Foo2(int number)
{
for (int i = 0; i < number; i++)
{
yield return i;
}
}
Rõ ràng là thay vì trả về nguyên một đối tượng IEnumerable, ta sẽ trả về từng phần tử riêng lẻ và khi kết thúc phương thức, bạn không cần phải return thêm bất kì đối tượng nào.
Bạn có thể chạy thử đoạn mã sau:
Listing 1:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Collections;
public class YieldTest
{
public static IEnumerable Foo2(int number)
{
for (int i = 0; i < number; i++)
{
yield return i;
}
}
static void Main()
{
foreach (var item in Foo2(10))
{
Console.Write(item);
}
Console.Read();
}
}
Output:
0123456789
Bạn có thể đặt break point tại dòng 10 và dòng 18 (hai dòng được highlight) và chức năng debug continue (F5) thì sẽ thấy rằng hai break point này sẽ thay phiên nhau được kích hoạt. Điều này chứng tỏ rằng chương trình có thể nhảy qua lại giữa hai phương thức mà không làm mất trạng thái hiện tại của chúng.
Giả sử bạn muốn thoát ra khỏi phương thức khi i = 5, ta cần sửa lại phương thức Foo2() trên:
?
1
2
3
4
5
6
7
8
9
public static IEnumerable Foo2(int number)
{
for (int i = 0; i < number; i++)
{
if (i == 5)
yield break;
yield return i;
}
}
Ưu điểm
Một ưu điểm của việc dùng “yield” mà bạn có thể thấy ngay ở ví dụ đầu tiên là số lượng dòng code sẽ được giảm đi.
Hơn nữa, vì hai phương thức làm việc luân phiên nhau, bạn không cần thiết phải tạo hoặc lấy nguyên một danh sách các phần tử để duyệt. Điều này được áp dụng trong những trường hợp như tìm kiếm, số lượng phần tử cần duyệt sẽ được giảm bớt tùy theo vị trí của phần tử cần tìm.

Cơ chế hoạt động

Trong phương thức Foo2() mới này, bạn sẽ không nhận thấy sự khác biệt nếu thay yield break bằng break bởi vì khi vòng lặp bị kết thúc bằng break thì phương thức cũng kết thúc theo. Muốn kiểm tra sự khác biệt, bạn hãy dùng Console.WriteLine() in gì ra màn hình ở cuối phương thức Foo2() trên.
Khi dùng ILDasm.exe phân tích Listing 1 ta nhận thấy compiler đã tạo ra một lớp con trong lớp YieldTest:
YieldTest ILDasm
Hoặc dùng một Reflector của hãng thứ ba để disassemble lớp này ra mã C# ta được code tổng quát sau:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private sealed class <foo2>d__0 : IEnumerable<object>, IEnumerable, IEnumerator<object>, IEnumerator, IDisposable
{
// Fields
private int <>1__state;
private object <>2__current;
public int <>3__number;
private int <>l__initialThreadId;
public int <i>5__1;
public int number;
// Methods
[DebuggerHidden]
public <foo2>d__0(int <>1__state);
private bool MoveNext();
[DebuggerHidden]
IEnumerator<object> IEnumerable<object>.GetEnumerator();
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator();
[DebuggerHidden]
void IEnumerator.Reset();
void IDisposable.Dispose();
// Properties
object IEnumerator<object>.Current { [DebuggerHidden] get; }
object IEnumerator.Current { [DebuggerHidden] get; }
}</object></object></object></foo2></i></object></object></foo2>

Như vậy bạn có thể rằng đây thực chất là một lớp làm việc dựa trên IEnumertor với các phương thức chính làMoveNext(), Reset() và property Current.
Dựa theo phương pháp này, bạn có thể dễ dàng cài đặt và tự tạo ra kĩ thuật tương tự “yield” để dùng trong các ngôn ngữ không hỗ trợ từ khóa này chẳng hạn như VB.Net.

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

Đăng nhận xét