Shallow Copy & Deep Copy in Dart

ကျွန်တော်တို့ dart language ကို အသုံးပြုတဲ့အခါ ရှိပြီးသား object တွေကို ကူးပြီး အသစ်တည်ဆောက်ရတာမျိုးတွေ ရှိပါတယ်။ Form တွေကို ရှိပြီးသား value တွေပေါ် အခြေခံပြီး ပြောင်းလဲတာတွေ လုပ်ဖို့ လိုတဲ့အခါ ရှိပြီးသား value ကို copy လုပ်ပြီး တည်ဆောက်လေ့ရှိပါတယ်။ ဒီလိုတည်ဆောက်တဲ့နေရာမှာ shallow copy နဲ့ deep copy ခွဲခြားပြီး သိထားဖို့ လိုအပ်ပါတယ်။ ၂ခုလုံးက object အသစ်တခုကို တည်ဆောက်တာချင်း တူပေမယ့် သူတို့ရဲ့ အလုပ်လုပ်ပုံတွေမှာ ကွာခြားမှုတွေ ရှိပါတယ်။

Shallow Copy

Shallow copy ဆိုတာကတော့ object တခုကနေ နောက်တခုကို ပွားယူလိုက်တဲ့အခါ object အသစ်က နဂို object မှာ ပါဝင်တဲ့ data တွေကို သွားပြီး reference ယူနေတာမျိုးကို ခေါ်ပါတယ်။ object ၂ခုထဲက တခုခုရဲ့ data ကို ပြောင်းလိုက်တဲ့အခါ နောက်တခုမှာပါ သွားပြီးတော့ ပြောင်းနေတဲ့ အခြေအနေမျိုး ဖြစ်နေမှာပဲ ဖြစ်ပါတယ်။

Deep Copy

Deep copy ဆိုတာကတော့ object တခုကနေ နောက်တခုကို ပွားယူလိုက်တဲ့အခါ object အသစ်က နဂို object မှာ ပါဝင်တဲ့ data တွေအားလုံးကို ကူးယူပြီး သီးခြား object အသစ်တခု တည်ဆောက်လိုက်တာမျိုးပဲ ဖြစ်ပါတယ်။ ဒီတော့ နဂီု object ပဲ ဖြစ်ဖြစ် ကူးယူလိုက်တဲ့ object ပဲ ဖြစ်ဖြစ် ဘယ် object ထဲမှာပါတဲ့ data တွေကို ပြောင်းလဲသည်ဖြစ်စေ သပ်သပ်စီ ပြောင်းလဲနေမှာပဲ ဖြစ်ပါတယ်။

Primitive Types vs. User-Created Types

Copy လုပ်နည်းအကြောင်း အသေးစိတ် မပြောခင် နားလည်ထားရမှာ တခုတော့ ရှိပါတယ်။​ ဒါကတော့ dart language ရဲ့ primitive type တွေနဲ့ user defined type တွေကို သိထား၊ နားလည်ထား ရမှာပဲ ဖြစ်ပါတယ်။ Dart language မှာ int, bool, double, String အစရှိတဲ့ primitive type တွေ ပါဝင်ပါတယ်။

Primitive Types

Primitive type တွေကို တခုကနေတခု ကူးတဲ့အခါ သူတို့ရဲ့ memory reference ကို ကူးတာမဟုတ်ဘဲ သူတို့ရဲ့ data ကို ကူးတာပဲ ဖြစ်ပါတယ်။ ဘာကြောင့်လဲဆိုတော့ primitive value တွေက immutable (ပြောင်းလဲလို့ မရတာ) ဖြစ်လို့ပါ။ ပြောင်းလဲလို့မရတဲ့အတွက် data တူတာကိုပဲ variable နောက်တခုမှာ assign လုပ်ချင်တဲ့အခါ data အသစ်တခုအနေနဲ့ assign လုပ်သွားပါတယ်။

void main() {
  int x = 4;
  int y = x;
  
  print("original x: ${x}");
  // original x: 4
  
  print("original y: ${y}"); 
  // original y: 4

  y = 5;
  print("y value is change to ${y} but x is still ${x}");
  // y value is change to 5 but x is still 4

  x = 3;
  print("x value is changed to ${x} but y is ${y} now");
  // x value is changed to 3 but y is 5 now
}

