สำหรับชาว Kotlin คงชื่นชอบและประทับใจใน Data Class กันไม่น้อย (เจ้าของบล็อกก็เช่นกัน) และหนึ่งในความสามารถสุดเท่ของ Data Class ก็คือการใช้คำสั่ง copy() ได้นั่นเอง

ทบทวนกับ Copy ใน Data Class กันก่อน

สมมติว่าเจ้าของบล็อกมี Data Class ตัวหนึ่งที่มีหน้าตาแบบนี้

data class Person(
    var name: String?,
    var age: Int,
    var weight: Float,
    var height: Float
)

และเวลาสร้าง Object ของ Person ขึ้นมาก็จะเป็นลักษณะแบบนี้

val person = Person(
    name = "Akexorcist",
    age = 18,
    weight = 52.5f,
    height = 180.2f
)

เมื่อต้องการสร้าง Object ใหม่ แต่ต้องการ Properties เหมือนกับของเดิมทั้งหมด ก็จะสามารถใช้ Copy ได้เลย

val person = Person(/* ... */)
val newPerson = person.copy()

ซึ่งคำสั่ง copy() นั้นจะเป็นการสร้าง Object ขึ้นมาใหม่ โดยที่มี Properties เหมือนกับตัวต้นฉบับเลย แต่ทว่า Object ทั้ง 2 ตัวนั้นจะเป็นคนละตัวกัน อันนี้ต้องจำให้ดีนะ

person == newPerson
// true

person === newPerson
// false

และที่เด็ดไปกว่านั้นก็คือถ้าต้องการจะ Modify ค่าของ Properties บางตัวด้วย ก็สามารถทำได้ในคำสั่ง copy() ได้เลย

val person = Person(
    name = "Akexorcist",
    age = 18,
    weight = 52.5f,
    height = 180.2f
)
/*
 * Person(
 *     name=Akexorcist, 
 *     age=18, 
 *     weight=52.5, 
 *     height=180.2
 * )
 */

val newPerson = person.copy(
    name = "Sleeping For Less"
)
/*
 * Person(
 *     name=Sleeping For Less, 
 *     age=18, 
 *     weight=52.5, 
 *     height=180.2
 * )
 */

จึงเป็นที่มาว่าทำไม Data Class ถึงเป็นหนึ่งในเหตุผลที่ทำให้นักพัฒนาชื่นชอบในภาษา Kotlin

แต่สิ่งหนึ่งที่นักพัฒนาควรรู้ก็คือ

Copy ใน Data Class เป็น Shallow Copy ไม่ใช่ Deep Copy

ทำให้การใช้ Copy จะต้องระมัดระวังเป็นอย่างยิ่งสำหรับ Data Class ที่มี Nested Properties อยู่ข้างใน

เพื่อให้เข้าใจภาพมากขึ้น ลองดูตัวอย่างนี้กัน

data class Person(
    var name: String?,
    var age: Int,
    var weight: Float,
    var height: Float,
    var contact: Contact
)

data class Contact(
    var email: String?,
    var twitter: String?,
    var facebook: String?,
    var gitHub: String?
)

จะเห็นว่า Person ได้มี Properties เพิ่มเข้ามาอีก 1 ตัวนั่นก็คือ Contact ที่จะมี Properties ต่างๆอยู่ข้างใน Contact อีกที ซึ่งจะต่างจาก Properties ตัวก่อนหน้าที่เป็น Primitive Data Type

จากนั้นก็ลอง Copy ด้วยคำสั่งเหมือนเดิม

val person = Person(
    name = "Akexorcist",
    age = 18,
    weight = 52.5f,
    height = 180.2f,
    contact = Contact(
        email = "[email protected]",
        twitter = "@akexorcist",
        facebook = "akexorcist",
        gitHub = "akexorcist"
    )
)
/*
 * Person(
 *     name=Akexorcist, 
 *     age=18, 
 *     weight=52.5, 
 *     height=180.2
 *     contact=Contact(
 *         [email protected]
 *         twitter=@akexorcist
 *         facebook=akexorcist
 *         gitHub=akexorcist
 *     )
 * )
 */

val newPerson = person.copy()
/*
 * Person(
 *     name=Akexorcist, 
 *     age=18, 
 *     weight=52.5, 
 *     height=180.2
 *     contact=Contact(
 *         [email protected]
 *         twitter=@akexorcist
 *         facebook=akexorcist
 *         gitHub=akexorcist
 *     )
 * )
 */

ถ้าลองเทียบค่าระหว่าง Object ทั้งสอง ก็จะดูเหมือนว่าไม่ได้มีอะไรแตกต่างไปจากเดิม

person == newPerson
// true

person === newPerson
// false

