บทความทั้งหมด

This entry is part 3 of 5 in the series Dart-Series

Dart ถือเป็นภาษาสมัยใหม่ (ที่มีรูปแบบsyntaxแบบค่อนข้างเก่า🤔) ดังนั้นขาดไม่ได้เลยที่จะต้องมีฟีเจอร์ของ Object-Oriented Programming ใส่เข้ามาอย่างแน่นอน

สิ่งที่บทความนี้จะโฟกัสคือการใช้ OOP ในภาษา Dart เท่านั้น แต่ไม่ได้มาสอน OOP ในบทความนี้นะ

📢 Note: ภาษา Dart มีหลักๆ 2 เวอร์ชัน ในบทความนี้จะพูดถึง Dart version 2 เป็นหลัก

Class and Object

การสร้าง class และ object ใน Dart ถือว่าไม่ค่อยมีอะไรแปลกจากภาษาทั่วไป โดยเฉพาะถ้าใครเคยเขียน Java มาก่อนจะเรียนรู้ได้ไม่ยากเลย

class People {
    int id;
    String name;

    void hello(){
        print('Hi, my name is $name');
    }
}

ส่วน object เวลาสร้างนั้นเราสามารถละคีย์เวิร์ด new ก็ได้ แบบนี้

var people = new People();  // จะใส่ new ก็ได้
var people = People();      // ตามมาตราฐานจะละ 

people.name = 'Ana';
people.hello();

📢 Note: หากเราสร้างตัวแปรแบบ local scope ที่มีชื่อเดียวกับ properties (แต่สำหรับDartจะเรียกว่า field) เช่น

class People {
    int id;     // <-- [id1]
    void foo(){
        int id; // <-- [id2]
        id = 1; // id ตรงนี้จะหากถึง [id2]
    }
}

เนื่องจากตัวแปรประเภท local scope หรือตัวแปรระดับฟังก์ชันจะถูกหาก่อนตัวแปรภายนอกที่เป็นของคลาสเสมอ วิธีแก้คือใช้คีย์เวิร์ด this ซึ่งหมายถึงตัว object เข้ามาช่วย

class People {
    int id;     // <-- [id1]
    void foo(){
        int id; // <-- [id2]
        this.id = 1; // id ตรงนี้จะหากถึง [id1]
    }
}

private vs public

ในภาษา Dart ไม่มีคีย์เวิร์ด private หรือ public ที่เอาไว้กำหนดว่า field หรือ method นี้สามารถเรียกใช้งานจากภายนอกคลาสได้หรือเปล่า นั่นหมายความว่าค่าทุกค่าจะถูกกำหนดเป็น public ยกเว้นกรณีเดียวคือเราตั้งชื่อ field หรือ method นั้นให้ขึ้นต้นด้วย _ (underscore)

class MyClass {
    int _privateField;
    int publicField;

    void _privateMethod() => 0
    void publicMethod() => 0
}

แล้วตามคอนเซ็ปของ OOP นั้นจะมีหลักการ encapsolution ข้อมูลภายใน โดยหากเราต้องการจะ access เข้าไปใช้ข้อมูลพวกนั้นเราจะต้องสร้าง method ที่เรียกว่า getter และ setter ขึ้นมา แบบนี้

class People {
    String _name;

    void setName(String name) => _name = name;
    String getName() => _name;
}

อย่างไรก็ตามล่ะ ... การสร้างทั้ง field แล้วเอา method มาครบมันอีกครั้งเป็นการเขียนสไตล์ของภาษา OOP ยุคเก่านิดนึง (เช่น Java 😜)

สำหรับใน Dart จะมีสิ่งที่เรียกว่า...

Getter Setter fields

ทั้งสองตัวนี้คล้ายๆ กัน "getter setter method" จะต่างกันตรงที่ Getter Setter field ถือว่าเป็น field (ก็ตามชื่อมันละนะ)

field พวกนี้จะถือเป็นค่าพิเศษที่สร้างในรูปแบบของ method แต่ยังถือว่าเป็น field ด้วยคียเวิร์ด get และ set

class People {
    String _name;

    // Getter
    int get name => _name;

    // Setter
    void set name(String name) => _name = name;
    // หรือแบบย่อ คือไม่ต้องใส่ void (Recommend)
    set name(String name) => _name = name;
}

ข้อสังเกตคือการสร้าง Getter, Setter จะมีรูปแบบเหมือน method มากกว่า field แต่ในส่วนของ Getter นั้นจะถูกบังคับให้ไม่เขียน parameter หรือ () ที่ตามหลัง ส่วน Setter นั้นมีข้อแนะนำว่าไม่ต้องใส่ return-type void

และเนื่องจากมันมีรูปแบบการเขียนแบบฟังก์ชัน เราสามารถเพิ่มโค้ดอย่างอื่นลงไปอีกก็ได้ เช่น

class People {
    String _name;

    int get name {
        print('call get name with value = $_name');
        return _name;
    }

    set name(String name){
        print('call set name with value = $name');
        _name = name;
    }
}

เมื่อทำแบบนี้ทุกครั้งเวลาเราเซ็ตค่าหรือขอค่า people.name ก็จะมีการทำงาน print เกิดขึ้นด้วย ซึ่งสามารถเอาไปประยุกต์ใช้งานกับอย่างอื่นได้อีกมากมายนะ

Constructor

คอนสตรักเตอร์คือการกำหนดค่าเริ่มต้น ตั้งแต่ครั้งแรกที่สร้าง object ขึ้นมาเลย ซึ่งในภาษา Dart สามารถทำได้หลายวิธีมากๆ

แต่แบบมาตราฐานจะเหมือนกับภาษา Java เป๊ะๆ นั่นคือสร้าง method ที่ไม่มี return-type และชื่อเหมือนกับชื่อคลาส

Initializing Field

class People {
    int _id;
    String _name;

    People(int id, String name){
        _id = id;
        _name = name;
    }
}

var people = People(1, 'Ana');

แต่ในกรณีแบบนี้ ถ้าคลาสเรามี field หลายตัวมากๆ เราจำเป็นต้องเขียนการกำหนดค่าเยอะมาก ดังนั้นDartเลยมี syntax แบบ่อซึ่งสั้นเป็นพิเศษ ซึ่งจะแปลงโค้ดจากข้างบนให้เหลือแค่

class People {
    int _id;
    String _name;

    People(this._id, this._name);
}

var people = People(1, 'Ana');

นั่นคือบอกไปเลยว่า ค่าที่ได้มาให้เอาไปใส่ใน field ที่กำหนดตรงๆ เลย (ต้องมี this นำหน้าตัวแปร)

หรือจะเอามาผสมกับ Named Parameter ก็ได้ เช่น

class People {
    int id;
    String name;

    People({this.id, this.name});
}

var people = People(id: 1, : 'Ana');

⚠️ Warning!: สำหรับ Named Paremeter นั้นไม่สามารถใช้กับ field แบบ private ได้ (พวกตัวแปรที่ขึ้นด้วย _) ในเคสนี้อาจจะต้องเขียนยาวหน่อยหากต้องการสร้าง field แบบ private

class People {
    int _id;
    String _name;

    People({int id, String name}){
        _id = id;
        _name = name;
    }
}

var people = People(id: 1, : 'Ana');

แต่! ก็ยังมีปัญหาอยู่ดี ถ้าเราประกาศตัวแปรภายนอกเป็น final!!

Initializer

ตามหลักการเขียนโค้ดที่ดีสไตล์ Functional Programming (ใครยังไม่รู้จักว่า FP คืออะไร, ต่างกับ OOP รึเปล่า ไปอ่านกันได้ที่นี่) เราควรจะใช้ตัวแปรแบบ immutable หรือเปลี่ยนแปลงค่าไม่ได้ เพื่อป้องกันการมี state ของโค้ดเยอะเกินไปจนเกิดบั๊ก!

เราก็เลยแก้โค้ดด้านบนอีกรอบเป็นแบบนี้

class People {
    final int _id;
    final String _name;

    People({int id, String name}){
        _id = id;
        _name = name;
    }
}

และผลที่ได้ก็คือ Compile Error!! ด้วยสาเหตุว่า All final variables must be initialized, but _id and _name are not