အပေါ်က ဥပမာပေးထားတဲ့ code မှာ ကြည့်မယ်ဆို x ကို အရင်ဆုံး 4 assign လုပ်လိုက်ပါတယ််။ ပြီးတော့ x မှာ ရှိတဲ့ value ကို y ထဲကို copy လုပ်ပါတယ်။ primitive type ဖြစ်တဲ့အတွက် y ထဲကို assign လုပ်လို့ ရောက်လာတဲ့ 4 က x ထဲမှာ assign လုပ်ထားတဲ့ 4 ကို reference လုပ်ထားတာမဟုတ်ဘဲ အသစ်တခုအနေနဲ့ တည်ဆောက်ထားတဲ့ 4 ဖြစ်သွားပါတယ်။ ဒီတော့ assign လုပ်ပြီးတဲ့နောက်ပိုင်း ဘယ်လိုပဲ x နဲ့ y ကို အပြောင်းအလဲလုပ်လုပ် တခုနဲ့တခု ဘယ်လိုမှ သက်ရောက်မှု ရှိမနေပါဘူး။ ဒီတော့ primitive value တွေကို copy လုပ်တဲ့အခါ shallow copy ဖြစ်သလား၊​ deep copy ဖြစ်သလား စဥ်းစားနေဖို့ မလိုတော့ပါဘူး။

User-Created Types

ဒါကတော့ ကျွန်တော်တို့ developer တွေ တည်ဆောက်ထားတဲ့ class တွေကို ပြောချင်တာ ဖြစ်ပါတယ်။ Class ကို instance တခုအနေနဲ့ တည်ဆောက်တဲ့အခါ object တခု ရလာမှာဖြစ်ပြီး dart language မှာတော့ object တွေကို reference type လို့ သတ်မှတ်ပါတယ်။ ဆိုလိုတာကတော့ object တခုကို ကူးယူပြီး တခြား variable တခုမှာ assign လုပ်တဲ့အခါ နဂိုရှိပြီးသား object ရဲ့ data တွေ အသစ် တည်ဆောက်တဲ့ object က reference လုပ်နေမှာကြောင့်ပဲ ဖြစ်ပါတယ်။

class Student {
  String name;
  String gender;
  
  Student(this.name, this.gender);
}

void main() {
  Student student1 = Student("Arkar", "Male");
  print("Name: ${student1.name}");
  // Name: Arkar
  print("Gender: ${student1.gender}");
  // Gender: Male
  
  Student student2 = student1;
  
  student2.name = "Max";
  print("Student1 name: ${student1.name}");
  // Student1 name: Max
  print("Student2 name: ${student2.name}");
  // Student2 name: Max
}

ဒီနေမှာတော့ Student ဆိုတဲ့ class ကို ကျွန်တော်တို့ တည်ဆောက်ပြီး သူ့ထဲမှာ name နဲ့ gender ဆိုတဲ့ primitive တွေကို သတ်မှတ်ထားပါတယ်။ student1 ဆိုတဲ့ object တည်ဆောက်ချိန်မှာ ကျွန်တော်တို့ name နဲ့ gender ကို တခါထဲ Arkar နဲ့ Male ဆိုပြီး သတ်မှတ်ပေးလိုက်ပါတယ်။ ပြီးတော့ ကျွန်တော်တို့ student1 ကို student2 ထဲကို assign လုပ်လိုက်ပါတယ်။ student2 ထဲကို student1 assign လုပ်တဲ့အချိန်မှာတော့ အပေါ်မှာ ပေးထားတဲ့ y ထဲကို x assign လုပ်တာနဲ့ မတူဘဲ student1 ထဲက value တွေရဲ့ reference တွေကို student2 ထဲမှာ ထည့်ပေးလိုက်တာပဲ ဖြစ်ပါတယ်။

ဒီလိုမျိုး copy လုပ်သွားတာကို shallow copy လို့ခေါ်ပါတယ်။ object ကို ကူးသွားသယောင် ရှိပေမယ့် တကယ်တော့ ရှိပြီးသား object ကို သွားပြီးတော့ reference လုပ်နေတာမျိုးပဲ ဖြစ်ပါတယ်။

