ในบทก่อนๆ เรารู้จักตัวแปรประเภท List กันมาแล้ว แต่ในภาษา Dart (และภาษาสมัยใหม่อื่นๆ ด้วย) จะมีตัวแปรอีกชนิดนึงที่ "สามารถนำมาวนลูปได้" หรือ "สามารถ access ค่าเป็นลำดับเรียงต่อกันได้"
ตามปกติเราสามารถสร้างลิสต์ได้แบบนี้
List<int> items = [1, 2, 3, 4];
แต่ถ้าเราลองเข้าไปดู source code ของคลาส List เราจะพบว่ามัน extends
มาจากคลาสๆ หนึ่งที่มีชื่อว่า Iterable
แปลว่าเราสามารถสร้างลิสต์แบบนี้ได้
Iterable<int> items = [1, 2, 3, 4];
ซึ่งทั้ง 2 แบบสามารถเอามาวน loop ได้ด้วยนะ
List<int> items1 = [1, 2, 3, 4];
for(var item in items){
print(item);
}
//output: 1 2 3 4
Iterable<int> items2 = [1, 2, 3, 4];
for(var item in items){
print(item);
}
//output: 1 2 3 4
ได้ผลเหมือนกันเลยนี่นา? แล้วถ้ามันทำได้เหมือนกันแบบนี้มันจะมี 2 คลาสทำไมกัน แปลว่ามันต้องมีความต่างกันล่ะ
และนั่นแหละ คือหัวข้อที่เราจะมาพูดกันในบทความนี้ คือเรื่องของ Generator และ Iterable
by Lazy ขี้เกียจไว้ก่อน จะใช้แล้วค่อยทำ
จากบทที่ 3 เราเคยพูดไปแล้วว่าวิธีการสร้าง List มีหลายวิธีมาก หนึ่งในนั้นคือการสร้างด้วย Generator
สิ่งที่เราต้องเตรียมคือฟังก์ชันที่รับ index
มาแล้วตอบค่าของ item ในตำแหน่งนั้นกลับไป
List<int> list = List<int>.generate(10, (index){
return index + 1;
});
// List [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
แต่ลองดูโค้ดต่อไปนี้
var list = List<int>.generate(3, (index){
print('creating item from List');
return index + 1;
});
var iter = Iterable<int>.generate(3, (index){
print('creating item from Iterable');
return index + 1;
});
คำถามคือ...ถ้าเอาโค้ดแค่นี้เลยนะ มารัน -> จะได้ output ออกมาเป็นยังไง?
คำตอบคือ...
creating item from List ━┓
creating item from List ┣━ 3ครั้งจากListอย่างเดียว
creating item from List ━┛
นั่นคือฟังก์ชันสำหรับสร้างไอเทมของ List ถูกรันจนครบ 3 ครั้งเลย แต่กลับกันคือ Iterable นั้นไม่ถูกรันเลยซักครั้ง!
แต่ถ้าเราเขียนโค้ดต่อไปอีกหน่อย เป็นแบบนี้
var list = List<int>.generate(3, (index){
print('creating item from List');
return index + 1;
});
var iter = Iterable<int>.generate(3, (index){
print('creating item from Iterable');
return index + 1;
});
print('first item of list: ${list[0]}');
//หรือจะเรียกให้เหมือน iterable คือ list.elementAt(0) ก็ได้
print('first item of iter: ${iter.elementAt(0)}');
//iterable ไม่มีคำสั่ง [] นะ
output กลับได้เป็น
creating item from List ━┓
creating item from List ┣━ 3 ครั้ง
creating item from List ━┛
first item of list: 1
creating item from Iterable ━━━ 1 ครั้ง
first item of iter: 1
เราจะพบว่าฟังก์ชันของ Iterable เริ่มทำงานแล้วล่ะนะ แต่ทำงานแค่ครั้งเดียว!?
ส่งนี้เรียกว่า "Lazy Evaluation" นั่นคือเราจะยังไม่สร้างไอเทมในทันทีที่ถูกสั่งให้สร้าง แต่ถ้าไหร่ก็ตามที่ถูกเรียกใช้ค่อยสร้างตอนนั้น
ถ้าจำลองสถาณการณ์ของโค้ดเมื่อกี้:
- สำหรับ List: เปิดมา ไม่ต้องพูดพร่ำทำเพลง สร้างมันรวดเดียวเลย 3 ไอเทม (ยังไม่มีคนเรียกใช้เลยนะ สร้างไว้ก่อน)
- สำหรับ Iterable: เปิดมา ยังไม่ทำอะไรทั้งนั้นแหละ (Lazy) แต่เมื่อมีการเรียกไอเทม elementAt(0) มันพบว่าไอเทมตัวที่ 0 น่ะยังไม่ถูกสร้างขึ้นมาเลย แต่เขาจะใช้งานแล้วนี่นา -> งั้นก็สร้างซะตอนนี้เลย!
แต่อ่านมาถึงตรงนี้ เราก็อาจจะงงๆ สงสัยกันว่า แล้วจะทำแบบนี้เพื่ออะไรกัน?
ลองดูตัวอย่างต่อไปครับ...
สร้าง Iterable ด้วยฟังก์ชัน Generator
สมมุติว่าเราจะสร้างฟังก์ชันสำหรับสร้างเลข 1-1,000,000 (หนึ่งถึงหนึ่งล้าน) ขึ้นมาหนึ่งตัวเพื่อเอาไปวนลูปอะไรสักอย่างนึง
for(var i in getNumbers()){
print(i);
}
ทั่วๆ ไปเราก็อาจจะนึกถึงฟังก์ชันที่สร้าง List คืนมา แบบนี้
List<int> getNumbers(){
return [for(var i=1; i<=1000000; i++) i];
}
ดังนั้นเวลาโค้ดทำงาน
- ฟังก์ชันสร้างตัวเลขทั้ง 1 ล้านตัวขึ้นมาก่อน
- ส่งกลับไปให้ลูปทำการวนปริ๊นค่าทั้ง 1 ล้านตัวนั่นออกมา
แต่ปัญหาจะเกิดขึ้น ถ้าลูปของเรามันสามารถหยุดกลางคันได้
for(var i in getNumbers()){
print(i);
if(i >= 10) break; //ถึง 10 เมื่อไหร่ก็จบลูปได้
}
ทีนี้ โค้ดก็จะทำงานเปลี่ยนไป
- ฟังก์ชันสร้างตัวเลขทั้ง 1 ล้านตัวขึ้นมาก่อน
- ส่งกลับไปให้ลูปทำการวนปริ๊นค่า
- แต่คราวนี้ ปริ๊นไปแค่ 10 ตัวก็จบลูปแล้ว (ตัวเลขที่เหลืออีก 999,900 ก็ไม่ได้ใช้ แต่สร้างมาแล้วนะ)
ถ้าถามว่าโลจิคแบบนี้มันโอเคมัน มันก็ได้อยู่นะ โลจิคไม่ได้เปลี่ยนไป แต่ในแง่ประสิทธิภาพ performance ของโปรแกรมนี่ไม่ผ่านแน่นอน
เพราะแบบนี้ ถ้าเราสร้างฟังก์ชัน
getNumbers()
ด้วย Iterable จะทำให้ประหยัด performance มากๆ เพราะถึงจะกำหนดว่าต้องสร้างเลข 1-1,000,000 แต่เวลาเรียกใช้ ใช้แค่10ตัว มันก็จะสร้างไอเทมขึ้นมาแค่10เท่าที่ต้องใช้ ไม่ต้องสร้างจริงทั้งหนึ่งล้านตัว
sync* ฟังก์ชันที่หยุดกลางคันได้
นอกจากวิธีใช้ factory function Iterable.generate
สร้าง Iterable ขึ้นมา จริงๆ ยังมีอีกวิธีในการสร้างมัน นั่นคือการใช้ "Generator Function"
ถ้าเราสร้างฟังก์ชันที่ตอบ int
ธรรมดาก็จะได้โค้ดหน้าตาแบบนี้
int getNumber(){
return 1;
}
แต่ถ้าเราจะตอบกลับเป็น Iterable เราจะต้องเขียนฟังก์ชันแบบนี้
Iterable<int> getNumbers() sync* {
yield 1;
}
จุดสังเกตคือมี 2 อย่างที่เปลี่ยนไป นั่นคือ
- เราไม่ได้ใช้คีย์เวิร์ด
return
อีกต่อไป แต่ใช้yield
แทน - มีการกำหนดฟังก์ชันเป็น
sync*
ด้วยนะ (จะมาพร้อม yield เสมอ คือถ้าต้องการใช้คำสั่งyield
ในฟังก์ชันจะต้องประกาศให้ฟังก์ชันเป็นsync*
เสมอ)
ความแตกต่างระหว่าง yield
กับ return
คือทันทีที่ return ทำงาน ฟังก์ชันจะจบการทำงานทันที แต่สำหรับ yield นั้นจะยังไม่หยุดจนกว่าจะจบฟังก์ชัน เช่น
Iterable<int> getNumbers() sync* {
yield 1;
yield 2;
yield 3;
}
for(var number in getNumbers()){
print(number);
}
//output: 1 2 3
ฟังก์ชันทั่วๆ ไปจะเป็นฟังก์ชันประเภท
sync
แต่เราจะถือว่าไม่ต้องเติมคีย์เวิร์ดนี้ลงไปนะภาษาทั่วไป การประกาศฟังก์ชันเป็น Generator จะใช้
*
เติมไม่ก่อนก็หลังฟังก์ชัน เช่นfunction* f()
แต่ใน Dart จะต้องระบุคีย์เวิร์ดsync*
ลงไปด้วย เพราะเดี๋ยวในบทต่อๆ ไปเราจะเจอกับasync*
ด้วย!!
แต่เดี๋ยวขอเก็บไว้ก่อนนะ ไว้ค่อยกลับมาพูดกัน
ข้อดีอีกอย่างของ sync* function คือเราสามารถเขียนโค้ดแบบธรรมดาได้เลย เช่น
ex. ฟังก์ชันสำหรับสร้างลำดับเลขฟีโบนัคชี (อ่านเพิ่มเติมใน Fibonacci Number)
Iterable<int> fibonacci() sync* {
var first = 1;
var second = 1;
yield first;
yield second;
while(true){
var next = first + second;
yield next;
first = second;
second = next;
}
}
จะเห็นว่าเราจะเขียนโค้ดได้แบบไม่ต้องแคร์เลยว่าการวนลูปของเราจะต้องจบเมื่อไหร่ สามารถวนลูปไปได้เรื่อยๆ เลย แล้วอยากจะตอบค่ากลับก็ yield
ได้เลย
แต่ถ้าเขียน infinity iterable แบบนี้ เวลาใช้งานต้องระวัง!! เพราะจะต้องมีการกำหนดขนาดก่อนใช้เสมอ เช่น
for(var number in fibonacci().take(10)){
print(number);
}
แบบนี้เราใช้ฟังก์ชัน take
ในการดึงค่าฟีโบนัคชี 10 ตัวแรกเท่านั้นพอ
Nested sync* function ซ้อนกันก็ยังได้นะ
ถ้าเรามีฟังก์ชัน Generator 2 ตัว
Iterable<int> threeTime(int x) sync* {
for(var i=0; i<3; i++){
yield x;
}
}
Iterable<int> oneToThree() sync* {
for(var i=1; i<=3; i++){
for(var item in threeTime(i)){
yield item;
}
}
}
เราสร้างฟังก์ชันมา 2 ตัว threeTimeจะรับเลขไปหนึ่งตัว แล้วปริ๊นเลขตัวนั้น 3 ครั้ง, oneToThree วนลูปเรียกฟังก์ชันแรกอีก 3 ครั้ง
ถ้าเรารัน oneToThree ก็จะได้ output แบบนี้
10, 11, 12, 20, 21, 22, 30, 31, 32
แต่เราสามารถเขียนย่อได้อีก โดยใช้คำสั่ง yield*
Iterable<int> oneToThree() sync* {
for(var i=1; i<=3; i++){
yield* threeTime(i);
}
}
ก็คือ ถ้าจะใช้ sync เรียก sync จะต้องสั่งด้วย yield*
นั่นเอง
ก็จบลงไปแล้วกับซีรีส์แนะนำภาษา Dart เบื้องต้น
ต่อไปจะเป็นบทความเกี่ยวกับการใช้ Async ในภาษา Dart กัน ... แล้วหลังจากนั้นก็จะเป็นซีรีส์การนำภาษา Dart มาเขียน Application แบบ cross-platform ด้วยเฟรมเวิร์คที่ชื่อว่า Flutter