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