แต่ถ้าลองเทียบค่าระหว่าง contact ที่อยู่ข้างใน Object ของทั้ง 2 ตัวนี้ดูล่ะ?

person.contact == newPerson.contact
// true

person.contact === newPerson.contact
// true

จะเห็นว่า Object ของ contact นั้นเป็นตัวเดียวกันเป๊ะๆ (จากการใช้ ===)

ซึ่งจะทำให้เกิดปัญหาในตอนที่ผู้ที่หลงเข้ามาอ่านไปแก้ไขข้อมูลของ Contact ใน Object ตัวใดก็ตาม ก็จะส่งผลให้ค่าที่อยู่ใน Object อีกตัวเปลี่ยนแปลงตามไปด้วย

val person = Person(/* ... */)
val newPerson = person.copy()
newPerson.contact.email = "[email protected]"

// person.contact    = Contact([email protected], /* ... */)
// newPerson.contact = Contact([email protected], /* ... */)

รวมไปถึง Properties อย่าง List ด้วยเช่นกัน

data class Person(
    /* ... */
    var abilities: List<String>
)

แต่สำหรับ Array นั้นจะมีผลเหมือนกับ Primitive Data Type ตัวอื่นๆ ถึงแม้ว่าการเขียนโค้ดจะมอง Array เป็นเสมือนคลาสตัวหนึ่ง แต่จริงๆแล้ว Array ก็ยังคงเป็น Primitive Data Type อยู่ดี

แล้วถ้าต้องการ Deep Copy ใน Data Class ต้องทำยังไง?

ต้องบอกว่า Kotlin Standard Library ไม่ได้มีคำสั่ง Deep Copy สำหรับ Data Class ให้ ดังนั้นจึงต้องใช้วิธีอื่นเพื่อทำ Deep Copy เอง

และวิธีการทำ Deep Copy ที่เจ้าของบล็อกไปเจอมา และชอบมากที่สุดก็คือการใช้ GSON

เพราะ GSON จะช่วยแปลงข้อมูลใน Data Class ของผู้ที่หลงเข้ามาอ่านให้กลายเป็น JSON ที่อยู่ในรูป String และสามารถแปลงกลับมาเป็น Data Class สำหรับ Object ตัวใหม่ได้

val person = Person(/* ... */)
val newPerson = Gson().run {
    fromJson(toJson(person), Person::class.java)
}

จึงทำให้หมดปัญหาจาก Shallow Copy จาก copy() ได้เลย

หรือจะทำเป็น Extension Function ก้ได้เช่นกัน

fun <T> Any.deepCopy(): T? = Gson().run {
    fromJson(toJson(this@deepCopy), [email protected]) as? T
}

แต่สิ่งที่ต้องระวังสำหรับการสร้าง Extension Function เพื่อทำ Deep Copy ให้กับ Data Class ก็คือ ผู้ที่หลงเข้ามาอ่านไม่สามารถสร้าง Extension Function เฉพาะ Data Class ได้ ต้องใช้เป็น Any แทน ซึ่งหมายถึงคลาสใดๆก็ได้ใน Kotlin ดังนั้นถ้าไม่จำเป็นก็ไม่แนะนำให้ใช้ Deep Copy กับคลาสอื่นๆที่ไม่ใช้ Data Class นะ

และถ้าต้องการป้องกันไม่ให้เผลอไปใช้ Deep Copy กับคลาสอื่นๆที่ไม่ใช่ Data Class ก็แนะนำให้สร้าง Base Class ขึ้นมา แล้วสร้างเป็น Extension Function สำหรับ Base Class ตัวนั้นแทน แล้วให้ Data Class ทุกๆตัว Inherite จากตัวนั้นแทน

open class Data

fun <T> Data.deepCopy(): T? = Gson().run {
    fromJson(toJson(this@deepCopy), [email protected]) as? T
}

data class Person(
    /* ... */
) : Data()

เพียงเท่านี้ก็สามารถทำ Deep Copy ให้กับ Data Class ได้อย่างสบายใจแล้ว

สามารถดูบทความต้นทางของวิธีนี้ได้ที่ Here is the easiest way to deep copy an object in Kotlin [Medium]

Here is the easiest way to deep copy an object in Kotlin
If you ever needed to deep copy an object in Kotlin you know what pain it can be. Every solution I’ve seen out there will point you towards data class because then you can use the copy() method, but…

สรุป

คำสั่ง copy() ของ Data Class เป็น Shallow Copy ซึ่งจะไม่ส่งผลต่อ Nested Properties ดังนั้นในกรณีที่ต้องการ Deep Copy ก็ขอแนะนำให้ใช้ Gson เข้ามาช่วยเพื่อทำให้ Deep Copy สำหรับ Data Class นั้นเป็นเรื่องง่ายครับ