Dart: Collections → List เบื้องต้น

List (List class) เป็น indexable collection ที่ทำหน้าที่คล้ายกับข้อมูลประเภท array ในภาษาคอมพิวเตอร์อื่น ๆ เป็นการเก็บข้อมูลหลาย ๆ ตัว เป็นลำดับแถว สามารถกำหนดประเภทข้อมูลที่อยู่ภายใน List ได้ว่าจะเป็นข้อมูลประเภทไหน ตัวเลข ตัวอักษร หรือ Object กำหนดประเภทข้อมูลผ่าน Generic

List class มีการ implements Iterable class

การสร้าง List

การสร้างและประกาศข้อมูล List จะใช้เครื่องหมายวงเล็บก้ามปู [...] เพื่อใส่สมาชิกภายในนั้น

void main() {
  var myList = [1, 2, 3];
  
  print(myList); // output → [1, 2, 3]
  print(myList.length); // output → 3
  print(myList.elementAt(0)); // output → 1
  print(myList.elementAt(1)); // output → 2
  print(myList.elementAt(2)); // output → 3
}

จากตัวอย่าง การประกาศโดยใช้ [...] จะตีความเป็น type annotation ดังนี้

  List<int> myList = [1, 2, 3];

ตัว myList ที่สร้างขึ้น จะเป็น List ที่จะเก็บเฉพาะข้อมูลที่เป็น int เท่านั้น หากพยายามเพิ่มหรือแก้ไขข้อมูลที่ไม่ใช่ int เข้าไป จะทำให้เกิด error ขึ้น

void main() {
  List<int> myList = [1, 2, 3];
  dynamic x = 10.5;
  myList.add(x); // Unhandled exception: type 'double' is not a subtype of type 'int'
  print(myList);
}

ในกรณีที่ต้องการให้ List เก็บข้อมูลแบบ dynamic ให้ประกาศ Generic เป็น dynamic แบบนี้

  var myList1 = <dynamic>[1, 2, 3, 'ABC', 10.5];
  List<dynamic> myList2 = [1, 2, 3, 'ABC', 10.5]; //same as myList1

การสร้าง List แบบแก้ไขค่าใน List ไม่ได้ (unmodifiable list)

หากต้องการสร้าง List ที่ไม่สามารถเปลี่ยนแปลงค่าได้ (แก้ไขค่า/เพิ่ม/ลด สมาชิกใน List) สำหรับไว้ใช้งาน ให้ใช้ keyword const ในการประกาศ List

  const myList1 = [0, 1, 2];
  const List<int> myList2 = [0, 1, 2]; //same as myList1
  var myList3 = const [0, 1, 2]; //same as myList1
  var myList4 = const <int>[0, 1, 2]; //same as myList1

  myList1.add(10); // error → Unsupported operation: Cannot add to an unmodifiable list
  myList2.add(20); // error → Unsupported operation: Cannot add to an unmodifiable list

หากต้องการสร้าง unmodifiable list จาก list ที่มีอยู่แล้วในตัวแปรอื่น สามารถสร้างด้วย .unmodifiable() constructor ตามตัวอย่าง

void main() {
  var myList1 = <int>[1, 2, 3]; // normal list
  myList1.add(30); //OK

  var myList2 = List.unmodifiable(myList1); // use myList1 as init data
  print(myList2); // output → [1, 2, 3, 30]
  myList2.add(30); // error → Unsupported operation: Cannot add to an unmodifiable list
  myList2[0] = 100; // error → Unsupported operation: Cannot modify an unmodifiable list
}

การสร้าง List แบบจำนวนสมาชิกคงที่ (Fixed-length list)

การสร้าง List ที่มีสมาชิกคงที่ ไม่สามารถเพิ่ม/ลด จำนวนสมาชิกใน List โดยการใช้ .from() และ .filled() constructor โดยกำหนดค่า growable: false

  • .from() จะเป็นการสร้าง List จาก List ที่มีอยู่แล้ว ดู API เพิ่มเติม
  • .filled() จะเป็นการสร้าง List โดยเติมค่าเริ่มต้นเข้าไปตามจำนวนสมาชิกที่ต้องการ ดู API เพิ่มเติม