ก็งงกันไปสิ!? ประกาศ final แถมกำหนดค่าใน constructor ด้วยแต่ทำไมบอกว่าไม่ได้กำหนดค่า

"คำตอบคือ construction state ของ Dart นั้นจบก่อนจะทำงาน body ของ constructor"

เพื่อความเข้าใจลองดูนี่

class People {
    final int _id;
    final String _name;

    // constructor header
    People({int id, String name})    
    // constructor body
    {
        _id = id;
        _name = name;
    }
}

Dart ถือว่าถ้าต้องมีการ initialize หรือประกาศค่าที่จำเป็น (เช่นในกรณีของเรา คือการกำหนดค่าให้ตัวแปร final) จะต้องทำให้เสร็จในส่วนของ header ก่อนที่ body จะเริ่มทำงาน

วิธีการแก้คือ ภาษาDartอนุญาตให้เรากำหนดค่าได้ในส่วน header เลย แบบนี้

class People {
    final int _id;
    final String _name;

    People({int id, String name}) :
        this._id = id,
        this._name = name ;

}

หลัง header ให้เราใส่ : ลงไป หลังจากนั้นจะเป็น statement ที่อนุญาตให้เรากำหนอค่า initialize ตัวแปรยังไงก็ได้ (และใช่! ถ้าคุณคิดว่า syntax มันแปลกๆ ก็อาจจะต้องลองใช้ซักพักกว่าจะชิน)

Named constructors

ภาษา Dart ไม่สามารถทำ Method Overloading หรือการสร้างเมธอดหลายตัวแต่ต่าง parameter กันได้ (ตัวอย่างภาษาที่ทำได้คือ Java) เพราะDartมี Optional Parameter ให้ใช้งานแทน

แต่สำหรับคอนสตรักเตอร์ถ้าเราต้องการสร้าง constructor หลายๆ ตัวเอาไว้สำหรับการสร้างวัตถุหลายแบบ เราสามารถสร้างสิ่งที่เรียกว่า Named constructors ขึ้นมาใช้งานได้

class Point {
    int x, y;

    Point(this.x, this.y);

    Point.origin() {
        x = 0;
        y = 0;
    }
}

var point1 = Point(10, 20);
var point0 = Point.origin();

เช่นตัวอย่างนี้เราสร้าง origin ขึ้นมาเป็น constructor อีกหนึ่งตัว เวลาจะสร้าง object ก็สามารถเลือกได้ว่าจะใช้ default constructor แบบธรรมดาหรือใช้ origin ที่เราสร้างขึ้นมาเองก็ได้

Factory constructors

นอกจากเราจะสร้าง object จากการใช้ constructorแบบธรรมดาได้แล้ว ในภาษานี้ยังมีวิธีสร้าง object อีกแบบนั่นคือการใช้ factory ซึ่งใช้หลักการเดียวกับ Factory Pattern นั่นคือทำยังไงก็ได้ให้ตอบ object กลับไป

class Logger{
    String name;

    factory Logger(){
        return Logger.withName('MyLogger');
    }

    Logger.withName(this.name);

    void log(String message){
        print('LOG: $message');
    }
}

var logger1 = Logger();
var logger2 = Logger.withName('logger-2');

จะเห็นว่า factory นั้นทำงานเหมือนกับ constructor ทุกอย่างนั่นคือกำหนดและสร้าง object แต่ข้อแตกต่างคือ constructor จะกำหนดค่าให้ตัวเอง (เพราะตัว constructor ถือเป็นเมธอดตัวหนึ่งของ object อยู่แล้ว) แต่ว่า factory จะเป็นการสร้าง object ชิ้นใหม่ขึ้นมาเลย

⚠️ Warning!: สำหรับ constructor นั้นสามารถเรียกใช้ this ได้เพราะถือว่าเป็นส่วนหนึ่งของ object อยู่แล้ว แต่ factory นั้นจะไม่สามารถเรียกใช้ this ได้เลย

อาจจะประยุกต์เอาไปใช้กับ Singleton Pattern ก็ได้ มาดูตัวอย่างกัน

//logger.dart
class Logger{
    void log(String message){
        print('LOG: $message');
    }
}

//module1.dart
var logger = Logger();
logger.log('log from module 1');

//module2.dart
var logger = Logger();
logger.log('log from module 2');

สมมุติให้เรามีคลาส Logger (หน้าที่คือเอาไว้ปริ๊น log ข้อมูลนั่นแหละนะ) ปัญหาคือถ้าเราต้องการเอา Logger ไปใช้หลายไฟล์เราจะต้องสร้าง object ใหม่ทุกรอบ

ในกรณีนี้ ถ้าเราต้องการให้ Logger มี instance ได้แค่ตัวเดียวเท่านั้น เราอาจจะนำ Single Pattern มาใช้งาน แบบนี้

//logger.dart
class Logger{
    static Logger _instance;
    static Logger getInstance() => _instance ??= Logger();

    void log(String message){
        print('LOG: $message');
    }
}

//module1.dart
var logger = Logger.getInstance();
logger.log('log from module 1');

//module2.dart
var logger = Logger.getInstance();
logger.log('log from module 2');

ซึ่งก็สามารถทำได้ ไม่ผิดอะไร แต่เราสามารถเปลี่ยนไปใช้ factory แทนได้แบบนี้

//logger.dart
class Logger{
    static Logger _instance;
    factory Logger() => _instance ??= Logger();

    void log(String message){
        print('LOG: $message');
    }
}

//module1.dart
var logger = Logger();
logger.log('log from module 1');

//module2.dart
var logger = Logger();
logger.log('log from module 2');

สังเกตว่าคำสั่ง factory นั้นจะทำงานเหมือนกับ constructor แบบในกรณีนี้ ในmodule1และmodule2นั้นจะไม่รู้เลยว่าการที่เขาสร้าง object ขึ้นมานั้นจริงๆ ไม่ได้เป็นการสร้างใหม่ แต่เป็นการเรียกใช้จาก shared instance นั่นเอง

equals และการทำ Operator Overriding

สำหรับ object ในภาษาโปรแกรมส่วนใหญ่จะเก็บค่าแบบ reference (คือเก็บเป็น pointer ไม่ได้เก็บเป็น value) การจะเช็กว่า object เท่ากันไหมเลยไม่สามารถใช้ == ได้

เช่นถ้าเป็นภาษา Java ก็จะต้องใช้ equals แทน เช่น

String string1 = "Centrillion Tech";
String string2 = "Centrillion Tech";
string1 == string2 //maybe true or false
string1.equals(string2) //true

สำหรับภาษา Dart นั้นจะใช้การสร้าง equals ขึ้นมาก็ได้ แต่โดยมาตราฐานของภาษาจะใช้การ override แบบพิเศษ นั่นคือ "Operator Overriding" แทน โดยใช้คีย์เวิร์ด operator นำหน้า

class People {
    int id;
    String name;

    //แบบสร้าง equals
    bool equals(dynamic other) {
        if (other is! Person) return false;
        Person person = other;
        return (person.id == id &&
            person.name == name);
    } 

    //แบบ override ==
    @override
    bool operator ==(dynamic other) {
        if (other is! Person) return false;
        Person person = other;
        return (person.id == id &&
            person.name == name);
    } 
}

ซึ่งพอเรา override == ไปแล้วเราสามารถจับ object มาเช็กว่ามันเท่ากันรึเปล่าโดยใช้ == ตรงๆ ได้เลย

⚠️ Warning!: ตามหลักการของภาษาที่ใช้โครงสร้าง OOP แนวเดียวกับภาษา Java ทุกครั้งที่เราเขียน equals เราจะโดยบังคับให้เขียน hashCode ด้วยเสมอ ไม่งั้นเวลา object ไปใช้งานใน HashMap จะเกิดปัญกาขึ้นได้

class People {
    int id;
    String name;

    @override
    int get hashCode {
        int result = 17;
        result = 37 * result + id.hashCode;
        result = 37 * result + name.hashCode;
        return result;
    }

    //แบบ override ==
    @override
    bool operator ==(dynamic other) {
        if (other is! Person) return false;
        Person person = other;
        return (person.id == id &&
            person.name == name);
    } 
}