User-defined class တွေတင်မကဘဲ dart language မှာပါတဲ့ List, Map, Set class တွေကလဲ ပုံမှန်အတိုင်း assign (newObject = oldObject) လုပ်ပြီးတော့ ကူးမယ်ဆို shallow copy ကိုပဲ ရရှိမှာဖြစ်ပါတယ်။ ဒါတွေကို reference အနေနဲ့ မဟုတ်ဘဲ deep copy လိုချင်တာဆိုရင်တော့ သက်ဆိုင်ရာ class တွေမှာပါတဲ့ function တွေကို အသုံးပြုပြီးတော့ တည်ဆောက်နိုင်ပါတယ်။

List

Shallow Copy

void main() {
  List<List<int>> original = [[1, 2, 3], [4, 5, 6]];
  List<List<int>> copied = List.from(original);

  copied[0][0] = 20;

  print(original[0][0]); // Output: 20
  print(copied[0][0]); // Output: 20
}

Deep Copy

void main() {
  List<List<int>> original = [[1, 2, 3], [4, 5, 6]];
  List<List<int>> copied = original.map((subList) => List<int>.from(subList)).toList();

  copied[0][0] = 10;

  print(original[0][0]); // Output: 1
  print(copied[0][0]); // Output: 10
}

Map

Shallow Copy

void main() {
  Map<String, List<int>> original = {'key1': [1, 2, 3], 'key2': [4, 5, 6]};
  Map<String, List<int>> copied = Map.from(original);

  copied['key1']?[0] = 10;

  print(original['key1']?[0]); // Output: 10
  print(copied['key1']?[0]); // Output: 10
}

Deep Copy


void main() {
  Map<String, List<int>> original = {'key1': [1, 2, 3], 'key2': [4, 5, 6]};
  Map<String, List<int>> copied = original.map((key, value) => MapEntry(key, List<int>.from(value)));

  copied['key1']?[0] = 10;

  print(original['key1']?[0]); // Output: 1
  print(copied['key1']?[0]); // Output: 10
}

Set

Shallow Copy

void main() {
  Set<List<int>> original = {[1, 2, 3], [4, 5, 6]};
  Set<List<int>> copied = Set.from(original);

  copied.first[0] = 10;

  print(original.first[0]); // Output: 10
  print(copied.first[0]); // Output: 10
}

Deep Copy

void main() {
  Set<List<int>> original = {[1, 2, 3], [4, 5, 6]};
  Set<List<int>> copied = original.map((list) => List<int>.from(list)).toSet();

  copied.first[0] = 10;

  print(original.first[0]); // Output: 1
  print(copied.first[0]); // Output: 10
}

User-defined class

ကျွန်တော်တို့ သတ်မှတ်ထားတဲ့ class တွေကနေ ဖြစ်လာမယ့် object တွေကိုလဲ deep copy လုပ်ချင်တဲ့အခါ အောက်မှာပေးထားသလိုမျိုး လုပ်သွားလို့ရပါတယ်။ Deep copy လုပ်ပုံလုပ်နည်း အမျိုးမျိုး ရှိတဲ့အတွက် ကိုယ်အသုံးပြုမယ့် အခြေအနေနဲ့ ကိုက်ညီမယ့် deep copy လုပ်နည်းကို သုံးသွားလို့ ရပါတယ်။ အဓိက ရည်ရွယ်ချက်ကတော့ object ကို reference တန်းယူတာမျိုးမဟုတ်ဘဲ object ထဲက value တွေကို ကူးယူသွားတာမျိုးပဲ ဖြစ်ပါတယ်။

class Student {
  String name;
  String gender;

  Student(this.name, this.gender);

  Student deepCopy1() {
    return Student(
      this.name,
      this.gender,
    );
  }

  Student.deepCopy2(Student oldStudent)
      : name = oldStudent.name,
        gender = oldStudent.gender;

  Student deepCopy3({String? name, String? gender}) {
    return Student(
      name ?? this.name,
      gender ?? this.gender,
    );
  }
}

void main() {
  Student student = Student("Arkar", "Male");
  print("Name: ${student.name}");
  // Name: Arkar
  print("Gender: ${student.gender}");
  // Gender: Male

  Student student1 = student.deepCopy1();
  Student student2 = Student.deepCopy2(student1);
  Student student3 = student.deepCopy3(name: "Max");
  

  student1.name = "Charles";
  student2.name = "Carlos";
  print("Original student name: ${student.name}");
  // Original student name: Arkar
  print("Student1 name: ${student1.name}");
  // Student1 name: Charles
  print("Student2 name: ${student2.name}");
  // Student2 name: Carlos
  print("Student2 name: ${student3.name}");
  // Student2 name: Max
}