ตัวอย่างการใช้ .from() ในการสร้างจาก List ที่มีอยู่แล้ว อาจเป็นตัวแปร หรือระบุ List ที่ต้องการสร้างลงไป

void main() {
  var myList = List.from(<int>[1, 2, 3], growable: false);
  print(myList); // output → [1, 2, 3]
  myList[0] = 0; // modify element index 0 from 1 to zero
  print(myList); // output → [0, 2, 3]
  myList.add(4); // error → Unsupported operation: Cannot add to a fixed-length list
}

ตัวอย่างการใช้ .filled() โดยต้องระบุค่าเริ่มต้นของสมาชิกใน List

void main() {
  var myList1 = List.filled(3, 0, growable: false);
  print(myList1); // output → [0, 0, 0]
  myList1[0] = 10; // modify element index 0 from 0 to 10
  print(myList1); // output → [10, 0, 0]
  myList1.add(4); // error → Unsupported operation: Cannot add to a fixed-length list
}

การเข้าถึงสมาชิกใน List

ตำแหน่งของสมาชิกใน List จะเป็น zero-based indexing นั้นคือ สมาชิกตัวแรกจะมีค่า index เป็น 0 สามารถเข้าถึงด้วยคำสั่ง .elementAt(index) หรือใช้วงเล็บก้ามปู [index] ก็ได้แล้วแต่สะดวก

void main() {
  var myList1 = [0, 1, 2];
  print(myList1[0]); // output → 0
  print(myList1[1]); // output → 1
  print(myList1[2]); // output → 2
  print(myList1[3]); // RangeError (index): Invalid value: Not in inclusive range 0..2: 3
}

ในตัวอย่าง ค่า index ของ myList1 จะมีค่าอยู่ที่ 0 ถึง myList1.length - 1

Dsmurat, penubag, Jelican9, CC BY-SA 4.0

หากพยายามเข้าถึง index ที่ไม่มีอยู่จริงจะเกิด RangeError ขึ้น โปรดระวังในการใช้งาน

การแก้ไขค่าของสมาชิก สามารถทำโดยใช้เครื่องหมาย = ในการกำหนดค่าใหม่ของสมาชิกในตำแหน่ง index ที่ต้องการ

  var myList1 = [0, 1, 2];
  myList1[1] = 2;
  myList1[2] = 4;
  print(myList1); // output → [0, 2, 4]

การเพิ่ม ลบ แก้ไข สมาชิกใน List

ตัว List จะมีคำสั่งในการจัดการข้อมูลของสมาชิก ดังนี้

เพิ่มสมาชิกต่อท้าย

แทรกสมาชิกตรงตำแหน่ง index

  • .insert() ทำการแทรกที่ตำแหน่ง index ด้วยสมาชิก 1 ตัว สมาชิกเดิมที่ตำแหน่ง index จะถูกเลื่อนไป index + 1 ดู API เพิ่มเติม
  • .insertAll() ทำการแทรกที่ตำแหน่ง index ด้วย List ที่ต้องการ สมาชิกเดิมที่ตำแหน่ง index จะถูกเลื่อนไป index + .length ของ List ที่แทรก ดู API เพิ่มเติม

ลบสมาชิก

  • .remove() ใช้ลบสมาชิกที่มีค่าตรงกับที่กำหนด ในกรณีที่มีข้อมูลซ้ำ จะลบตัวแรกที่พบแค่ตัวเดียว คืนค่า false หากไม่เจอข้อมูลที่จะให้ลบ ดู API เพิ่มเติม
  • .removeAt() ใช้ลบสมาชิกตรงตำแหน่ง index ที่ระบุ หากทำสำเร็จจะคืนค่าเป็นสมาชิกตัวที่ลบ และจะมีผลทำให้ .length มีค่าลดลง 1 สมาชิกที่อยู่หลังจาก index ที่ลบ จะถูกเลื่อนมาข้างหน้าแทนตัวที่ลบไป ดู API เพิ่มเติม
  • .removeLast() จะลบจากตัวสมาชิกที่อยู่ด้านหลังสุด หากทำสำเร็จจะคืนค่าเป็นสมาชิกตัวที่ลบ และจะมีผลทำให้ .length มีค่าลดลง 1 ดู API เพิ่มเติม
  • .removeRange() ลบสมาชิกที่มีค่า index ในช่วงที่ระบุ เช่น list.removeRange(1, 4) ผลคือจะลบสมาชิกที่ index เท่ากับ 1 2 และ 3 ทิ้งไป มีผลทำให้ .length มีค่าลดลง 4-1=3 ดู API เพิ่มเติม
  • .removeWhere() ลบสมาชิก ทุกตัว ที่ตรงกับเงื่อนไขทดสอบ ดู API เพิ่มเติม
  • .clear() ลบสมาชิกทุกตัวทิ้ง มีผลทำให้ .length เท่ากับ 0 ดู API เพิ่มเติม