เอาจริงๆ คือถ้าต้องมาเขียนทั้ง equals และ hashCode เองทั้งหมดทุกคลาสน่ะ ไม่ไหวแน่ๆ ... ดังนั้นเราขอแนะนำให้คุณ generate code ด้วยฟีเจอร์ของ IDE แทนจะดีกว่านะ


เนื้อหา Class-Object ยังมีอีกมาก แต่ในเนื้อหาบทความนี้เลือกมาเฉพาะที่เห็นว่าแตกต่างจากภาษาอื่นๆ ในบทต่อไปเราจะมาพูดกันเรื่องการทำ Inheritance และ Abstract Class กัน

Series Navigation<< Dart 102: โครงสร้างข้อมูลพื้นฐาน List และ MapDart 104: การสืบทอดคลาส Inheritance และ Mixin >>

ในบทความที่เรา เราแนะนำการใช้งาน List ไปแล้ว ในบทความนี้จะมาพูดกันต่อถึงเมธอดที่ควรรู้สำหรับการใช้งาน List ให้ดี/ง่ายยิ่งขึ้นกัน

แต่ก่อนอื่น เราจะอธิบายคำศัพท์ที่จะใช้ในบทความนี้ซะก่อน

Immutable vs Mutable

เมธอดของ List แต่ละตัวมีรูปแบบการทำงานที่ไม่เหมือนกัน โดยจะแบ่งเป็น

  • Immutable - คำสั่งประเภทนี้จะไม่เปลี่ยนแปลง List (แต่ให้ผลลัพธ์ผ่านการ return)
  • Mutable - คำสั่งประเภทนี้จะเปลี่ยนค่าของ List ตรงๆ

ซึ่งในบทความนี้จะใช้สัญลักษณ์แบบนี้ 😤 Immutable | 😱 Mutable

Result Type

เมธอดแต่ละตัวมีการตอบผลลัพธ์ (return) กลับมาไม่เหมือนกัน แต่แบ่งได้เป็นประมาณนี้

  • List - ตอบกลับมาเป็น List เหมือนเดิม
  • Iterable - ตอบกลับเป็น Iterable (เป็นโครงสร้างแบบ abstract คือสามารถเอามาวนลูปเพื่อขอค่าทีละตัวได้ ซึ่งส่วนใหญ่ทำงานแบบ lazy นั่นคือถ้ายังไม่มีการเรียกใช้จะไม่ทำงาน ต่างจาก List ที่ทำงานทันที)
  • Scala - ตอบกลับมาเป็นค่าชนิดอื่นเลย เช่น int, bool, String
  • void - ไม่ตอบค่าอะไรกลับมาเลย

ซึ่งในบทความนี้จะใช้สัญลักษณ์แบบนี้ 🍇 List | 🍒 Iterable | 🍐 Scala | 🥚 void

หมวดการสร้าง

generate()

😤 Immutable | 🍇 List

ใช้สำหรับการสร้างลิสต์ขึ้นมาโดยกำหนดจำนวน item ที่ต้องการ แล้วเราต้องกำหนด function สำหรับสร้าง item ขึ้นมา โดยสิ่งที่เราจะได้คือ index

List<int> list = List<int>.generate(10, (i) => i + 1);
// List [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

List Comprehension

😤 Immutable | 🍇 List

ซึ่งแทนที่จะใช้ .generate() เราสามารถสร้างลิสต์โดยใช้การวน for แทนก็ได้

List<int> list = [ for(var i=0; i<10; i++) i + 1 ];
// List [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

หมวดการคัดลอก/เพิ่ม/ลบ

form(), of()

😤 Immutable | 🍇 List

เป็น 2 คำสั่งที่ใช้สร้างลิสต์จากลิสต์อีกตัวหนึ่ง ซึ่งมีข้อแตกต่างกันเล็กน้อยนั่นคือถ้าเราสั่งด้วย from ผลที่ได้จะเป็นไทป์แบบ dynamic แต่ถ้าใช้ of ผลที่ได้จะได้ไทป์ของลิสต์ต้นฉบับมาด้วย

var list1 = new List.from(<int>[1, 2, 3]); 
// list1 is List<dynamic>
var list2 = new List.of(<int>[1, 2, 3]); 
// list2 is List<int>

List<String> list3 = new List.from(<int>[1, 2, 3]); 
// Ok until runtime.
List<String> list4 = new List.of(<int>[1, 2, 3]); 
// Compile Time Error!

add(), addAll()

😱 Mutable | 🥚 void

ใช้เพื่อเพิ่ม item ลงไปในลิสต์ โดยใช้ add สำหรับเพิ่ม item แบบทีละตัวและใช้ addAll สำหรับการเพิ่มหลายๆ ตัวในรูปแบบของ List หรือ Iterable

List<int> list1 = [1, 2, 3];
list1.add(4);
// list1 is [1, 2, 3, 4]

List<int> list2 = [1, 2, 3];
list2.addAll([4, 5]);
// list2 is [1, 2, 3, 4, 5]

followedBy()

😤 Immutable | 🍒 Iterable

เป็นการสร้างลิสต์ตัวใหม่โดยเอาลิสต์อีกตัวมาต่อกัน แต่จะไม่ใช่การ add หรือ addAll ที่เพิ่ม item เข้าไปในลิสต์ แต่เป็นการสร้างลิสต์ตัวใหม่ออกมาแทน

List<int> list1 = [1, 2, 3];
Iterable<int> list2 = list1.followedBy([4, 5]);

//list1 is List [1, 2, 3]
//list2 is Iterable (1, 2, 3, 4, 5)

แต่ใน Dart การต่อ List สามารถใช้การ + หรือ ... (spread operator) แทนได้

List<int> list1 = [1, 2, 3];
List<int> list2 = list1 + [4, 5];
//list1 is [1, 2, 3]
//list2 is [1, 2, 3, 4, 5]

List<int> list3 = [1, 2, 3];
List<int> list4 = [...list1, ...[4, 5], 6];
//list3: [1, 2, 3]
//list4: [1, 2, 3, 4, 5, 6]

หมวด map/filter/reduce

forEach()

😤 Immutable | 🥚 void

ใช้ในการวนลูป item ทุกตัวในลิสต์ในรูปแบบการสร้างฟังก์ชันขึ้นมา (ซึ่งมาผลเทียบกับการวน for แบบธรรมดานั่นแหละ)

var list = [1, 2, 3];
list.forEach((item) => print(item));

// เขียนแบบใช้ลูป
var list = [1, 2, 3];
for(var item in list){
    print(item);
}

map()

😤 Immutable| 🍒 Iterable

ใช้ในการแปลง item ทุกค่าในลิสต์ให้กลายเป็นอีกค่าหนึ่งด้วยวิธีการเดียวกันทั้งหมด โดยสร้างฟังก์ชันขึ้นมา

เช่น ในตัวอย่างข้างล่าง เราต้องการคูณเลขทุกตัวในลิสต์ด้วย 10 ก็ให้สร้าง (x) => x * 10 ฟังก์ชันที่รับตัวเลขเข้าไปหนึ่งตัวแล้วตอบค่านั้นเอาไปคูณด้วย 10 กลับมา

List<int> num1 = [1, 2, 3, 4, 5];

Iterable<int> num2 = num1.map((x) => x * 10);

List<int> num3 = num1.map((x) => x * 10).toList();

// num2 is Iterable (10, 20, 30, 40, 50)
// num3 is List [10, 20, 30, 40, 50]

// เขียนแบบใช้ลูป
List<int> num2 = <int>[];
for(var x in num1){
    num2.add(x * 10);
}

ข้อควรระวังคือ map รีเทิร์นค่ากลับมาเป็น Iterable ไม่ใช่ List ดังนั้นถ้าเราต้องการผลลัพธ์เป็นลิสต์เหมือนเดิมจะต้องสั่ง toList() อีกที

where() (หรือ filter)

😤 Immutable | 🍒 Iterable

ฟังก์ชัน where หรือในภาษาอื่นมักจะเรียกว่า filter มีไว้ทำการเลือกเฉพาะ item ที่ตรงกับเงื่อนไข โดยจะต้องสร้างฟังก์ชันประเภท predicate หรือฟังก์ชันที่ตอบค่าเป็น bool (true = เลือกไอเทมนี้, false = ไม่เอาไอเทมนี้)

