Iterator
วนดูสมาชิกของ collection โดยไม่เปิดเผยโครงสร้างภายใน
เวลาต้องเขียน code ที่ “วนดูสมาชิกทีละตัว” ของ collection ปัญหาที่พบบ่อยคือ collection แต่ละชนิดมีโครงสร้างภายในต่างกัน — array ใช้ index, linked list ต้องไล่ตาม pointer, ต้นไม้ (tree) ต้องเดินแบบ depth-first หรือ breadth-first, บาง sequence ก็ยังไม่มีอยู่จริงจนกว่าจะถูกขอ (เช่น stream ข้อมูลจากเซนเซอร์) ถ้าปล่อยให้ client code รู้จักโครงสร้างภายในเหล่านี้โดยตรง จะเกิดปัญหาสองทาง คือ (1) client ผูกติดกับ implementation ของ collection แน่นเกินไป เปลี่ยนโครงสร้างเก็บข้อมูลทีก็กระทบ code ที่ใช้งานทันที และ (2) ตัว collection เองก็ต้องแบกรับทั้งหน้าที่ “เก็บข้อมูลอย่างมีประสิทธิภาพ” และ “รู้วิธีท่องตัวเองในทุกรูปแบบที่ client ต้องการ” ซึ่งขัดกับ Single Responsibility Principle
Iterator แก้ปัญหานี้ด้วยการ แยกตรรกะการท่อง (traversal) ออกจากตัว collection ไปไว้ใน object แยกต่างหากที่เรียกว่า iterator ตัว iterator เก็บสถานะของการวน (เช่น ตำแหน่งปัจจุบัน) ไว้ในตัวเอง และเปิด interface กลาง ๆ ให้ client เรียกใช้ได้เหมือนกันไม่ว่าเบื้องหลังจะเป็น collection ชนิดใด — โดยทั่วไปคือ “ขอสมาชิกตัวถัดไป” (MoveNext() หรือ next()) และ “อ่านค่าปัจจุบัน” (Current) client จึงไม่จำเป็นต้องรู้เลยว่ากำลังวนอยู่บน array, tree หรือ lazy sequence
ใน C# pattern นี้ถูกฝังเข้าไปในภาษาและ BCL เรียบร้อยแล้วผ่านคู่ interface IEnumerable<T> / IEnumerator<T> คำสั่ง foreach เป็นเพียง syntactic sugar ที่ compiler ขยายออกมาเป็นการเรียก GetEnumerator(), MoveNext(), Current และดูแล Dispose() ให้อัตโนมัติ ส่วนคำสั่ง yield return ทำให้ compiler สร้าง class iterator แบบ lazy ให้เราโดยไม่ต้องเขียน state machine เองเลย และ LINQ ทั้งชุดก็สร้างอยู่บนรากฐานของ IEnumerable<T> นี้
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Client
class IAggregate
IAggregate : +CreateIterator() IIterator
class ConcreteAggregate
ConcreteAggregate : -items
ConcreteAggregate : +CreateIterator() IIterator
class IIterator
IIterator : +MoveNext() bool
IIterator : +Current() Item
IIterator : +Reset()
class ConcreteIterator
ConcreteIterator : -aggregate
ConcreteIterator : -position
ConcreteIterator : +MoveNext() bool
ConcreteIterator : +Current() Item
ConcreteIterator : +Reset()
Client --> IAggregate
Client --> IIterator
IAggregate <|.. ConcreteAggregate
IIterator <|.. ConcreteIterator
ConcreteAggregate ..> ConcreteIterator : creates
ConcreteIterator --> ConcreteAggregate
- IAggregate — interface ของ collection ที่ประกาศ method factory สำหรับสร้าง iterator (ใน .NET คือ
IEnumerable<T>.GetEnumerator()) - ConcreteAggregate — collection จริง (array, tree, custom data structure) ที่เก็บข้อมูลและรู้วิธีสร้าง iterator ที่เหมาะกับโครงสร้างของตัวเอง
- IIterator — interface กลางที่ client ใช้คุยด้วย ประกาศ
MoveNext(),Current, บางแบบก็มีReset()(ใน .NET คือIEnumerator<T>) - ConcreteIterator — เก็บตำแหน่งปัจจุบันและอ้างอิงกลับไปยัง aggregate ที่มันวนอยู่ เป็นตัวเดียวที่รู้ว่าจะเดินไปยัง “ตัวถัดไป” อย่างไรสำหรับโครงสร้างข้อมูลนั้น ๆ
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ลำดับการทำงานเริ่มจาก client ขอ iterator จาก aggregate แทนที่จะเข้าถึงข้อมูลภายในตรง ๆ จากนั้นก็วนเรียก MoveNext() ควบคู่กับอ่าน Current จนกว่าจะหมด collection — ตัว aggregate ไม่ต้องรู้เลยว่า client จะวนแบบไหนหรือหยุดกลางทางเมื่อไหร่ ส่วน iterator ก็ไม่ต้องรู้เลยว่า client จะเอาค่าไปทำอะไรต่อ นี่คือจุดที่ทำให้ทั้งสองฝั่งแยกอิสระจากกัน (decoupled)
sequenceDiagram
participant Client
participant Aggregate as ConcreteAggregate
participant Iterator as ConcreteIterator
Client->>Aggregate: CreateIterator()
Aggregate->>Iterator: new ConcreteIterator(this)
Aggregate-->>Client: iterator
loop จนกว่าจะหมด collection
Client->>Iterator: MoveNext()
Iterator-->>Client: true / false
Client->>Iterator: Current
Iterator-->>Client: item
end
ใน C# ขั้นตอนนี้เกิดขึ้นทุกครั้งที่เราเขียน foreach compiler จะขยายเป็น code ประมาณนี้โดยอัตโนมัติ (และครอบด้วย try/finally เพื่อเรียก Dispose() ให้ด้วย):
IEnumerator<int> enumerator = collection.GetEnumerator();try{ while (enumerator.MoveNext()) { var item = enumerator.Current; // ทำงานกับ item }}finally{ (enumerator as IDisposable)?.Dispose();}จุดที่ทำให้ yield return ทรงพลังคือมัน สร้าง state machine ของ ConcreteIterator ให้อัตโนมัติแบบ lazy — code ใน method จะไม่ทำงานทันทีตอนเรียก แต่จะทำงานทีละ “ก้าว” ทุกครั้งที่ MoveNext() ถูกเรียก ทำให้สร้าง sequence ที่ไม่จำกัดความยาว (infinite sequence) หรือ sequence ที่คำนวณแพงได้โดยไม่เปลืองหน่วยความจำเก็บทั้งชุดล่วงหน้า
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างนี้จำลอง collection แบบ binary tree ที่ต้องเดินแบบ in-order traversal — เป็นกรณีคลาสสิกที่ Iterator ช่วยได้มาก เพราะ client ไม่จำเป็นต้องรู้เลยว่าเบื้องหลังเป็นโครงสร้างต้นไม้:
// node ของ binary search treepublic class TreeNode<T>{ public T Value { get; } public TreeNode<T>? Left { get; set; } public TreeNode<T>? Right { get; set; }
public TreeNode(T value) => Value = value;}
// collection ที่ implement IEnumerable<T> เพื่อให้ foreach ใช้งานได้public class BinarySearchTree<T> : IEnumerable<T>{ private TreeNode<T>? _root;
public void Add(TreeNode<T> node) => _root = node; // ตัวอย่างแบบย่อ ไม่รวม logic การแทรกจริง
// เปิดเผยแค่วิธีการขอ iterator เท่านั้น ไม่เปิดเผยโครงสร้างต้นไม้เลย public IEnumerator<T> GetEnumerator() => InOrderTraverse(_root); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// yield return ให้ compiler สร้าง ConcreteIterator แบบ lazy ให้อัตโนมัติ private static IEnumerator<T> InOrderTraverse(TreeNode<T>? node) { if (node is null) yield break;
// เดิน subtree ซ้ายก่อน แล้วค่อยคืนค่าตัวเอง แล้วค่อยเดิน subtree ขวา if (node.Left is not null) { var left = InOrderTraverse(node.Left); while (left.MoveNext()) yield return left.Current; }
yield return node.Value;
if (node.Right is not null) { var right = InOrderTraverse(node.Right); while (right.MoveNext()) yield return right.Current; } }}
// client ไม่รู้เลยว่าเบื้องหลังเป็น tree — วนด้วย foreach เหมือน collection ทั่วไปvar tree = new BinarySearchTree<int>();// ... เติมข้อมูลเข้า tree ...
foreach (var number in tree){ Console.WriteLine(number); // ได้ค่าตามลำดับ in-order โดยไม่ต้องรู้จัก TreeNode เลย}ถ้าต้องการ iterator ที่ควบคุมเองแบบไม่ผ่าน foreach ก็ทำได้ตรง ๆ ผ่าน IEnumerator<T>:
using var it = tree.GetEnumerator();while (it.MoveNext()){ Console.WriteLine(it.Current);}เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- collection มีโครงสร้างภายในซับซ้อน (tree, graph, linked structure) ที่ไม่อยากให้ client ต้องรู้จัก
- ต้องการรองรับการวนหลายรูปแบบพร้อมกัน (เช่น หลาย iterator วนบน collection เดียวกันพร้อมกันแบบอิสระ)
- ต้องการเปิดให้เพิ่ม algorithm การท่องแบบใหม่ในอนาคตโดยไม่ต้องแก้ collection เดิม (เปิดขยาย ปิดแก้ไข — Open/Closed Principle)
- ต้องการ sequence แบบ lazy ที่คำนวณทีละตัวตามต้องการ เช่น stream ข้อมูลขนาดใหญ่หรือ sequence ไม่จำกัดความยาว
- ต้องการให้ client เขียน code แบบเดียวกันไม่ว่าจะวนบน collection ชนิดใด (polymorphic traversal)
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- collection เรียบง่ายมาก เช่น array หรือ
List<T>ที่ index-based access ตรงไปตรงมาอยู่แล้ว การเพิ่ม iterator ของตัวเองมีแต่จะเพิ่มความซับซ้อนโดยไม่จำเป็น (ใน .NET แทบทุกกรณีใช้IEnumerable<T>ที่มีอยู่แล้วได้เลย) - ต้องการประสิทธิภาพสูงสุดในการเข้าถึงแบบสุ่ม (random access) — การครอบด้วย iterator interface เพิ่ม overhead ของการเรียกผ่าน interface และตัด optimization เฉพาะโครงสร้าง (เช่น index จริงของ array) ออกไป
- ต้องการแก้ไข collection ระหว่างวน (add/remove) โดยไม่ระวัง — iterator ส่วนใหญ่ (รวมถึง
IEnumerator<T>ของ .NET) จะ throwInvalidOperationExceptionเมื่อ collection ถูกแก้ไขระหว่างวน ต้องออกแบบเผื่อกรณีนี้เป็นพิเศษ
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | แยกตรรกะการท่องออกจาก collection ตรงตาม Single Responsibility Principle |
| ข้อดี | เพิ่มวิธีท่องแบบใหม่ได้โดยไม่แก้ collection เดิม (Open/Closed Principle) |
| ข้อดี | รองรับหลาย iterator วนบน collection เดียวกันพร้อมกันได้อย่างอิสระ |
| ข้อดี | เปิดทางให้ทำ lazy evaluation ผ่าน yield return ประหยัดหน่วยความจำสำหรับ sequence ขนาดใหญ่ |
| ข้อเสีย | เพิ่ม class/object อีกชั้นหนึ่ง อาจดูเกินความจำเป็นสำหรับ collection ง่าย ๆ |
| ข้อเสีย | มี overhead เล็กน้อยจากการเรียกผ่าน interface เทียบกับ index-based access ตรง ๆ |
| ข้อเสีย | ต้องระวังเรื่อง collection ถูกแก้ไขระหว่างวน (invalidated iterator) |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Encapsulation — Iterator คือการนำหลัก encapsulation มาใช้กับการท่อง collection โดยเฉพาะ
- Composite — โครงสร้างต้นไม้แบบ Composite มักถูกวนด้วย Iterator เพื่อเดินทั้ง object ที่เป็นใบและที่เป็นกิ่ง
- Visitor — ทั้งคู่แยกตรรกะออกจาก collection แต่ Visitor เน้นแยก “การกระทำ” ส่วน Iterator เน้นแยก “การเดิน”
- Strategy — algorithm การท่องแบบต่าง ๆ (เช่น pre-order vs in-order) สามารถมองเป็น strategy ที่สลับกันได้
- Lazy Load —
yield returnใช้หลักการเดียวกับ lazy loading คือคำนวณ/โหลดค่าเมื่อถูกร้องขอจริงเท่านั้น