การแทนที่สมาชิก

  • .replaceRange() เป็นคำสั่งที่รวม 2 คำสั่งเข้าด้วยกันคือ .removeRange().insertAll() มีข้อดีคือประสิทธิภาพดีกว่า ดู API เพิ่มเติม

คำสั่งอรรถประโยชน์ อื่น ๆ

  • .sort() เรียงลำดับสมาชิกใน List สามารถใช้การเรียงลำดับตามค่าปริยายของข้อมูลก็ได้ หรือจะใช้ Comparator ฟังก์ชั่น เพื่อกำหนดวิธีการเรียงลำดับก็ได้ ดู API เพิ่มเติม
  • .shuffle() เปลี่ยนลำดับ index ของสมาชิกแบบสุ่ม ดู API เพิ่มเติม
    แต่หากต้องการสุ่มเลือก index ไม่ได้ต้องการสุ่มสลับลำดับใน List ทั้งหมด แนะนำว่าใช้ Random().nextInt(List.length); เพื่อเอาค่า index มาดึงข้อมูลดีกว่า Random class
  • .sublist() คืนค่าเป็น List ตามช่วงค่า index ที่ระบุ เช่น .sublist(1, 3) จะคืน List ของสมาชิก index ที่ 1 และ 2 กลับมา หากไม่กำหนดค่า end index จะคืน List จนถึงสมาชิกตัวสุดท้าย ดู API เพิ่มเติม

การค้นหาสมาชิกใน List ด้วย .indexOf() .lastIndexOf() .indexWhere() .lastIndexWhere()

List รองรับการค้นหาค่าของสมาชิก และคืนค่า index ของสมาชิกที่ตรงกับค่าหรือเงื่อนไขที่ต้องการ

ในการค้นหา index ของสมาชิกใน List ที่มีค่าซ้ำ สามารถเพิ่มตำแหน่ง start index เพื่อข้ามไปตำแหน่งที่จะค้นหาถัดไป

ในกรณีที่ไม่เจอสิ่งที่ค้นหา จะคืนค่า -1 กลับมา

การค้นหาสมาชิกแบบตรงไปตรงมา ตามค่าที่สนใจ

การค้นหาสมาชิกที่ต้องการใน List อยู่ตำแหน่ง index ที่เท่าไหร่ หากเป็นการหาค่าเป็นตรงไปตรงมา สามารถใช้ .indexOf() และ .lastIndexOf()

ตัวอย่างการใช้ .indexOf() ในการค้นหาสมาชิก โดยจะจำลองว่าหากมีสมาชิกที่ค่าซ้ำกัน จะค้นหาตัวต่อไปอย่างไร

void main() {
  var myList = ['A', 'B', 'C', 'B', 'C'];
  print(myList.indexOf('X')); // output → -1  
  print(myList.indexOf('A')); // output → 0
  print(myList.indexOf('C')); // output → 2

  // search duplicate value
  var lastFound = myList.indexOf('B');
  print(lastFound); // output → 1
  print(myList.indexOf('B', lastFound + 1)); // output → 3
}

ตัวอย่างการใช้ .lastIndexOf() ในการค้นหาสมาชิก จากหลังมาหน้า โดยจะจำลองว่าหากมีสมาชิกที่ค่าซ้ำกัน จะค้นหาตัวก่อนหน้าอย่างไร

void main() {
  var myList = ['A', 'B', 'C', 'B', 'C'];
  print(myList.lastIndexOf('X')); // output → -1  
  print(myList.lastIndexOf('A')); // output → 0
  print(myList.lastIndexOf('C')); // output → 4

  // search duplicate value
  var lastFound = myList.lastIndexOf('B');
  print(lastFound); // output → 3
  print(myList.lastIndexOf('B', lastFound - 1)); // output → 1
}

การค้นหาสมาชิกที่ตรงกับเงื่อนไขในฟังก์ชั่นทีต้องการ

การค้นหาที่ต้องเขียนโปรแกรมเพื่อตรวจสอบสมาชิกว่าตรงกับที่ต้องการหรือไม่ สามารถใช้ .indexWhere() และ .lastIndexWhere() ให้เขียนฟังก์ชั่นที่ทดสอบและส่งค่า boolean กลับมา โดยจะคืนค่า index ของสมาชิกที่ตรงกับเงื่อนไขที่กำหนด

ตัวอย่าง การค้นหาค่าสมาชิก ที่มีค่ามากกว่า 2 ที่เจอเป็นตัวแรก

void main() {
  var myList = [0, 1, 2, 3, 4, 5, 6];
  var foundIndex = myList.indexWhere((element) => element > 2);
  print(foundIndex); // output → 3
}

ตัวอย่าง การค้นหาค่าสมาชิก ที่มีค่ามากกว่า 2 ที่เจอเป็นตัวสุดท้าย (ค้นหาจากหลังมาหน้า)

void main() {
  var myList = [0, 1, 2, 3, 4, 5, 6];
  var foundIndex = myList.lastIndexWhere((element) => element > 2);
  print(foundIndex); // output → 6
}

การค้นหาข้อมูลสมาชิกที่มีประสิทธิภาพ

เนื่องจากตัว .indexOf() .lastIndexOf() .indexWhere() .lastIndexWhere() ออกแบบมาเพื่อค้นหา ว่ามีสมาชิกใน List ตรงกับที่ค้นหาหรือไม่ ถ้าไม่เจอก็ได้ค่า -1 กลับมา การที่พยายามเอาไปทดสอบเพื่อหาสมาชิกทุกตัวที่เหลือว่ามี index ใดบ้างที่ตรง หาก List มีขนาดใหญ่ จะเกิดปัญหาเรื่องประสิทธิภาพขึ้น แนะนำว่าใช้คำสั่ง for() เพื่อไล่ตรวจสอบสมาชิกทุกตัวจะดีกว่า

void main() {
  var myList = [0, 1, 2, 3, 4, 5, 6];
  for (int index = 0; index < myList.length; ++index) {
    if (myList[index] > 2) {
      print(index);
    }
  }
}

ผลที่ได้

3
4
5
6

การสร้าง List ใหม่ด้วย +

หากต้องการสร้าง List ใหม่ ที่เกิด List จำนวน 2 ตัวเข้าด้วยกัน สามารถใช้ + ในการเชื่อมต่อ List ได้

void main() {
  var myList1 = [1, 2];
  var myList2 = [3, 4];
  var myList12 = myList1 + myList2;
  print(myList12); // output → [1, 2, 3, 4]
}
Dsmurat, penubag, Jelican9, CC BY-SA 4.0

ใช้เพื่อสร้าง List ใหม่เท่านั้น หากต้องการจะเอาไปต่อ List เดิม อย่าใช้วิธีนี้
myList1 = myList1 + myList2;
ให้ใช้ ✅ myList1.addAll(myList2);

การใช้ Spread Operator ... เพื่อใช้เพิ่มสมาชิกใน List ใหม่ ด้วย List ที่มีอยู่

นอกจากการใช้ .addAll() และ + เพื่อทำงานกับตัวแปรข้อมูล List แล้ว ยังสามารถใช้ ... เพื่ออ้างถึงตัวแปร List ที่จะนำไปใส่ใน List ใหม่ที่ต้องการสร้างได้อีกด้วย ซึ่งบางครั้งก็สะดวกในการเขียนโปรแกรม เนื่องจากสั้นและเข้าใจได้ง่าย

void main() {
  var myList1 = [1, 2];
  var myList2 = [10, 20, ...myList1];
  print(myList2); // output → [10, 20, 1, 2]

  var myList3 = [...myList1, ...myList2];
  print(myList3); // output → [1, 2, 10, 20, 1, 2]
}

จากตัวอย่างข้างบน สามารถเขียนโดยไม่ใช้ ... เพื่อให้ผลเหมือนกันดังนี้