เช่นหากเราต้องการเลือกเฉพาะตัวเลขที่เป็นเลขคู่ ก็ต้องสร้าง predicate function แบบนี้ (x) => x % 2 == 0

List<int> num1 = [1, 2, 3, 4, 5];

Iterable<int> num2 = num1.where((x) => x % 2 == 0);

List<int> num3 = num1.where((x) => x % 2 == 0).toList();

// num2 is Iterable (2, 4)
// num3 is List [2, 4]

// เขียนแบบใช้ลูป
var num1 = [1, 2, 3, 4, 5];
var num2 = <int>[];
for(var x in num1){
    if(x % 2 == 0){
        num2.add(x);
    }
}

เช่นเดียวกับ map คือ where รีเทิร์นค่ากลับเป็น Iterable

ส่วนกลับของ where คือ removeWhere ซึ่งจะลบตัวที่ตรงกับ predicate ทิ้งออกไปแทน

ข้อควรระวังคือ removeWhere เป็น Mutable นะ!

firstWhere() และ singleWhere()

😤 Immutable | 🍐 Scala

นอกจากนี้ ภาษาDartยังมีฟังก์ชันสำหรับเลือกค่าที่ตรงเงื่อนไขอีก 2 ตัวคือ firstWhere และ singleWhere ซึ่งจะตอบตัวเลขที่ตรงเงื่อนไขกลับมาแค่ตัวเดียวเท่านั้น

ข้อแตกต่างระหว่าง firstWhere และ singleWhere คือการใช้ singleWhere จะต้องมีค่านั้นเพียงตัวเดียวเท่านั้น ถ้ามีตัวที่ตรงกับเงื่อนไขหลายตัวจะเจอ Error: Bad state: Too many elements

List<int> list = [1, 2, 3, 4, 5];
int num = num1.firstWhere(
    (x) => x % 2 == 0,
    orElse: () => null
);

List<int> list = [1, 2, 3, 4, 5];
int num = num1.singleWhere(
    (x) => x % 2 == 0,
    orElse: () => null
);

reduce(), fold()

😤 Immutable | 🍐 Scala
reduce จะใช้ในกรณีที่เราต้องการรวม item ทุกตัวในลิสต์ให้ออกมาเป็นค่าใหม่ค่าเดียวเท่านั้นด้วยวิธีการอะไรบางอย่าง ซึ่งเราต้องเขียนขึ้นมาด้วยฟังก์ชันที่เรียกว่า combine

เช่นหากเราต้องการเอา item ทุกตัวในลิสต์มา + กัน ให้เขียน combine function ว่า (x, y) => x + y นั่นคือหากเรามีตัวเลขสองตัว ให้เอาสองตัวนั้นมาบวกกัน

List<int> list = [1, 2, 3, 4, 5];
int num = list.reduce((x, y) => x + y);

// num is 1 + 2 + 3 + 4 + 5
// num is 15

// เขียนแบบใช้ลูป
int num = list.first;
for(var x in list.skip(1)){
    num += x;
}

แต่สังเกตดูว่ามันจะเท่ากับการเขียนลูป โดย accumulator (หมายถึงตัวแปรที่เอาไว้สะสมค่า ในกรณีนี้คือ num) จะต้องเริ่มต้นที่ item ตัวแรกเป็นตัวตั้ง แต่ถ้าเราต้องการกำหนดค่าเริ่มต้นเอง เราสามารถเขียนแบบนี้ได้

List<int> list = [1, 2, 3, 4, 5];
int num = [100, ...list].reduce((x, y) => x + y);
// num is 100+ 1 + 2 + 3 + 4 + 5

แต่ก็อย่างที่เห็นค่ามันไม่สวยเลย! ในเคสนี้เราสามารถเปลี่ยนจาก reduce ไปใช้ fold แทนได้ แบบนี้

List<int> list = [1, 2, 3, 4, 5];
int num = list.fold(100, (x, y) => x + y);
// num is 100+ 1 + 2 + 3 + 4 + 5

ลองมาดูตัวอย่างกันอีก

เช่นเราต้องการหาค่าที่มากที่สุดในลิสต์เราอาจจะเขียน combine แบบนี้

import 'dart:math';

List<int> list = [1, 2, 3, 4, 5];
int maxNum = list.reduce((x, y) => max(x, y));

และไม่จำเป็นจะต้องใช้กับลิสต์ของ int เท่านั้น สามารถเอาไปแอพพลายใช้งานกับ String ก็ยังได้ เช่นต้องการจะเชื่อมสตริง (concat) เข้าด้วยกันด้วย - ก็เขียนแบบนี้ได้

List<String> list = ['A', 'B', 'C'];
String str = list.reduce((x, y) => '$x-$y');
// str is "A-B-C"

หมวด Utility

length, isEmpty

🍐 Scala

length ใช้เพื่อเช็กว่าตอนนี้ในลิสต์มี item อยู่กี่ตัว ส่วน isEmpty ใช้เช็กว่าตอนนี้ item ในลิสต์ไม่มีเลยใช่หรือไม่

List<int> list = [1, 2, 3, 4, 5];
print(list.length);     // 5
print(list.isEmpty);    // false

sublist(), getRange()

😤 Immutable |
sublist() = 🍇 List, getRange() = 🍒 Iterable

ใช้สำหรับตัดลิสต์ย่อยออกมาโดยเราต้องกำหนด index ที่เริ่มและจบ (start, end) ข้อควรระวังคือ end จะไม่ถูกรวมอยู่ในคำตอบด้วย

List<int> list = [1, 2, 3, 4, 5];
list.sublist(1, 3);     // [2, 3]
list.getRange(1, 3);    // (2, 3)

first, last

🍐 Scala

ใช้ขอ item ตัวแรกสุดและตัวท้ายสุดของลิสต์ มีค่าเท่ากับ list[0] และ list[list.length - 1]

List<int> list = [1, 2, 3, 4, 5];
print(list.first);      // 1
print(list.last);       // 5

take(), skip(), takeWhile(), skipWhile()

😤 Immutable | 🍒 Iterable

คล้ายกับ getRange แต่เป็นการเลือก/ข้ามจาก head หรือหัวแถวของลิสต์แทน

สำหรับ takeWhile กับ skipWhile จะเป็นการสร้างฟังก์ชันเงื่อนไขขึ้นมาเช็กแทน

List<int> list = [1, 2, 3, 4, 5];

li.take(2)  // (1, 2)
li.skip(2)  // (3, 4, 5)

li.takeWhile((x) => x < 3)  // (1, 2)
li.skipWhile((x) => x < 3)  // (3, 4, 5)

contains(), any(), every()

🍐 Scala

ใช้สำหรับหาว่ามี item ที่ต้องการหรือไม่

สำหรับ any (ข้อแค่ตรงเงื่อนไขอย่างน้อย 1 ตัว) และ every (ไอเทมทุกตัวในลิสต์จะต้องตรงเงื่อนไขทั้งหมด) จะต้องสร้างฟังก์ชัน test ขึ้นมา

List<int> list = [1, 2, 3, 4, 5];

list.contains(3);               // true
list.any((x) => x % 2 == 0);    // true
list.every((x) => x % 2 == 0);  // false

sort()

😱 Mutable | 🥚 void

ใช้สำหรับเรียงข้อมูลในลิสต์

List<int> list = [4, 2, 1, 5, 3];
list.sort();

// list is [1, 2, 3, 4, 5]

ในกรณีที่ต้องการเรียงแบบอื่นเช่น descending list หรือลิสต์ที่เรียงจาก มาก -> น้อย แทนหรือต้องการ sort ลิสต์ของ object ที่ไม่ใช่ Primitive Type เราจะต้องสร้างฟังก์ชัน comparator ขึ้นมา โดยให้ยึดหลักการว่า ตัวแรก-ตัวที่สองเสมอ เช่น

List<int> list = [4, 2, 1, 5, 3];
list.sort((first, second) => -(first-second));
// นำมาลบกัน แต่ใส่ค่า - เอาไว้เพื่อให้เรียงจากมากไปน้อยแทน

