DEV Community

Ta for tamemo

Posted on • Edited on • Originally published at tamemo.com

5

Dart 105: มันวนซ้ำได้นะ! มาสร้าง Generator และใช้งาน Iterable กันเถอะ

ในบทก่อนๆ เรารู้จักตัวแปรประเภท List กันมาแล้ว แต่ในภาษา Dart (และภาษาสมัยใหม่อื่นๆ ด้วย) จะมีตัวแปรอีกชนิดนึงที่ "สามารถนำมาวนลูปได้" หรือ "สามารถ access ค่าเป็นลำดับเรียงต่อกันได้"

ตามปกติเราสามารถสร้างลิสต์ได้แบบนี้

List<int> items = [1, 2, 3, 4];
Enter fullscreen mode Exit fullscreen mode

แต่ถ้าเราลองเข้าไปดู source code ของคลาส List เราจะพบว่ามัน extends มาจากคลาสๆ หนึ่งที่มีชื่อว่า Iterable

แปลว่าเราสามารถสร้างลิสต์แบบนี้ได้

Iterable<int> items = [1, 2, 3, 4];
Enter fullscreen mode Exit fullscreen mode

ซึ่งทั้ง 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
Enter fullscreen mode Exit fullscreen mode

ได้ผลเหมือนกันเลยนี่นา? แล้วถ้ามันทำได้เหมือนกันแบบนี้มันจะมี 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]
Enter fullscreen mode Exit fullscreen mode

แต่ลองดูโค้ดต่อไปนี้

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;
});
Enter fullscreen mode Exit fullscreen mode

คำถามคือ...ถ้าเอาโค้ดแค่นี้เลยนะ มารัน -> จะได้ output ออกมาเป็นยังไง?

คำตอบคือ...

creating item from List  ━┓
creating item from List   ┣━ 3ครั้งจากListอย่างเดียว
creating item from List  ━┛
Enter fullscreen mode Exit fullscreen mode

นั่นคือฟังก์ชันสำหรับสร้างไอเทมของ 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 ไม่มีคำสั่ง [] นะ
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

เราจะพบว่าฟังก์ชันของ Iterable เริ่มทำงานแล้วล่ะนะ แต่ทำงานแค่ครั้งเดียว!?

ส่งนี้เรียกว่า "Lazy Evaluation" นั่นคือเราจะยังไม่สร้างไอเทมในทันทีที่ถูกสั่งให้สร้าง แต่ถ้าไหร่ก็ตามที่ถูกเรียกใช้ค่อยสร้างตอนนั้น

ถ้าจำลองสถาณการณ์ของโค้ดเมื่อกี้:

  • สำหรับ List: เปิดมา ไม่ต้องพูดพร่ำทำเพลง สร้างมันรวดเดียวเลย 3 ไอเทม (ยังไม่มีคนเรียกใช้เลยนะ สร้างไว้ก่อน)
  • สำหรับ Iterable: เปิดมา ยังไม่ทำอะไรทั้งนั้นแหละ (Lazy) แต่เมื่อมีการเรียกไอเทม elementAt(0) มันพบว่าไอเทมตัวที่ 0 น่ะยังไม่ถูกสร้างขึ้นมาเลย แต่เขาจะใช้งานแล้วนี่นา -> งั้นก็สร้างซะตอนนี้เลย!

แต่อ่านมาถึงตรงนี้ เราก็อาจจะงงๆ สงสัยกันว่า แล้วจะทำแบบนี้เพื่ออะไรกัน?

ลองดูตัวอย่างต่อไปครับ...

สร้าง Iterable ด้วยฟังก์ชัน Generator

สมมุติว่าเราจะสร้างฟังก์ชันสำหรับสร้างเลข 1-1,000,000 (หนึ่งถึงหนึ่งล้าน) ขึ้นมาหนึ่งตัวเพื่อเอาไปวนลูปอะไรสักอย่างนึง

