ใน Dart 3 ขึ้นไปจะมีประเภทข้อมูลชนิดใหม่ Records (Record class) ที่ช่วยให้คนเขียนโปรแกรมสามารถส่งชุดข้อมูลที่มีมากกว่า 1 ตัว ไปให้ฟังก์ชั่น หรือคืนค่ากลับมาจากฟังก์ชั่นได้ ช่วยลดความยุ่งยากในการเขียนโปรแกรมโดยไม่ต้องใช้พวกกลุ่ม Collection เช่น List Map หรือต้องสร้างเป็น data class ในการจัดการข้อมูลที่ส่งพร้อมกันมากกว่า 1 ตัว
หลักการทำงานของ Records จะคล้ายการเก็บข้อมูลของภาษาโปรแกรมอื่น ๆ ที่มี tuple หรือ product ใน Dart ข้อมูล Records ออกแบบมาให้ค่อนข้างยืดหยุ่น สามารถปรับเปลี่ยนรูปแบบในการจัดการได้หลายแบบ ทั้งอ้างตามลำดับ แบบอ้างตามชื่อ field หรือผสมกัน
Records เป็น immutable ไม่สามารถแก้ไขได้เมื่อสร้างข้อมูลแล้ว หากไม่ใช่จะถูกทิ้งไว้ในหน่วยความจำ รอ Garbage collector มาเก็บกวาดภายหลัง
ข้อมูลแบบ Records จะเป็นการนำข้อมูลประเภทอื่น ๆ เอามาอยู่รวมกัน โดยสมาชิกที่อยู่ใน Records จะไม่สามารถเพิ่มหรือลดได้หลังจากสร้างไปแล้ว ข้อมูลที่เป็นสมาชิกจะอยู่ใน (...)
การเข้าถึงสมาชิกของ Records ที่ประกาศแบบเรียงตามลำดับ จะใช้ .$1 .$2 .$3 ...
เพื่อเข้าถึงสมาชิกลำดับที่ 1 2 3 และที่เหลือถ้ามี
void main() {
var recNumber = (1, 2, 3, 4);
print(recNumber.$1); // output → 1
print(recNumber.$2); // output → 2
print(recNumber.$3); // output → 3
print(recNumber.$4); // output → 4
}
สมาชิกใน Records จะเป็นข้อมูลต่างชนิดกันได้ เพียงแต่เมื่อประกาศสมาชิกแล้ว เวลาที่ใส่ Record ใหม่ในตัวแปร ต้องเป็นรูปแบบข้อมูลแบบเดียวกันเท่านั้น
void main() {
var recNumber = (1, 2, 3, 4);
recNumber = ('a', 'b'); // compile error → A value of type '(String, String)' can't be assigned to a variable of type '(int, int, int, int)'.
recNumber = (1, 2); // compile error → A value of type '(int, int)' can't be assigned to a variable of type '(int, int, int, int)'.
}
ในกรณีที่ไม่ใช่ var สามารถระบุ type annotation ดังนี้
void main() {
var recNumber1 = (1, 2, 3);
(int, int, int) recNumber2 = (1, 2, 3); // type annotation same as recNumber1
print(recNumber1); // output → (1, 2, 3)
print(recNumber2); // output → (1, 2, 3)
}
จากตัวอย่างที่ผ่านมา จะเห็นว่าการเข้าถึงค่าสมาชิกของ Records จะใช้ .$1 .$2 .$3 ...
เพื่อเข้าถึงสมาชิกตามลำดับ แต่ Records ยังสามารถประกาศแบบเขียนระบุชื่อ field เพื่อเข้าถึงสามาชิกตามชื่อที่ต้องการได้อีกด้วย ข้อดีของวิธีนี้คือ สามารถสลับตำแหน่งของข้อมูลได้ เพราะอิงจากชื่อ field เป็นหลัก
void main() {
var point1 = (x: 100, y: 200);
print(point1.x); //output → 100
print(point1.y); //output → 200
point1 = (y: 35, x: 20);
print(point1.x); //output → 20
print(point1.y); //output → 35
}
หากมีการผสมระหว่าง มีชื่อ field กับไม่มี จะอ้างอิงอย่างไร ตัว Dart จะใช้วิธีแยกการเข้าถึงทั้งสองแบบ โดยแบบไม่มีชื่อ field ก็ใช้ .$1 .$2 .$3 ...
ส่วนที่ประกาศชื่อ field ก็ให้ใช้ชื่อในการเข้าถึงสมาชิก ไม่สามารถใช้ .$1 .$2 .$3
เข้าถึงได้
void main() {
var point1 = ('no field1', x: 100, y: 200, 'no field2');
print(point1.$1); //output → no field1
print(point1.$2); //output → no field2
print(point1.x); //output → 100
print(point1.y); //output → 200
point1 = ('no field1', 'no field2', y: 200, x: 100); // OK same as above
point1 = (y: 200, x: 100, 'no field1', 'no field2'); // OK same as above
point1 = (y: 200, 'no field1', x: 100, 'no field2'); // OK same as above
}
หากต้องการใช้งานแบบลำดับแต่ไม่มีชื่อ field อาจทำให้สับสนว่าข้อมูลใน Records คืออะไร เขียนชื่อของข้อมูลเอาไว้ จะช่วยให้กลับมาอ่านและแก้ไขโปรแกรมได้ง่ายกว่า ตัวอย่างต่อไปนี้จะเป็นการประกาศแบบต่าง ๆ ที่สามารถทำได้ และทำไม่ได้ (compile error)
void main() {
// declare as name of member in Records
(int pointX, int pointY) point1 = (10, 20);
print(point1.$1); //output → 10
print(point1.$2); //output → 20
print(point1.pointX); // compile error!!
print(point1.pointY); // compile error!!
// declare as field of member in Records
({int pointX, int pointY}) point2 = (pointX: 10, pointY: 20);
print(point2.pointX); // output → 10
print(point2.pointY); // output → 20
print(point2.$1); // compile error!!
print(point2.$2); // compile error!!
// mix
(int pointX, {int pointY}) point3 = (10, pointY: 20);
print(point3.$1); // output → 10
print(point3.pointY); // output → 20
}
จากตัวอย่างจะเห็นว่า หากประกาศแบบโดยตั้งชื่อข้อมูลเฉย ๆ ไม่ได้ประกาศเป็น field เวลาเข้าถึงสมาชิกต้องใช้ .$1 .$2 .$3 ...
เสมอ ไม่สามารถใช้ชื่อที่ตั้งได้ วิธีการนี้จะเน้นเพื่อประกาศสำหรับให้เข้าใจว่าข้อมูลคืออะไร แต่ตอนใช้ไม่ต้องการเขียนชื่อ field ให้อ้างตามลำดับข้อมูล อยู่ที่จุดประสงค์ในการใช้งานและออกแบบโปรแกรม
หากไม่มี Records แต่ข้อมูลที่ประมวลผลและคืนจากฟังก์ชั่น มีค่ามากกว่า 1 ตัว ปกติจะใช้พวก Collection ต่าง ๆ เช่น List Map หรือ Set ในการส่งข้อมูลกลับมา สำหรับ Records มีความเรียบง่ายและจัดการข้อมูลได้ดีกว่า เพราะสามารถกำหนด field และประเภทของข้อมูลกลับมาได้ จุดนี้ยังช่วยลด bug ที่เกิดจากการส่งข้อมูลกลับมาไม่ตรงกับที่คาดหวังเอาไว้ (พวกส่งกลับมาผสมกับหลายอย่าง เลยต้องประกาศเป็น dynamic)
(int, int) swapNumber((int, int) input) {
(int, int) result = (input.$2, input.$1);
return result;
}
void main() {
var testNum = (1, 2);
print(swapNumber(testNum)); // output → (2, 1)
}
จากตัวอย่างจะเป็นการเขียนฟังก์ชั่น swapNumber เพื่อสลับตัวเลขที่อยู่ใน Records แล้วคือกลับมาเป็น Records เนื่องจาก Records สามารถกำหนดประเภทข้อมูลของสมาชิกเป็นอะไรก็ได้ จึงทำให้ผู้เขียนโปรแกรมสามารถออกแบบได้ตามความเหมาะสมว่าจะใช้ข้อมูลประเภทใด หากลองดัดแปลงตัวอย่าง ที่เดิมรับข้อมูลเป็น int ให้เป็น num การใช้งานก็จะกว้างขึ้น แต่ก็ยังสามารถควบคุมประเภทข้อมูลได้ (ไม่ประกาศเป็น dynamic)
(num, num) swapNumber((num, num) input) {
(num, num) result = (input.$2, input.$1);
return result;
}
void main() {
(int, double) testNum = (1, 2.5);
print(swapNumber(testNum)); // output → (2.5, 1)
}
การ Destructuring เป็นวีธีการนำเอาสมาชิกแต่ละตัวของ Records มาใส่ในตัวแปรที่ต้องการ เพื่อความสะดวกในการใช้งาน ตัวอย่างต่อไปนี้จะเอาผลที่ได้จากฟังก์ชั่น swapNumber()
มาใส่ในตัวแปร numA
และ numB
ตามลำดับ
(num, num) swapNumber((num, num) input) {
(num, num) result = (input.$2, input.$1);
return result;
}
void main() {
(int, double) testNum = (1, 2.5);
var (numA, numB) = swapNumber(testNum); // Destructuring to numA and numB
print(numA); // output → 2.5
print(numB); // output → 1
}
จากตัวอย่าง ผลที่ได้จาก swapNumber()
จะมีค่าเป็น Records (2.5, 1)
วิธี Destructuring จะเป็นการกำหนดรูปแบบข้อมูลที่จะแตกมาเก็บในตัวแปรที่ต้องการ โดยรูปแบบข้อมูลต้นทางและปลายทางต้องเหมือนกันถึงจะทำได้ จากตัวอย่างข้างบน สามารถเขียนแบบที่ไม่ใช้วิธี Destructuring ได้ดังนี้
void main() {
(int, double) testNum = (1, 2.5);
var result = swapNumber(testNum);
var numA = result.$1;
var numB = result.$2;
print(numA); // output → 2.5
print(numB); // output → 1
}
ในกรณีที่ Records เก็บข้อมูลแบบ field ซึ่งไม่สามารถเข้าถึงแบบลำดับได้ ดังนี้วิธีการ Destructuring จำเป็นต้องระบุชื่อ field ที่จะนำมาใส่ในตัวแปร กำหนดโดย :
นำหน้าชื่อ field ที่ต้องการ
void main() {
var point1 = (x: 10, y: 20, z: 0);
var (:x, :y, :z) = point1;
print(x); // output → 10
print(y); // output → 20
print(z); // output → 0
}
ข้อจำกัดของวิธี Destructuring กับ Records ที่ระบุชื่อ field ตัวแปรที่ได้จะเป็นชื่อเดียวกับ field ที่ระบุ ดังนั้น หากชื่อตัวแปรซ้ำกับตัวแปรที่ประกาศไปก่อนหน้า จะเกิด compile error ขึ้น วิธีแก้ไขอาจทำ code block {...}
ครอบส่วนที่จะใช้งานเพื่อให้เป็น local variable ใน block นั้นไม่อ้างอิงออกไปข้างนอก block
==
ตัวแปรที่เป็น Records สามารถนำมาตรวจสอบว่าเท่ากันหรือไม่ด้วย ==
ผลการเปรียบเทียบจะเป็น true
ก็ต่อเมื่อ โครงสร้างของ Records เหมือนกัน สมาชิกเมื่อเทียบแต่ละตัวเท่ากันหมดทุกตัว
var point1 = (1, 2);
var point2 = (1, 2);
print(point1 == point2); // output → true
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);
print(point == color); // output → true
({int x, int y, int z}) point = (x: 1, y: 2, z: 3);
({int r, int g, int b}) color = (r: 1, g: 2, b: 3);
print(point == color); // output → false (fields name dose not same)
ในกรณีที่ต้องการสร้าง Records แต่สมาชิกตอนออกแบบมีแค่ 1 ตัว วางแผนไว้ว่าในอนาคตจะมีการเพิ่มสมาชิกเข้าไปอีก วิธีการให้ใส่ ,
หลังสมาชิกตัวแรก เพื่อให้ compiler รู้ว่ามันเป็นข้อมูลแบบ Records
var a = (1,); // OK
(int,) b = (1,); // OK
(int) c = (1); // error → A record type with exactly one positional field requires a trailing comma.
การประยุกต์ใช้งานตัว Records ยังสามารถใช้การตรวจสอบกับ Patterns ใน Dart ได้อีกด้วย แต่คงไม่ขอพูดถึงใน note ตัวนี้