class Score{
    int point;
    People(this.point);
}
var list = [Score(20), Score(5), Score(10))];
list.sort((first, second) => first.point - second.point);

shuffle()

😱 Mutable | 🥚 void

เมื่อมีการสั่งให้เรียงข้อมูลด้วย sort แล้วก็ต้องมีคำสั่งที่เป็นส่วนกลับกัน นั่นคือ shuffle หรือการสลับตำแหน่งลิสต์แบบ random (คล้ายสับกองการ์ดให้เรียงแบบมั่วๆ นั่นแหละ)

ซึ่งการสั่ง shuffle แต่ละครั้งก็จะให้ผลออกมาไม่เหมือนกัน เพราะมัน random นะ

List<int> list = [1, 2, 3, 4, 5];

list.shuffle();
// list is [4, 2, 1, 5, 3]

list.shuffle(); 
// call again!
// list is [3, 4, 5, 2, 1]

แต่ถ้าอยากให้ผลการสลับออกมาเหมือนเดิมเราสามารถใส่ seed ซึ่งเป็นอ็อบเจก Random ลงไปได้ โดยถ้า seed เป็นเลขเดิมจะได้ผลลัพธ์ออกมาเหมือนเดิม

import 'dart:math';

List<int> list = [1, 2, 3, 4, 5];

list.shuffle(Random(100));
// list is [2, 5, 1, 3, 4]

list.shuffle(Random(100));
// call again!
// list is [2, 5, 1, 3, 4] same result!

reversed

😤 Immutable | 🍒 Iterable

คำสั่งสำหรับสร้างลิสต์ตัวใหม่ที่กลับหัว item ทั้งหมดแทน

List<int> list = [1, 2, 3, 4, 5];
var re = list.reversed;

// list is List [1, 2, 3, 4, 5]
// re is Iterable [5, 4, 3, 2, 1]
This entry is part 2 of 5 in the series Dart-Series

โครงสร้างข้อมูลหรือ Data Structure เป็นสิ่งที่ต้องมีในทุกภาษาโปรแกรม เพราะในการเขียนโปรแกรมจริงๆ การที่เรามีแค่ variable ไม่พอจะที่จัดการข้อมูลที่มากมายและซับซ้อนในแอพของเรา

Data Structure: List และ Map

โครงสร้างข้อมูลใน Dart มี 2 ประเภทหลักๆ คือ

  • List: เก็บข้อมูลแบบ linear, มีค่าเท่ากับ array ในบางภาษา
  • Map: เก็บข้อมูลแบบ key-value, มีค่าเท่ากับ dictionary หรือ hash-table ในบางภาษา

List

การสร้างลิสต์ของข้อมูลขึ้นมาทำได้โดยการประกาศตัวแปรชนิด List

List ใน Dart ไม่ใช่โครงสร้างแบบ fixed-length นั่นคือสามารถเพิ่ม/ลบ element ในลิสต์ได้ผ่านคำสั่ง เช่น add(), remove(), removeAt()

List myList = [10, 20, 30, 40];

ซึ่งจะได้ค่าดังนี้

print(myList);      // [10, 20, 30, 40]

print(myList[0]);   // 10
print(myList[1]);   // 20
print(myList[2]);   // 30
print(myList[3]);   // 40

แต่ก็ตามสไตล์ภาษามี type ทั่วๆ ไปคือการสร้าง List โดยที่ไม่ระบุอะไรนั้นมีความเสี่ยงอย่างมาก! เพราะ compiler จะไม่รู้ว่า element แต่ละตัวข้างในเป็นตัวแปรชนิดอะไร จึงช่วยเราเช็กค่าไม่ได้เลย --> ต้องไปเสี่ยงดวงตอน runtime กันเอง

List myList = [1, 'hi', true];

int x    = myList[0];  // ok!
String s = myList[1];  // ok!
bool b   = myList[2];  // ok!

int y    = myList[1];  // Runtime Error! TypeError: String is not a subtype of type int

ในภาษา Dart มีฟีเจอร์ที่เรียกว่า Type Interface หรือ Generic ให้ใช้ นั่นคือการส่ง type เข้าไปใน class หรือ function ได้โดยใช้สัญลักษณ์ ``

List<int> myList = [10, 20, 30, 40];  
// แบบนี้ ok!

var myList = <int>[10, 20, 30, 40];  
// แบบนี้ก็ ok!

//แต่...

List<int> myList = [10, 20, 'hi'];
//Error: A value of type 'String' can't be assigned to a variable of type 'int'.
//  List<int> myList = [10, 20, 'hi'];
//                               ^

การประกาศ List โดยไม่มี Generic จะถูกมองว่าเป็น List นั่นคืออนุญาตให้เก็บตัวแปรชนิดอะไรก็ได้

loop

การวนลูปสามารถทำได้ด้วยวิธีใช้ตัว counter แบบดั้งเดิม และนับขนาดของลิสต์ด้วย .length

List<int> numbers = [10, 20, 30, 40];
for(var i = 0; i < numbers.length; i++){
    print(numbers[i]);
}

หรือใช้ for-in

List<int> numbers = [10, 20, 30, 40];
for(var number in numbers){
    print(number);
}

การเพิ่มหรือลบ item ออกจาก List

การเพิ่ม item ลงใน List จะใช้คำสั่ง

  • add - ใช้สำหรับเพิ่ม item ที่ตำแหน่งท้ายสุดของลิสต์
  • insert - ใช้สำหรับเพิ่ม item ลงที่ตำแหน่งตรงกลางลิสต์
var list = ['A', 'B', 'C'];

list.add('D');          // เพิ่ม D เข้าไปท้ายสุดของลิสต์
print(list);            // [A, B, C, D]

list.insert(0, 'E');    // เพิ่ม E เข้าไปที่ตำแหน่งที่ 0
print(list);            // [E, A, B, C, D]

list.insert(3, 'F');    // เพิ่ม F เข้าไปที่ตำแหน่งที่ 3
print(list);            // [E, A, B, F, C, D]

list.insert(6, 'G');    // เพิ่ม G เข้าไปที่ตำแหน่งที่ 6
print(list);            // [E, A, B, F, C, D, G]

ในภาษา Dart ไม่มีคำสั่ง pop ซึ่งเอาไว้เพิ่ม item เข้าไปที่ตำแหน่งแรกสุดของลิสต์ แต่เราสามารถใช้การ insert ในตำแหน่งที่ 0 แทนได้

แต่ถ้า item ที่เราต้องการเพิ่มลงไปเป็น List (หรือ Iterable) เราจะใช้คำสั่ง addAll และ insertAll แทน

var list = ['A'];

list.addAll(['B', 'C']);
print(list);            // [A, B, C]

list.insertAll(2, ['D', 'E']);
print(list);            // [A, B, D, E, C]

ส่วนการลบ item ออกจากลิสต์จะใช้คำสั่ง remove

List<String> list;

list = ['A', 'B', 'C', 'D', 'E'];
list.remove('A');
print(list);    // [B, C, D, E]

list = ['A', 'B', 'C', 'D', 'E'];
list.removeAt(0);
print(list);    // [B, C, D, E]

list = ['A', 'B', 'C', 'D', 'E'];
list.removeRange(1,3);
print(list);    // [A, D, E]

var numbers = [1, 2, 3, 4, 5];
numbers.removeWhere( (number) => number % 2 == 0 );
print(numbers);    // [1, 3, 5]

final List และ const List

เราสามารถสร้างลิสต์ที่ไม่สามารถเปลี่ยนแปลงค่าได้ด้วยการใช้ final และ const ซึ่งมีข้อแตกต่างกันคือ

var list = const [1, 2, 3];

// const List ไม่สามารถแก้ไขหรือเพิ่ม/ลดข้อมูลได้
list.add(4);        // Error!

// แต่เราสามมรถกำหนดค่าให้ตัวแปร list ใหม่ได้
list = [5, 6, 7];

ส่วน final นั้นจะเป็นแบบนี้

final list = [1, 2, 3];

// final ถูกเซ็ตที่ตัวแปร List ไม่ใช่ค่าของลิสต์
list.add(4);        // Ok!

// แต่เราไม่สามมรถกำหนดค่าใหม่ให้ตัวแปร list ใหม่ได้
list = [5, 6, 7];   // Error!

และเราสามารถสร้างลิสต์ที่เป็นทั้ง final และ const ได้

final list = const [1, 2, 3];

Spread Operator

เราสามารถใช้ ... เพื่อแยกลิสต์ได้ หากใช้เขียน JavaScript มาก่อนน่าจะคุ้นกับคำสั่งนี้

var list1 = [1, 2, 3, 4];
var list2 = [0, ...list1, 5];
//list2 is [0, 1, 2, 3, 4, 5]

และเพราะตัวแปรใน Dart สามารถเป็น null ได้ เราสามารถใช้คำสั่ง null-aware spread operator ได้

var list1;
var list2 = [0, ...?list1, 5];
//list2 is [0, 5]

นอกจากนี้เรายังสามารถใช้คำสั่ง if และ for ในลิสต์ได้ด้วย

var nav = [
  'Home',
  'Furniture',
  'Plants',
  if (promoActive) 'Outlet'
];

var listOfInts = [1, 2, 3];
var listOfStrings = [
  'number 0',
  for (var i in listOfInts) 'number $i'
];

Map

map เป็น Data Structure อีกตัวหนึ่ง ซึ่งเป็นแบบ key-value (คล้ายๆ List ที่สามารถกำหนดชื่อของ index ได้)

Map data = {'id': 1, 'name': 'Ann'};
print(data['id']);      // 1
print(data['name']);    // Ann

และเหมือนกับ List คือถ้าเราไม่ได้กำหนดชนิดข้อมูลมันจะถูกกำหนดอัตโนมัติให้เป็น Map

ดังนั้นคำแนะนำในการใช้ Map คือควรกำหนดชนิดของ key และ value ทุกครั้ง

Map<String, int> score = {'A': 100, 'B': 200};
//หรือ
var score = <String, int>{'A': 100, 'B': 200};

การเพิ่มหรือลบ item ออกจาก Map

การเพิ่ม item ลงใน Map นั้นทำง่ายกว่า List เพราะเราสามารถกำหนดค่าลงไปตรงๆ ได้เลย

Map<String, int> data = {'A': 100};
data['B'] = 200;

ส่วนการลบข้อมูลออกเราจะใช้คำสั่ง remove ซึ่งจะใช้การกำหนดด้วย key ไม่ใช่ value

Map<String, int> data = {'A': 1, 'B': 2};
data.remove('A');
print(data);    // {B: 2}

หรือถ้าต้องการตั้งเงื่อนไขการลบเองก็สามารถใช้คำสั่ง removeWhere

Map<String, int> data = {
    'A': 1, 
    'B': 2, 
    'C': 3,
};
data.removeWhere((key, value){
    return value==2 || key=='A';
});
print(data);    // {C: 3}

final Map และ const Map

เหมือนกับ List นั่นคือเราสามารถเราสามารถกำหนด const Map ซึ่งจะไม่สามารถเปลี่ยนแปลงค่าอะไรได้เลย