for(var i in getNumbers()){
    print(i);
}
Enter fullscreen mode Exit fullscreen mode

ทั่วๆ ไปเราก็อาจจะนึกถึงฟังก์ชันที่สร้าง List คืนมา แบบนี้

List<int> getNumbers(){
    return [for(var i=1; i<=1000000; i++) i];
}
Enter fullscreen mode Exit fullscreen mode

ดังนั้นเวลาโค้ดทำงาน

  1. ฟังก์ชันสร้างตัวเลขทั้ง 1 ล้านตัวขึ้นมาก่อน
  2. ส่งกลับไปให้ลูปทำการวนปริ๊นค่าทั้ง 1 ล้านตัวนั่นออกมา

แต่ปัญหาจะเกิดขึ้น ถ้าลูปของเรามันสามารถหยุดกลางคันได้

for(var i in getNumbers()){
    print(i);
    if(i >= 10) break; //ถึง 10 เมื่อไหร่ก็จบลูปได้
}
Enter fullscreen mode Exit fullscreen mode

ทีนี้ โค้ดก็จะทำงานเปลี่ยนไป

  1. ฟังก์ชันสร้างตัวเลขทั้ง 1 ล้านตัวขึ้นมาก่อน
  2. ส่งกลับไปให้ลูปทำการวนปริ๊นค่า
  3. แต่คราวนี้ ปริ๊นไปแค่ 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;
}
Enter fullscreen mode Exit fullscreen mode

แต่ถ้าเราจะตอบกลับเป็น Iterable เราจะต้องเขียนฟังก์ชันแบบนี้

Iterable<int> getNumbers() sync* {
    yield 1;
}
Enter fullscreen mode Exit fullscreen mode

จุดสังเกตคือมี 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
Enter fullscreen mode Exit fullscreen mode

ฟังก์ชันทั่วๆ ไปจะเป็นฟังก์ชันประเภท 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

จะเห็นว่าเราจะเขียนโค้ดได้แบบไม่ต้องแคร์เลยว่าการวนลูปของเราจะต้องจบเมื่อไหร่ สามารถวนลูปไปได้เรื่อยๆ เลย แล้วอยากจะตอบค่ากลับก็ yield ได้เลย

แต่ถ้าเขียน infinity iterable แบบนี้ เวลาใช้งานต้องระวัง!! เพราะจะต้องมีการกำหนดขนาดก่อนใช้เสมอ เช่น

for(var number in fibonacci().take(10)){
    print(number);
}
Enter fullscreen mode Exit fullscreen mode

แบบนี้เราใช้ฟังก์ชัน 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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

เราสร้างฟังก์ชันมา 2 ตัว threeTimeจะรับเลขไปหนึ่งตัว แล้วปริ๊นเลขตัวนั้น 3 ครั้ง, oneToThree วนลูปเรียกฟังก์ชันแรกอีก 3 ครั้ง

ถ้าเรารัน oneToThree ก็จะได้ output แบบนี้

10, 11, 12, 20, 21, 22, 30, 31, 32
Enter fullscreen mode Exit fullscreen mode

แต่เราสามารถเขียนย่อได้อีก โดยใช้คำสั่ง yield*

Iterable<int> oneToThree() sync* {
    for(var i=1; i<=3; i++){
       yield* threeTime(i);
    }
}
Enter fullscreen mode Exit fullscreen mode

ก็คือ ถ้าจะใช้ sync* เรียก sync* จะต้องสั่งด้วย yield* นั่นเอง


ก็จบลงไปแล้วกับซีรีส์แนะนำภาษา Dart เบื้องต้น

ต่อไปจะเป็นบทความเกี่ยวกับการใช้ Async ในภาษา Dart กัน ... แล้วหลังจากนั้นก็จะเป็นซีรีส์การนำภาษา Dart มาเขียน Application แบบ cross-platform ด้วยเฟรมเวิร์คที่ชื่อว่า Flutter

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs