Dart: การใช้งาน Finalizer สั่งทำงานก่อน object โดนลบ

Finalizer เป็น class ที่ออกแบบมาเพื่อช่วยจัดการงานที่จะต้องทำก่อนที่ class ที่ออกแบบถูกลบทิ้งออกจากหน่วยความจำด้วย Garbage Collection หรือ GC คนที่เคยเขียนภาษาอื่นมาอาจสงสัยว่าทำไมตัว Dart คือไม่มี Class destructor มาให้ด้วย ถ้าดูจากการออกแบบจะพบว่า เนื่องจาก Dart ถูกออกแบบมาให้มี GC ช่วยในการเก็บกวาด Object ที่ไม่ถูกใช้งานแล้ว (ไม่มีการอ้างถึงตัวแปรใดในโปรแกรมขณะที่ทำงาน) ก็จะถูกลบทิ้ง โดยตัว Class ต่าง ๆ ที่ออกแบบมาให้ใช้งานใน Dart ล้วนแต่รองรับการถูกลบด้วย GC มาแล้ว นั้นคือ เมื่อมันจะถูกทำลายมันจะทำงานที่ยังจำเป็นให้เรียบร้อยก่อน เช่น หากมีการเปิดไฟล์เอาไว้ ด้วย FILE เมื่อมันจะถูก GC ลบ มันจะทำการปิดการเชื่อมต่อไฟล์ให้อัตโนมัติเป็นต้น ดังนั้นในด้านการเขียนโปรแกรมที่ใช้ Class ต่าง ๆ ที่อยู่ใน Dart อยู่แล้ว ตัว Finalizer จึงไม่มีความจำเป็นใด ๆ

งานที่จำเป็นต้องใช้ Finalizer ได้แก่งานที่เรียกใช้ API ภายนอกโปรแกรมที่ผู้เรียกใช้ต้องสั่งปิดการทำงานเมื่อสิ้นสุดการทำงานด้วยตัวเอง เช่น การเชื่อมต่อ database โดยใช้ driver ด้วยภาษาอื่นผ่าน dart:ffi หรืออาจเป็นงานที่ต้องทำก่อนจบ เช่น หากเปิดไฟล์ข้อมูลไว้ หากจบการทำงานต้องเขียนข้อมูลปิดท้ายไว้เสมอ ไม่ใช่แค่ปิดการเชื่อมต่อไฟล์เท่านั้นเป็นต้น

สิ่งสำคัญในการใช้ Finalizer ในการช่วยทำงานเมื่อ object กำลังจะโดนลบคือ ตัว Finalizer จะต้องไม่สูญเสียการอ้างอิงจากโปรแกรมที่กำลังทำงานอยู่ เพราะหากตัว Finalizer โดยลบไปด้วย มันจะไม่สามารถไปทำงานที่สั่งเอาไว้ได้ เทคนิคที่นิยมในการทำแบบนี้คือ กำหนดให้ Finalizer เป็น static member ใน class ที่จะใช้ Finalizer เพื่อมันคงอยู่จนปิดแอปที่ทำงาน

การสร้าง Finalizer

ตัว constructor ของ Finalizer จะเป็น callback ของสิ่งที่จะต้องการให้ทำเมื่อ class ที่กำหนดไม่ถูกอ้างถึงในแอปแล้ว

static final Finalizer<T> _finalizer = Finalizer((T) {
	// TODO: work wite T before object destroy
});

คำสั่ง .attach()

เมื่อสร้าง obect ขึ้นมาจาก class ผู้ใช้ต้องเพิ่มส่วนของ constructor ให้เรียกคำสั่ง .attach() เพื่อเพิ่ม object ของ class ที่สร้างขึ้นเข้าไปใน Finalizer ดังนี้

void attach(
  Object value,
  T finalizationToken, {
  Object? detach,
})
  • value คือ object ที่สร้างขึ้นและแนบไปกับ Finalizer
  • T คือตัว token ที่จะส่งต่อให้ callback เพื่อนำไปใช้ให้ทำงานก่อนที่ตัว object นี้จะถูกทำลาย
  • detach หากมีการส่งค่า object ที่จะใช้กับคำสั่ง .detach() จะใช้ detach ตัวนี้เป็น key ในการเอาออกจาก Finalizer

ในการใช้งานผู้ใช้สามารถ .attach() ได้หลายครั้งที่เป็น value เดียวกัน แต่ detach ต่างกัน เมื่อมีการเรียกคำสั่ง .detach() มันจะยกเลิกการ .attach() ที่มีการระบุ detach ตรงกันเท่านั้น ซึ่งตรงนี้ส่วนตัวก็ไม่ค่อยเข้าใจเท่าไหร่ เนื่องจากไม่มีตัวอย่างที่งานแบบที่ว่ามา😅

คำสั่ง .detach()

ใช้สำหรับยกเลิกสิ่ง .attach() ไปแล้ว โดยส่ง object ที่จะอ้างถึงตัว detach จะใช้ตอนที่ผู้ใช้งานสั่งงานสิ่งที่ได้ .attach() ด้วยตัวเอง เลยไม่จำเป็นต้องใช้งาน Finalizer อีก หากผู้ใช้ไม่ทำการยกเลิกตัว Finalizer จะเรียก callback อีกครั้งตอนที่ GC สั่งทำลาย Object ทำให้เกิด error ได้

ตัวอย่างการใช้งาน

ลองสร้าง Class ชื่อ Door มี .open() เป็น constructor เมื่อใช้งานเสร็จให้สั่ง .close() หากผู้ใช้งานต้องการปิดประตูด้วยตัวเอง ในตัวอย่าง จะมี Finalizer ที่เป็น static member ไว้คอยทำหน้าที่จัดการหลังใช้งานเสร็จ ซึ่งในตัวอย่างจะเป็นการพิมพ์ข้อความออกมาว่ามันถูกเรียกจาก callback จาก Finalizer การลองจำลองการสร้าง List ที่สมาชิกจำนวน 1,024,000 (ใช้เนื้อที่หน่วยความจำประมาณ 8MB) เพื่อเร่งให้ GC ทำการคืนหน่วยความจำที่ไม่ใช้แล้ว เพราะใช้หน่วยความจำมาก

class Door {
  static final Finalizer<List> _finalizer = Finalizer((doorNumber) {
    listSize -= doorNumber.length;
    print("Door #${doorNumber[0]} was close by Finalizer.");
    print('          listSize: $listSize');
  });

  static num nextDoorNumber = 0;
  static int listSize = 0;

  final List _doorNumber;

  Door.open() : _doorNumber = List.filled(1024000, nextDoorNumber) {
    nextDoorNumber++;
    listSize += _doorNumber.length;
    print('Door() →      listSize: $listSize');
    _finalizer.attach(this, _doorNumber, detach: this);
  }

  void close() {
    listSize -= _doorNumber.length;
    print("Door #${_doorNumber[0]} was close.");
    _finalizer.detach(this);
  }

  get doorNumber => _doorNumber;
}

void main() async {
  print('start');

  void createDoor() {
    Door d = Door.open();
    print('new door ${d.doorNumber[0]}');
  }

  while (true) {
    createDoor();
    await Future.delayed(Duration(milliseconds: 250));
  }
}

ผลที่ได้ จากการลองสร้าง Object ที่มาจาก class Door พบว่าพอสร้างทิ้ง ๆ ไป 20 ครั้ง ตัว GC ก็เริ่มคืนหน่วยความจำ ดูได้จากตัว Finalizer ที่ถูกเรียก callback

start
Door() →      listSize: 1024000
new door 0
Door() →      listSize: 2048000
new door 1
Door() →      listSize: 3072000
new door 2
Door() →      listSize: 4096000
new door 3
Door() →      listSize: 5120000
new door 4
Door() →      listSize: 6144000
new door 5
Door() →      listSize: 7168000
new door 6
Door() →      listSize: 8192000
new door 7
Door() →      listSize: 9216000
new door 8
Door() →      listSize: 10240000
new door 9
Door() →      listSize: 11264000
new door 10
Door() →      listSize: 12288000
new door 11
Door() →      listSize: 13312000
new door 12
Door() →      listSize: 14336000
new door 13
Door() →      listSize: 15360000
new door 14
Door() →      listSize: 16384000
new door 15
Door() →      listSize: 17408000
new door 16
Door() →      listSize: 18432000
new door 17
Door() →      listSize: 19456000
new door 18
Door() →      listSize: 20480000
new door 19
Door #17 was close by Finalizer.
     listSize: 19456000
Door #16 was close by Finalizer.
     listSize: 18432000
Door #15 was close by Finalizer.
     listSize: 17408000
Door #14 was close by Finalizer.
     listSize: 16384000
Door #13 was close by Finalizer.
     listSize: 15360000
Door #12 was close by Finalizer.
     listSize: 14336000
Door #11 was close by Finalizer.
     listSize: 13312000
Door #10 was close by Finalizer.
     listSize: 12288000
Door #9 was close by Finalizer.
     listSize: 11264000
Door #8 was close by Finalizer.
     listSize: 10240000
Door #7 was close by Finalizer.
     listSize: 9216000
Door #6 was close by Finalizer.
     listSize: 8192000
Door #5 was close by Finalizer.
     listSize: 7168000
Door #4 was close by Finalizer.
     listSize: 6144000
Door #3 was close by Finalizer.
     listSize: 5120000
Door #2 was close by Finalizer.
     listSize: 4096000
Door #1 was close by Finalizer.
     listSize: 3072000
Door #0 was close by Finalizer.
     listSize: 2048000
Door #18 was close by Finalizer.
     listSize: 1024000
Door() →      listSize: 2048000
new door 20
Door() →      listSize: 3072000
new door 21