void main() {
  var myList1 = [1, 2];
  var myList2 = [10, 20];
  myList2.addAll(myList1);
  print(myList2); // output → [10, 20, 1, 2]

  var myList3 = myList1 + myList2;
  print(myList3); // output → [1, 2, 10, 20, 1, 2]
}

ดูข้อมูลเพิ่มเติมเกี่ยวกับ Spread Operator

ห้าม❎เปรียบเทียบ List มีสมาชิกเท่ากันหรือไม่ด้วย ==

อันนี้อาจดูแปลก ๆ หน่อย😅ที่ไม่สามารถใช้ == เปรียบเทียบระหว่าง List ได้ แม้จะมีสมาชิกเหมือนกันทุกประการ แต่ใน API บอกไว้ว่า

Whether this list is equal to other.
Lists are, by default, only equal to themselves. Even if other is also a list, the equality comparison does not compare the elements of the two lists.

void main() {
  var myList1 = [1, 2];
  var myList2 = List.from(myList1);
  var myList3 = [1, 2];

  print(myList1); // output → [1, 2]
  print(myList2); // output → [1, 2]
  print(myList1 == myList2); // output → false
  print(myList1 == myList3); // output → false
  print(myList1 == myList1); // output → true

  var myList4 = myList1;
  print(myList1 == myList4); // output → true
}

จากตัวอย่างจะเห็นว่า == เอาไว้ทดสอบว่าตัวแปรสองตัว อ้างอิงไปที่ตัว List เดียวกันหรือไม่เท่านั้น (ในตัวอย่างคือ myList1 กับ myList4)

การสร้าง array แบบหลายมิติ

อ้างอิงตาม Dart 3.5.0 จะใช้ List เป็นประเภทข้อมูลที่เก็บเป็นแถว แต่มีแค่มิติเดียว หากต้องการใช้งานแบบหลายมิติ จำเป็นต้องใช้วิธีการประกาศแบบ List ใน List ซ้อนกันอีกที ตัวอย่างการสร้าง List แบบ 2 มิติ

void main() {
  var array2d = [
    [1, 2],
    [3, 4]
  ];
  print(array2d[0][0]); // output → 1
  print(array2d[0][1]); // output → 2
  print(array2d[1][0]); // output → 3
  print(array2d[1][1]); // output → 4
}

หากต้องการเขียนใช้งานใสรูปแบบที่เคยใช้ในภาษาอื่น เช่น array2d(1, 0) array2d[1, 0] แบบนี้จำเป็นต้องเขียน Class ขึ้นมาจัดการเอง หรืออาจจะไปลองดูใน Dart Packages ในเว็บ pub.dev ว่ามีใครที่สร้างเอาไว้บ้างหรือเปล่า

เทคนิคการเอาตัวสมาชิกที่ซ้ำออกจาก List

เนื่องจาก List สามารถใส่ข้อมูลซ้ำได้ หากต้องการใช้ข้อมูลของ List ที่เป็นค่าที่ตัดตัวซ้ำออกไป วิธีการที่เขียนโปรแกรมสั้น และสะดวก สามารถทำได้โดยแปลงจาก List → Set → List เนื่องจาก Set เป็น collection ที่ไม่มีการเก็บข้อมูลที่ซ้ำกัน เมื่อแปลงจาก List ที่มีสมาชิกข้อมูลซ้ำกัน มันจะตัดออกเหลือแค่ตัวเดียว

void main() {
  var myList = [0, 1, 2, 1, 2, 1, 2, 3];
  var mySet = Set.from(myList); // alternate method → myList.toSet()
  var resultList = mySet.toList();
  print(resultList); // output → [0, 1, 2, 3]
}
Dsmurat, penubag, Jelican9, CC BY-SA 4.0

วิธีนี้เหมาะกับการแปลงเพื่อใช้งานเล็กน้อย ถ้าเป็นไปได้การเอาผลของ Set ที่ได้จากการแปลง List ไปใช่ต่อเลยจะช่วยประหยัดหน่วยความจำ
ในกรณี List มีขนาดใหญ่มาก ๆ ควรเขียนคำสั่งจัดการเองเลยอาจมีประสิทธิภาพดีกว่า