ข้อมูลประเภท Collections ใน Dart เช่น List Set ต่างสืบทอดมาจาก Iterable class ส่วน Map class จะมี Map.entries Map.keys Map.values ที่คืนค่ามาเป็น Iterable เพื่อให้ผู้ใช้งานเข้าถึงค่าแต่ละส่วนที่อยู่ใน Map ได้
Iterable คือ ชุดของค่าหรือ elements ที่สามารถเข้าถึงได้ตามลำดับ การเข้าตามลำดับจากตัว element แรกจนถึงตัวสุดท้าย จะใช้ Iterator class ในการเข้าถึง ผ่านคำสั่ง Iterator.current
และ Iterator.moveNext()
เนื่องจาก Iterable class เป็น Abstract classes ไม่สามารถ instance โดยตรงได้ แต่ต้องอาศัยสร้างจาก subtype พวก List หรือ Set แต่ถ้าหากจะสร้างโดยตรงจะมี constructor มาให้ 2 ตัวคือ
Iterable.empty()
ใช้สำหรับสร้าง empty iterableIterable.generate()
ใช้สำหรับสร้างสมาชิกจากฟังก์ชั่นที่ผู้ใช้กำหนดตัวอย่างการสร้าง empty iterable โดยใช้ Iterable.empty()
constructor ผลที่ได้จะว่า Iterable ว่าง ๆ มา ซึ่งในการใช้งานในชีวิตจริง ก็ไม่รู้เอาไปใช้ทำอะไรเหมือนกัน 😅 เพราะตัว Iterable class ไม่มีคำสั่งในการ เพิ่ม ลบ หรือแก้ไขสมาชิกมาให้
void main() {
var emptyIterable = Iterable.empty();
print(emptyIterable); // output → ()
print(emptyIterable.length); // output → 0
print(emptyIterable.first); // exception error → Bad state: No element
}
อีกวิธีในการสร้าง Iterable คือ ผู้ใช้ต้องเขียนฟังก์ชั่น ที่รับค่า index มาแล้วคืนค่าเป็นสมาชิกของ Iterable กลับไป หากไม่เขียนฟังก์ชั่น จะได้ Iterable ที่มีตัวเลขตาม index เป็นสมาชิกแทน
void main() {
var elements1 = Iterable.generate(10);
var elements2 = Iterable.generate(10, (index) => index); //same as elements1
print(elements1); // output → (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
print(elements2); // output → (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
print(elements1.length); // output → 10
print(elements1.first); // output → 0
// generate Iterable of even numbers 0 to 18
var elements3 = Iterable.generate(10, (index) => index * 2);
print(elements3); // output → (0, 2, 4, 6, 8, 10, 12, 14, 16, 18)
// generate Iterable of 'a' to 'j'
int codeOfA = 'a'.codeUnitAt(0);
var elements4 = Iterable.generate(10, (index) => String.fromCharCode(codeOfA + index));
print(elements4); // output → (a, b, c, d, e, f, g, h, i, j)
}
ใน Set และ List ซึ่งสืบทอดมาจาก Iterable สามารถใช้คำสั่งต่าง ๆ ของ Iterable class ได้ทันที
void main() {
var list1 = ["a", "b", "c"];
print(list1.length); // → 3
print(list1.first); // → a
print(list1.last); // → c
print(list1.elementAt(1)); // → b
// exception error → RangeError (index): Invalid value: Not in inclusive range 0..2: 3
print(list1.elementAt(3));
}
ใน Map สามารถเข้าถึง elements ของ key-value pair ได้โดยใช้ Map.keys
Map.values
void main() {
var map1 = {'a': 'Ant', 'b': 'Boy', 'c': 'Cat'};
Iterable<String> k = map1.keys;
print(k.length); // → 3
print(k.first); // → a
print(k.elementAt(1)); // → b
print(k.last); // → c
Iterable<String> v = map1.values;
print(v.length); // → 3
print(v.first); // → Ant
print(v.elementAt(1)); // → Boy
print(v.last); // → Cat
}
หากต้องการเข้าถึง elements แบบ key-value pair เป็นชุด ๆ จะมีคำสั่ง Map.entries เป็น Iterable ของ MapEntry class
void main() {
var map1 = {'a': 'Ant', 'b': 'Boy', 'c': 'Cat'};
Iterable<MapEntry<String, String>> entriesMap1 = map1.entries;
print(entriesMap1.length); // → 3
print(entriesMap1.first); // → MapEntry(a: Ant)
print(entriesMap1.last); // → MapEntry(c: Cat)
print(entriesMap1.elementAt(1)); // → MapEntry(b: Boy)
MapEntry item1 = entriesMap1.first;
print(item1.key); // → a
print(item1.value); // → Ant
}
Iterator class เป็น interface ที่มีคำสั่งที่ใช้สำหรับอ่านค่า elements ทั้งหมดเพียง 1 เที่ยว จุดประสงค์เพื่อความเรียบง่าย ไม่ต้องมี index ในการระบุตำแหน่ง สามารถนำไปใช้ในคำสั่งพวก loop ต่าง ๆ ในการไล่อ่านทีละ element จนครบ มีคำสั่งที่ใช้งานดังนี้
.moveNext()
ใช้สำหรับเลื่อนไปลำดับถัดไป โดยจะคือค่า false
เมื่อไม่มีตัวถัดไปแล้ว.current
ใช้อ่านค่า element ลำดับปัจจุบันvoid main() {
var myList = [1, 2, 3];
Iterator<int> myIterator = myList.iterator;
// you need call moveNext() before read the value
print(myIterator.moveNext()); // output → true
print(myIterator.current); // output → 1
print(myIterator.moveNext()); // output → true
print(myIterator.current); // output → 2
print(myIterator.moveNext()); // output → true
print(myIterator.current); // output → 3
print(myIterator.moveNext()); // output → false
print(myIterator.current); // error → type 'Null' is not a subtype of type 'int' in type cast
}
จากตัวอย่าง เมื่อต้องการจะอ่านค่า element จาก elements ตามลำดับ ให้เรียก .moveNext()
เพื่อเลื่อนไปลำดับไปเรื่อย ๆ โดยหากค่าที่คืนมาเป็น false
แสดงว่าไม่มีค่าให้อ่านแล้ว ถ้าพยายามจะอ่านค่า .current
จะคืนค่าเป็น null
และอาจเกิด Exception error ขึ้นได้
โดยทั่วไปการใช้งาน Iterator จะใช้ใน loop เพื่อวนอ่านค่าจนครบทุกตัว
void main() {
var myList = [1, 2, 3];
Iterator<int> myIterator = myList.iterator;
while (myIterator.moveNext()) {
print(myIterator.current); // output → 1 2 3
}
}
จากตัวอย่างการใช้งาน Iterator ที่ไม่เหมือนกับ Iterable คือ มันจะสำเนาตัว Iterator ออกมาทุกครั้งที่ดึงออกมาใช้งาน และจะมีการจำตำแหน่งของแต่ละตัวแยกกันออกไป หากสร้าง Iterator ออกมา 2 ตัว แต่ละตัวจะทำงานแยกอิสระออกจากกัน ลองดูตัวอย่างการเขียนแบบ asynchronous เพื่ออ่านค่าจาก Iterator
void main() {
var myList = [1, 2, 3, 4];
Iterator<int> test1 = myList.iterator;
Iterator<int> test2 = myList.iterator;
delayPrint('test1-', test1, 1); // print value every 1 second
delayPrint('test2--', test2, 2); // print value every 2 seconds
}
void delayPrint(String elementsName, Iterator<int> items, int delaySeconds) async {
while (items.moveNext()) {
await Future.delayed(Duration(seconds: delaySeconds));
print("$elementsName: ${items.current}");
}
}
ผลที่ได้
test1-: 1
test2--: 1
test1-: 2
test1-: 3
test2--: 2
test1-: 4
test2--: 3
test2--: 4
เมื่อทำการสร้าง Iterator ออกมาจาก collection แล้ว หากมีการแก้ไข เพิ่ม ลบ สมาชิกใน collection ระหว่างการ moveNext() จะทำให้เกิด Error exception ขึ้นได้
.any()
ทดสอบว่ามี element ตัวไหนที่ตรงกับ test หรือไม่.any()
ใช้ทดสอบ element ว่ามีอย่างน้อย 1 ตัว ที่ตรงกับเงื่อนไข test ที่ต้องการหรือไม่
var numbers = <int>[1, 2, 3, 5, 6, 7];
print(numbers.any((element) => element >= 5)); // true;
print(numbers.any((element) => element >= 10)); // false;
.every()
ทดสอบว่า elements ทั้งหมดตรงกับ test หรือไม่.every()
ใช้ทดสอบ elements ทุกตัว ตรงกับเงื่อนไข test ที่ต้องการหรือไม่
var numbers = <int>[1, 2, 3, 5, 6, 7];
print(numbers.every((element) => element >= 0)); // true;
print(numbers.every((element) => element >= 5)); // false;
.contains()
ตรวจสอบว่ามีค่าที่สนใจอยู่ใน elements หรือไม่.contains()
ใช้ตรวจสอบว่ามีสามาชิกตรงกับค่าที่สนใจหรือไม่ ถ้ามีจะคืนค่า true
กลับมา
var intList = [1, 2];
print(intList.contains(1)); // → true
print(intList.contains(5)); // → false
.firstWhere()
หาสมาชิกที่ตรงกับเงื่อนไข test ตัวแรก.firstWhere()
ใช้สำหรับหาสมาชิกที่ตรงกับ test ที่กำหนด ถ้าพบจะคืนค่าสมาชิกที่พบ หากไม่พบจะคือค่าใน orElse หากไม่กำหนด orElse แล้วไม่เจอสมาชิก จะมีผลทำให้เกิด Exception error ขึ้น
var intList = [1, 2, 3, 4, 5];
print(intList.firstWhere((element) => element > 1)); // output → 2
print(intList.firstWhere((element) => element > 5, orElse: () => -1)); // output → -1
// if not found and missing orElse: will throw error
// exception error → Bad state: No element
print(intList.firstWhere((element) => element < 1));
.lastWhere()
ทำงานเหมือน .firstWhere()
แต่กลับทิศค้นหาจากหลังมาหน้า.lastWhere()
ใช้สำหรับหาสมาชิกที่ตรงกับ test ที่กำหนด หลักการเดียวกัน .firstWhere()
แต่จะทำงานในทิศตรงกันข้าม จะค้นหาจากหลังมาหน้า เมื่อเจอจะคืนค่าสมาชิกที่ตรงกับเงื่อนไขกลับมา
var intList = [1, 2, 3, 4, 5];
print(intList.lastWhere((element) => element > 1)); // output → 5
print(intList.lastWhere((element) => element > 5, orElse: () => -1)); // output → -1
// if not found and missing orElse: will throw error
// exception error → Bad state: No element
print(intList.lastWhere((element) => element < 1));
.take()
ใช้สำหรับเอาสมาชิกด้านหน้าตามจำนวนที่ระบุ มาสร้างเป็น iterable ใหม่.skip()
ทำงานตรงข้ามกับ .take() ไม่เอาสมาชิกด้านหน้าตามจำนวนที่ระบุ มาสร้างเป็น iterable ใหม่.takeWhile()
กับ .skipWhile()
ใช้เงื่อนไข test ในการเลือกสมาชิก มาสร้างเป็น iterable ใหม่ ส่วนตัวรู้สึกว่าค่อนข้างสับสนใจการใช้งาน เลยไม่อยากแนะนำเท่าไหร่var intList = [1, 2, 3, 4, 5];
print(intList.take(3)); // → (1, 2, 3)
print(intList.skip(3)); // → (4, 5)
// in case is count > length
print(intList.take(100)); // → (1, 2, 3, 4, 5)
print(intList.skip(100)); // → ()
print(intList.takeWhile((value) => value != 3)); // → (1, 2)
print(intList.skipWhile((value) => value != 3)); // → (3, 4, 5)
.where()
ใช้เงื่อนไข test ในการเลือกสมาชิกทั้งหมด มาสร้างเป็น iterable ใหม่.whereType()
ใช้เงื่อนไข List<num?> numList = [1, 2.5, 3.0, 4, null];
print(numList.where((element) => element is int && element > 0)); // → (1, 4)
print(numList.whereType<int>()); // → (1, 4)
print(numList.whereType<double>()); // → (2.5, 3.0)
print(numList.whereType<int?>()); // → (1, 4, null)
.join()
สำหรับเอาสมาชิกทุกตัวแปลงเป็น String แล้วเอามาต่อกัน โดยมันตัวคั่นตามที่ระบุ.map()
สำหรับเอาสมาชิกทุกตัวมาแปลงตามฟังก์ชั่นที่ผู้ใช้กำหนด แล้วคืนเป็น Iterable ใหม่.reduce()
สำหรับลดจำนวนสมาชิกให้เหลือแค่ค่าเดียว โดยใช้วิธีการตามฟังก์ชั่นที่ผู้ใช้กำหนดตัวอย่างการใช้งาน .join()
สำหรับเอาสมาชิกมาแปลงเป็น String แล้วต่อกันตามตัวคั่นที่กำหนด หากไม่กำหนดตัวคั่น ผลที่ได้จะเป็นเอาสมาชิกทั้งหมดมาต่อกันโดยตรง
var words = ['Hello', 'World'];
print(words.join()); // output → HelloWorld
print(words.join(', ')); // output → Hello, World
print(words.join(' - ')); // output → Hello - World
ตัวอย่างการใช้งาน .map()
เพื่อสร้าง Iterable ใหม่ โดยใช้แปลงข้อมูลที่แปลงโดยฟังก์ชั่นที่ผู้ใช้กำหนด
var words = ['Hello', 'World'];
print(words.map((element) => element.toUpperCase())); // output → (HELLO, WORLD)
print(words.map((element) => element.length)); // output → (5, 5)
ตัวอย่างการใช้งาน .reduce()
เพื่อสร้าง Iterable ใหม่ โดยใช้แปลงข้อมูลที่แปลงโดยฟังก์ชั่นที่ผู้ใช้กำหนด
var numbers = [0, 5, 10, 30];
var result = numbers.reduce((lastResult, element) => lastResult + element);
print(result); // output → 45
อธิบายการทำงานของ .reduce()
ในตัวอย่างจะเห็นว่าฟังก์ชั่นที่ใช้จะรับค่า 2 ตัวคือ lastResult กับ element โดยในแต่ละรอบที่นำสมาชิกมาประมวลผล จะเอาผลที่ได้ในแต่ละรอบไปใช้ในรอบถัดไปคือ lastResult เพื่อให้เห็นภาพการทำงาน จะแสดงค่าของตัวแปรที่รับมาแต่ละรอบดังนี้
void main() {
var numbers = [0, 5, 10, 30];
var loopCount = 0;
var result = numbers.reduce(
(lastResult, element) {
++loopCount;
print('loopCount: $loopCount\n');
print('lastResult: $lastResult');
print('element: $element');
var returnValue = lastResult + element;
print('returnValue: $returnValue');
print('------------------------------');
return returnValue;
}
);
}
ผลที่ได้
loopCount: 1
lastResult: 0
element: 5
returnValue: 5
------------------------------
loopCount: 2
lastResult: 5
element: 10
returnValue: 15
------------------------------
loopCount: 3
lastResult: 15
element: 30
returnValue: 45
------------------------------
จากผลที่ได้อธิบายการทำงานได้ดังนี้
castFrom()
.castFrom()
เป็น static method ของ Iterable class ใช้สำหรับ cast เปลี่ยนประเภทของ Generic ใหม่ โดยผู้ใช้ต้องทราบเองว่าสิ่งที่ต้องการ cast สามารถทำ type casting ได้เมื่อตอน runtime เช่น
double
→ num
int
→ num
List<int> intList = [0, 5, 10, 30];
List<num> newList = Iterable.castFrom<int, num>(intList).toList();
newList.add(10.5); // add double is OK for num
print(newList); // → [0, 5, 10, 30, 10.5]
List<int> intList2 = [0, 5, 10, 30];
List<int?> newListWithNull = Iterable.castFrom<int, int?>(intList2).toList();
newListWithNull.add(null);
print(newListWithNull); // → [0, 5, 10, 30, null]
ถ้าพยายาม cast ในสิ่งที่เป็นไปไม่ได้ ก็จะเกิด Exception error → type 'xxx' is not a subtype of type 'yyy' in type cast
ข้อสังเกต ในการเขียนโปรแกรมทั่วไป ผู้ใช้สามารถใช้ ...
เพื่อนำ List ที่มีอยู่มาสร้าง List ใหม่ โดยกำหนด Generic ก็สามารถทำได้เหมือนกับ .castFrom()
บางทีก็ไม่รู้ว่าจะเขียนให้ยาวไปเพื่ออะไร 🙄
List<int> intList = [0, 5, 10, 30];
List<num> newList = <num>[...intList];
newList.add(10.5); // add double is OK for num
print(newList); // → [0, 5, 10, 30, 10.5]
หากไม่สามารถใช้การ cast ด้วยวิธีปกติของ Dart ได้ สามารถใช้คำสั่ง .map()
แล้วเขียนฟังก์ชั่นระบุเงื่อนไขวิธีการแปลงข้อมูลเองก็เป็นอีกทางเลือกเช่นกัน
List<num> numList = [0, 5, 10, 30.0, 1.5];
List<int> intList = numList.map(
(element) => (element is int) ? element : element.toInt()
).toList();
print(intList); // → [0, 5, 10, 30, 1]