final constantMap = const {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

.map() method

เมธอด map เป็นคนละตัวกันคลาส Map นะ เป็นเมธอดที่ใช้ได้กับทั้ง List และ Map เลย

วิธีการใช้ map คือหากเราต้องการจะแปลงค่าทุกค่าใน List หรือ Map ให้เป็นอีกค่าหนึ่ง เช่นถ้าเรามี items ตัวหนึ่งที่ต้องการเอาค่าทุกค่าไป x 10

จากปกติที่เราเขียนกันแบบใช้ลูปธรรมดา

List<int> items = [1, 2, 3, 4];
List<int> items10;
for(var item in items){
    items10.add(item * 10);
}

// items10 is [10, 20, 30, 40]

ก็สามารถเขียนเป็นแบบนี้ได้

List<int> items = [1, 2, 3, 4];
List<int> items10 = items.map((item) => item * 10).toList();

// items10 is [10, 20, 30, 40]

เวลาเขียนให้สร้างฟังก์ชันสำหรับบอกว่า ถ้าเรามี item หนึ่ง จะแปลงให้มันเป็นค่าอะไรเท่านั้นพอ

แต่ map ของ List นั้นไม่ได้รีเทิร์นค่ากลับมาเป็น List แต่ให้ค่ากลับมาเป็น Iterable นั่นคือถ้าเราต้องการให้มันกลับมาเป็น List จะต้องใช้คำสั่ง toList() ต่ออีกที

ส่วนถ้าเป็น Map อาจจะใช้ยากขึ้นหน่อยเพราะจะต้องเขียนทั้ง key และ value และต้องตอบค่ากลับมาเป็น MapEntry แทน

Map<String, int> data = {
    'A': 1, 
    'B': 2, 
    'C': 3,
};

Map<String, int> data2 = data.map((key, value){
    return MapEntry('#$key', value * 10);
});

// data2 is {#A: 10, #B: 20, #C: 30}

Convert List-Map

เราสามารถแปลง List --> Map ได้โดย

List<String> data = ['A', 'B', 'C'];

Map<int, String> m = data.asMap();
// m is {0: A, 1: B, 3: C}

ซึ่ง key ของ Map ที่ได้ออกมาจะเป็น int และได้ค่ามาจาก index ของ List นั้นๆ

ส่วนการแปลง Map --> List นั้นง่ายกว่าแต่เราจะต้องเลือกว่าจะเอา List of key หรือ List of value

Map<String, int> m = {'A': 10, 'B': 20, 'C': 30};

m.keys      // [A, B, C]
m.values    // [10, 20, 30]
Series Navigation<< Dart 101: ทำความรู้จักภาษา Dart ฉบับโปรแกรมเมอร์Dart 103: มารู้จัก Class และ Object สไตล์ภาษาDartกันเถอะ >>
This entry is part 1 of 5 in the series Dart-Series

ปี 2011 กูเกิลได้เปิดตัวภาษาโปรแกรมตัวใหม่ชื่อว่าภาษา Dart (เวอร์ชันแรก)

โครงสร้างของภาษา DART คล้ายกับ C/C++ และ Java โดยที่จะมีความเป็นภาษาแบบ Structure Programming แต่ก็ยังมีความสามารถแบบภาษาประเภท Object Oriented Programming ด้วย นั่นคือมี class และ inheritance ให้ใช้งาน

เป้าหมายของการสร้างภาษา Dart ขึ้นมา กูเกิลบอกว่าอยากสร้างภาษาเชิงโครงสร้างที่ยืดหยุ่นมากพอ (structured yet flexible language) และเป็นการออกแบบตัวภาษาไปพร้อมกับตัว Engine สำหรับรันภาษาเลยเพื่อแก้ปัญหาโปรแกรมทำงานช้าและกินmemory ซึ่งเป้าหมายของภาษา Dart คือเป็นภาษาที่เรียนรู้ง่าย และทำงานได้บนอุปกรณ์พกพาขนาดเล็ก มือถือ ไปจนถึงserver

ซึ่งสิ่งที่เด่นที่สุดสำหรับภาษา Dart ในตอนนี้คือเป็นภาษาที่ใช้ในการสร้าง Application ด้วยเฟรมเวิร์ก Flutter นั่นเอง!

ในตอนนี้ Dart มีออกมาแล้ว 2 เวอร์ชันคือ Dart1 และ Dart2, ในบทความนี้จะพูดถึง Dart2 เป็นหลัก

หากต้องการลองเล่นภาษา Dart ดู สามารถเข้าไปดาวน์โหลดตัว installer ได้ที่ dart.dev
หรือลองเขียนภาษา Dart แบบ online เลยได้ที่ DartPad

Hello World!

ตัวอย่างโปรแกรมของ Dart นั้นหน้าตาคล้ายๆ กับภาษา C มาก ถ้าใครเคยเขียนภาษา C หรือภาษาตระกูล C มาก่อน (เช่น C++, C#, Java) จะคุ้นกับ syntax พวกนี้ทำให้เรียนรู้ได้ไม่ยาก

Dart เป็นภาษากลุ่ม Compiler นั่นคือจำต้อง Compile ก่อนเอาโปรแกรมไปรัน ไม่เหมือนภาษากลุ่ม Script ที่ใช้ interpreter ในการรันตัว source code ตรงๆ

void main(){
    print("Hello World!");
}

ตัวโปรแกรมจำเริ่มทำงานที่ฟังก์ชัน main เป็นหลัก เราไม่สามารถเขียน statement นอกฟังก์ชันได้

การแสดงผลมาตราฐานจะใช้คำสั่ง print (คำสั่งนี้ auto-newline เสมอนะ)

เรื่องหนึ่งที่ควรจำคือภาษา Dart นั้นการเขียน ; (semi-colon) ไม่ใช่ optional คือจำเป็นต้องใส่ ; ทุกครั้งหลังจบ statement ไม่สามารถละ ; ได้แบบภาษาตระกูล C ยุคใหม่ๆ เช่น JavaScript หรือ Kotlin

Comment

การใส่คอมเมนท์ทำได้เหมือนภาษา C ทุกอย่างคือ

  1. // สำหรับ inline comment
  2. เปิดด้วย /* และปิดด้วย */ สำหรับ multi-line comment (ไม่สามารถ nested ได้นะ)
int x; //ตั้งแต่ตรงนี้ไป เป็นส่วนของคอมเมนท์

/*
    ในนี้
    ทั้งหมด
    เป็นคอมเมนท์
*/

Variable และ Data Type

type คำอธิบาย ตัวอย่าง
int เลขจำนวนเต็ม 0, 1, -5, 86400
double เลขทศนิยม 0.0, 0.1, 0.14, -12.34
num เลขทศนิยม หรือ เลขทศนิยม 123, 0.123
bool ค่าทางตรรกศาสตร์ true, false
String สายอักขระ (ประโยค) 'hello world!', "This is a book" <-- ในภาษา Dart สามารถใช้ได้ทั้ง " (double quote) และ ' (single quote) แต่เขาแนะนำให้ใช้ ' หรือ single quote กันนะ
dynamic ตัวแปรชนิดเปลี่ยนแปลงได้ 1, 0.14, true, 'Hi!'

ตัวแปรของ Dart ทั้งหมดเป็นแบบ reference type ทั้งหมด ทำให้สามารถมีค่าเป็น null ได้ทั้งหมด

int x;
double d;
bool isDone;
String name;

//ตัวแปรทั้งหมดมีค่าเป็น null เพราะยังไม่ได้กำหนดค่า

แต่ใน Dart ยังมีชนิดของตัวแปรแบบพิเศษ ซึ่งไม่จำเป็นต้องประกาศ type เลย แต่ตัวภาษาจะ auto assign ชนิดของตัวแปรให้เอง

type คำอธิบาย
var เป็นการละ type เอาไว้ให้โปรแกรมกำหนดให้ (ตาม value)
final เหมือน var แต่ไม่สามารถเปลี่ยนแปลงค่าได้
const ค่าคงที่

ข้อแตกต่างระหว่าง dynamic vs var คือ

dynamic เป็นการบอกว่าตัวแปรนี้ เก็บค่าชนิดไหนก็ได้ เปลี่ยนแปลงได้เรื่อยๆ (หากใครเคยเขียนภาษา script น่าจะคุ้นกัน) แน่นอนว่าการใช้ dynamic มีความเสี่ยงทำให้เกิด runtime error! ได้เพราะ Compiler ไม่สามารถช่วยเช็กชนิดของตัวแปรได้เลย

var จะเป็นการกำหนดชนิดตัวแปรในจังหวะที่ประกาศตัวแปร โดยดูชนิดตัวแปรจาก value ตอนนั้นเลย หลังจากนั้นตัวแปรจะถูกกำหนดเป็น type นั้นไปตลอด ไม่สามารถเปลี่ยนแปลงได้แล้ว

dynamic d = 1;      // ตอนนี้ค่า d เก็บค่า int
d = 'new value!';   // Ok! ค่า d เปลี่ยนไปเก็บค่า String แทน
d = true;           // Ok! ค่า d เปลี่ยนไปเก็บค่า bool แทน

var v = 1;          // สร้างตัวแปร v ซึ่ง value ในด้านขวาเป็น int ดังนั้นจะมีผลเท่ากับการเขียนว่า int v = 1; นั่นเอง
v = 'new value';    // Error: A value of type 'String' can't be assigned to a variable of type 'int'

ข้อแตกต่างระหว่าง final vs const คือ

final เป็นการกำหนดว่าตัวแปรนี้ ไม่สามารถเปลี่ยนแปลงค่าได้ กำหนดค่าแล้วกำหนดเลย (immutable) ซึ่งเป็นตัวแปรประเภท runtime ดังนั้นเราสามารถกำหนดค่า final จากตัวแปรหรือฟังก์ชันอื่นได้

const เป็นการประกาศค่าคงที่ โดยค่าที่กำหนดให้จะต้องเป็น literal เท่านั้น (เช่น 10, 'value') เพราะเป็นตัวแปรที่กำหนดค่าตั้งแต่ตอน compile-time

int x = 10;

final int f1 = 1;       //กำหนดตัวแปร int ให้เป็นค่าคงที่
final f2 = 'final-val'; //ใช้เหมือน var คือไม่กำหนด type ก็ได้
final f3 = x + 20;      //กำหนดค่าจากกโดยคำนวณมาจากตัวแปรอื่นอีกที

const int c1 = 1;       //กำหนดตัวแปร int ให้เป็นค่าคงที่
const c2 = 'const-val'; //ใช้เหมือน var คือไม่กำหนด type ก็ได้
const c3 = x + 20;      //Error: Not a constant expression. เพราะ x เป็นตัวแปรที่ value มาตอน runtime ไม่สามารถกำหนดให้ const ได้

Math Operation

การใช้ +, -, *, / และ % เหมือนกับภาษาอื่นๆ แต่มีข้อควรระวังที่ตัว /

สำหรับภาษาอื่นถ้าเรานำ int / int ผลที่ออกมาจะได้เป็น int แน่นอน แต่สำหรับ Dart นั้นการหารจะได้ค่าออกมาเป็น double เสมอ

int x = 4 / 2;        // Error: A value of type 'double' can't be assigned to a variable of type 'int'.

int y = (int)(4 / 2); // Error: case แบบภาษา C ไม่ได้ด้วยนะ

วิธีการแก้คือใช้ operation ~/ คือการหารแล้วปัดจุดทิ้ง หรือใช้คำสั่ง as หรือจะใช้คำสั่ง toInt() ก็ได้

int x = 4 ~/ 2;             // Ok! แบบนี้ได้
int x = 4 / 2 as int;    // Ok! แบบนี้ได้
int x = (4 / 2).toInt();    // Ok! แบบนี้ได้

คำเตือน! ระวังสับสนกับคำสั่ง int.parse() กับ int.tryParse() ที่ใช้แปลง String --> int, เราไม่สามรถใช้ 2 คำสั่งนี้ในการแปลง double --> int ได้นะ

String Concatenate การต่อสตริง

การต่อสตริงใช้เครื่องหมาย + เหมือนภาษาทั่วๆ ไป แต่ก็มีข้อควรระวัง (อีกแล้ว!) คือไม่สามารถต่อสตริงกับตัวแปรที่ไม่ใช่สตริงได้!

int x = 100;
print('x is ' + x);     // Error: A value of type 'int' can't be assigned to a variable of type 'String'.

เราจะต้องแปลงตัวแปรที่ต้องการจะต่อสตริงให้เป็น String ซะก่อน หรือทำ String Interpolation ซะก่อน โดยใช้ตัว $ เพื่อระบุว่าตรงนี้เป็นตัวแปร (ถ้ามี expression ด้วยให้ครอบด้วย ${})

int x = 100, y = 200;
print('x is ' + x.toString());     // x is 100
print('x is $x');                  // x is 100
print('x is ${x}');                // x is 100

print('x + y is ${x + y}');        // x + y is 300

Null Handling

ตัวแปรใน Dart เป็นแบบ reference ดังนั้นเลยสามารถเป็นค่า null ได้ทุกตัวเลย ภาษาDartเลยมี operation สำหรับจัดการค่า null พวกนี้มาให้เราใช้งานด้วย

?? Null Coalescing

เป็นการเช็กว่าตัวแปรตัวนี้ ถ้ามีค่าเป็น null ให้ใช้ค่า default ที่กำหนดให้แทน

output = input ?? defaultValue;

// เป็น short-hand ของ...

if (input != null) {
    output = input;
} else {
    output = defaultValue;
}

เช่น

int number = ...;

int x = number ?? 1; // กำหนด x = number แต่ถ้า number เป็น null ให้กำหนด x = 1 แทน

?. Null Conditional

หากตัวแปรของเราเป็น object ซึ่งสามารถเรียกใช้งาน method ต่างๆ ได้ ... แต่ถ้า object ตัวนั้นเป็น null ก็จะเกิดปัญหา Null Pointer Exception ได้

object?.action();

// เป็น short-hand ของ...

if (object != null) {
    object.action();
}

เช่น

class People{
    void sayHi(){ print("hi!"); }
}
void main(){
    People people = ...;
    people?.sayHi();    // ถ้า people เป็น object ก็จะมีการปริ้นค่า "hi!" ออกมา แต่ถ้า people เป็น null คำสั่งนั้นก็จะไม่ถูกสั่งให้ทำงานเลย
}

??= Null Coalescing Assignment

หากไม่ชัวร์ว่าตัวแปรตัวนั้นเป็น null รึเปล่า สามารถใช้ ??= กำหนดค่า default ลงไปได้

variable ??= defaultValue

// เป็น short-hand ของ...

variable = variable ?? defaultValue;

// หรือใช้ Ternary Operator

variable = variable != null ? variable : defaultValue;

// หรือเขียนแบบ if-else

if (input == null) {
    output = defaultValue;
}

Flow Control

if-else

if (condition) {
    // TODO
} else {
    // TODO
}

switch case

switch (command) {
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  default:
    executeUnknown();
}

ข้อควรระวัง! switch ในภาษา Dart ต้องมี break ตอนจบ case ทุกครั้ง ถ้าไม่ใส่ลงไป โปรแกรมจะไม่หยุดทำงาน แล้วรันคำสั่งบรรทัดต่อไปต่อเลย

Loop: while, do-while

while (!isDone()) {
  doSomething();
}

do {
  printLine();
} while (!atEndOfPage());

เมื่อภาษาทั่วๆ ไป มีตัวcontrolเสริมคือ break และ continue ให้ใช้งานด้วย

Loop: for

for (var i = 0; i < 5; i++) {
  print(i);
}
// output: 0 1 2 3 4

หรือใช้งานแบบ for-each สำหรับวนลูปทุก element ใน list

var numbers = [0, 1, 2, 3, 4];
for (var number in numbers) {
  print(number);
}
// output: 0 1 2 3 4

Function

การสร้างฟังก์ชันในภาษา Dart มี syntax เหมือนภาษา C แต่สามารถละ type ทิ้งไปได้

เช่น

int add(int x, int y) {
    return x + y;
}

// สามารถเขียนย่อได้ว่า

add(x, y) {
    return x + y;
}

Arrow Function

และหากเคยเขียนภาษา JavaScript มา มีหลายครั้งที่เราสร้างฟังก์ชันที่มี return statement เดียวเท่านั้น เราก็สามารถเขียนย่อโดยใช้ Arrow Function ได้ ... และแน่นอน Dart ก็ทำได้เหมือนกัน โดยใช้ =>

int add(int x, int y) {
    return x + y;
}

// สามารถเขียนย่อได้ว่า

add(x, y) => x + y;

Optional Parameter

เราสามารถกำหนดค่าเริ่มต้นให้ parameter ได้โดยใช้ [] ครอบ parameter ที่อยากประกาศให้เป็น optional

int add(int x, [int y = 1]) {
    return x + y;
}

add(10, 20);    // result: 30
add(10);        // ไม่เซ็ตค่า y, ดังนั้น y = 1 result: 11

Named Parameter

บางกรณี การสร้างฟังก์ชันที่มี parameter เยอะมาก ตอนที่เรียกใช้ฟังก์ชันอาจจะงงเรื่องลำดับตัวแปรได้

int setConfig(
    String basePath,
    String appPath,
    int retry,
    int maxThread,
    String defaultController
) {
    // TODO
}

setConfig("/", "/app", 10, 4, "Main");

ในกรณีนี้เราสามาร้ถตั้งชื่อ parameter แต่ละตัวได้ โดยใช้ {}

int setConfig({
    String basePath,
    String appPath,
    int retry,
    int maxThread,
    String defaultController
}) {
    // TODO
}

setConfig(
    basePath: "/", 
    appPath: "/app", 
    retry: 10, 
    maxThread: 4, 
    defaultController: "Main"
);

ซึ่งตัว parameter ทั้งหมด สามารถสลับตำแหน่งกันได้

ข้อควรระวัง! ตอนประกาศฟังก์ชันต้องมี {} ครอบตัวแปร แต่ตอนเรียกใช้งานฟังก์ชัน ห้ามใส่ {} ลงไปนะ

การใช้งาน Named Parameter จะถือว่าเป็น optional ทั้งหมด (แปลว่าไม่ใส่ค่าก็ได้) ซึ่งก็จะได้ค่าเป็น null

หากต้องการให้เวลาเรียกใช้งานฟังก์ชัน จำเป็นต้องใส่ค่านั้นลงไปเสมอจะต้องใช้ annotation @required เข้ามาช่วย

ซึ่ง @required นั้นอยู่ใน package ชื่อ meta ที่ต้องติดตั้งเพื่อก่อนจะใช้งาน โดยการเพิ่ม dependency ในไฟล์ pubspec.yaml

dependencies:
  meta: ^1.1.8

เวลาใช้งานก็...

import 'package:meta/meta.dart';

int setConfig({
    @required String basePath,
    @required String appPath,
    int retry,
    int maxThread,
    String defaultController
}) {
    // TODO
}

แบบนี้หมายความว่า parameter basePath และ appPath นั้นจำเป็นต้องใส่ทุกครั้งที่เรียกใช้งานฟังก์ชัน

First Class Function

ตามสไตล์ภาษาสมัยใหม่ เราสามารถจับฟังก์ชันใส่ตัวแปรได้

int getNumber() => 123;

void main(){
    var func = getNumber;   // ไม่ใช่ getNumber() นะ,ไม่มี () 
    print(func());          // output: 123
}

หรือเราจะกำหนดว่าตัวแปรฟังก์ชันจะเป็น type อะไรและมี parameter อะไรบ้างก็ได้

โดยใช้รูปแบบการกำหนด type ดังนี้

return-type Function(params-type)

void func1(){ ... }
int func2(){ ... }
String func3(int x){ ... }

void main(){
    void Function() f1 = func1;
    int Function() f2 = func2;
    String Function(int) f3 = func3;
}

และยังใช้ได้กับ method อีกด้วย เช่น

class People{
    String sayHi() => "Hi!";
}

void main(){
    People p = People();
    String Function() f = p.sayHi;
    print(f());     // output: Hi!
}

สรุป

บทความนี้ได้แนะนำภาษา Dart ซึ่งเราจะเห็นว่าภาษานี้นั้นให้อารมณ์เหมือนภาษาตระกูล C ที่มีการปรับอะไรให้เป็นภาษาสมัยใหม่มากขึ้น แต่ก็ยังไม่ทิ้งความเป็น Structure Language อยู่

หากใครเขียน C หรือหรือแม้แต่ภาษา(ที่เคย)โมเดิร์นอย่าง Java มาก่อน จะพบว่ามันมีความคล่องตัวในการเขียนมากขึ้น แต่ถ้าเอาไปเทียบกับภาษายุคใหม่อย่างเช่น Kotlin, Swift ก็ยังเรียกว่าสู้ไม่ได้ อาจจะมีอะไรขัดใจอยู่หลายอย่างเวลาเขียน

สรุปคือเป็นภาษาที่ค่อนข้างธรรมดา ไม่ได้ Wow! อะไรมาก แต่เราต้องใช้มัน หากต้องการจะต่อยอดไปเขียน Flutter ต่อไป

ในบทต่อไปเราจะมาดู Data Structure ในภาษา Dart กันต่อ

Series NavigationDart 102: โครงสร้างข้อมูลพื้นฐาน List และ Map >>