ประกาศ Extension Functions ใน Class อื่นก็ได้เหรอ

Travis P
Black Lens
4 min readMay 11, 2018

--

ทุกคนคงทราบดีว่าจุดขายใหญ่ที่สุดของ Extension Functions ใน Kotlin ก็คือการเอามาแทน static utility methods อันเวิ่นเว้อของ Java นั่นเอง

แต่ประโยชน์ของมันไม่ได้หมดแค่นั้นครับ ผมเพิ่งบังเอิญเจอข้อดีของมันอีกอย่าง ก็เลยเอามาเล่าให้ฟังกัน

Motivation

section นี้ผมขอเล่าเหตุการณ์ย้อนหลังนะครับว่าไปไงมาไง ถ้าขี้เกียจอ่านก็ skip ไป

Flashback
  1. วันก่อนครับ ขณะที่ผมกำลังส่ง Parcelable ผ่าน Intent ระหว่าง Activity ตามปกติอยู่นั้น
class MyActivity : AppCompatActivity() {
fun onCreate(bundle: Bundle?) {
val
p: P? = intent.getParcelableExtra("EXTRA_P")
if (p != null) {
// do something with p
}
}
}

2. เห็นโค้ดยาวๆก็เลย extract function ออกไปก่อน

class MyActivity : AppCompatActivity() {  fun onCreate(bundle: Bundle?) {
super.onCreate(bundle)
val p: P? = intent.getParcelableExtra("EXTRA_P")
if (p != null) {
doSomething(p)
}
}
fun doSomething(p: P) {
// do something with p
}
}

3. ด้วยความที่เราทราบว่า Kotlin มี safe call operator .? พอเราเห็น if (p != null) ก็เลยอยากใช้ท่าสวยๆ

val p: P? = intent.getParcelableExtra("EXTRA_P")
p?.let {
doSomething(it)
}

4. เอาจริงๆไม่ได้ต่างกับแบบ if not null เลยแม้แต่น้อย งั้นก็ลองยุบให้เหลือ expression เดียวดู

intent.getParcelableExtra<P?>("EXTRA_P")?.let { doSomething(it) }

5. สั้นลงจริง ยังมี let มี it อยู่ เหมือนเดิม ไม่ได้ช่วยให้ชีวิตดีขึ้น ทำให้เกิดคำถามขึ้นในหัวว่า เราจะทำยังไงให้ doSomething(P) ทำงานก็ต่อเมื่อ P ไม่เป็น null จะได้ไม่ต้องมาห่อด้วย let อีกชั้น แต่คิดยังไงก็คิดไม่ออก ผมจึงตัดสินใจเปิด instagram เพื่อหาแรงบันดาลใจ

หนึ่งในแรงบันดาลใจ

6. หลังจากได้แรงบันดาลใจ ผมจึงตระหนักได้ว่า เอ๊ะ! extension functions นี่จริงๆแล้วเป็นแค่ syntactic sugar นี่หว่า การที่เราประกาศ

fun P.foo() {} 

จริงๆแล้วมันคือ static method อันนึงใน Java เท่านั้นเอง

public static void foo(P receiver) {}

ถ้าผมเปลี่ยน P จากเดิมที่เป็น parameter ตัวแรก ให้กลายเป็น extension receiver ผมจะสามารถใช้ safe call operator ?. ได้ฟรี!

7. ทดสอบสมมติฐาน ลองเปลี่ยนเป็น extension function ดู

class MyActivity : AppCompatActivity() {
fun P.doSomething() {
// do something with this (p)
}
}

เห้ย ได้แฮะ สุดท้ายก็สามารถเขียนได้สวยอย่างนี้

intent.getParcelableExtra<P?>("EXTRA_P")?.doSomething()

แต่ก็ต้องแก้ doSomething() นิดหน่อย เช่นจาก p.a ก็เรียก a ห้วนๆได้เลย หรือจาก print(p) ก็กลายเป็น print(this) เหมือน extension functions ทั่วไป

นิยามของคำว่าสวย

Extension Functions as Members

ถ้าคุณได้อ่าน documentation อย่างละเอียดจะพบว่ามี section เล็กๆที่ชื่อว่า Declaring Extensions as Members อยู่ (member คือสมาชิกของ class เช่น function หรือ property)

คือโดยปกติเรามักจะประกาศ extension functions เป็น top level functions กัน แต่รู้หรือไม่ว่าเราสามารถประกาศ extension functions ใน class ได้ด้วย!

class D {  fun E.foo() {  
// define an extension function of E inside D
}
fun f() {
val e = E()
e.foo() // call E.foo() defined above
}
}

แปลว่า ถ้าเรามี member function ที่รับ 1 parameter เป็นอย่างน้อย เราสามารถเลือกที่จะเขียนในรูปแบบของ extension function ได้ เช่น

class A {
fun foo(b: B) {}
fun bar(b: B, c: C): D {}
fun baz(b: B, c: C): D {}
}

สามารถเขียนได้อย่างนี้

class A {
fun B.foo() {}
fun B.bar(c: C): D {}
fun C.baz(b: B): D {}
}

เพื่ออะไร?

เพื่อความสะดวก เพราะอยากเขียน เพราะสวย ชอบ แล้วแต่เลย ถ้าคุณเขียนแบบนี้แล้วมีความสุขก็เขียนเถอะ

อย่างผมนี่ก็เขียนเพราะมัน chain call สวยขึ้น เขียน expression สวยขึ้น แทนที่จะต้องมาเขียนแบบนี้

val v = a.b?.let { foo(it) }.cfun foo(b: B) {}

ผมสามารถเขียนให้สวยขึ้นได้แบบนี้

val v = a.b?.foo().cfun B.foo() {}

ศัพท์ที่ควรรู้

ทีนี้พอเราประกาศในคลาส มันก็จะมี this 2 ตัว ตัวนึงเป็นของคลาสที่มันอาศัยอยู่ อีกตัวนึงเป็นของ extension เค้านิยามไว้ตามนี้

receiver คือออพเจ็คที่เราสามารถเรียกฟังก์ชั่นหรือพรอพเพอร์ตี้ของมันได้โดยไม่ต้องระบุว่าเรียกบนใคร หรือว่ากันง่ายๆ

receiver คือศัพท์ทางการของ this นั่นเอง

ทีนี้พอเราประการ ext fun ในคลาสก็มีศัพท์เฉพาะสำหรับ this ทั้ง 2 ตัวดังนี้
dispatching receiver คือ this ของคลาสปัจจุบัน (คลาสที่ ext fun อาศัยอยู่)
extension receiver คือ this ของคลาสที่เรากำลังต่อเติม (คลาสที่ ext fun ต่อเติม)

พอมี receiver 2 ตัวแล้วถ้าชื่อ function ซ้ำกัน ก็ต้องมีการระบุด้วยว่าจะเอา this ไหนด้วย qualified this

class D {
fun E.foo() {
toString() // E.toString() extension receiver
this@D.toString() // D.toString() dispatching receiver
}
}

เบื้องลึกเบื้องหลัง

เบื้องหลังการทำงานของ extension as members ก็คล้ายๆ กับ top level เลย ต่างกันตรงที่ top level จะได้เป็น static method แต่ ext as member จะได้เป็น instance method แทน

สำหรับโค้ดทั้ง 2 แบบ ถ้าวัดกันในระดับ bytecode แล้ว แทบจะไม่ต่างกันเลย อาจจะ metadata ไม่เหมือนกันนิดหน่อยเท่านั้นเอง

same JVM signature
same Java equivalent

ทดสอบสมมติฐานด้วยการประกาศทั้ง 2 แบบ พบว่าเป็นจริง function ทั้ง 2 อันนี้หน้าตาเหมือนกันเป๊ะ(ในมุมมองของ JVM) พอลอง decompile กลับไปเป็น Java ก็ยิ่งชัดเข้าไปใหญ่

ถ้ามันจะเป๊ะขนาดนี้ ก็ไม่ต่างกับ instance function ธรรมดาเลยน่ะสิ แค่เปลี่ยน syntax เป็นอีกสไตล์เท่านั้นเอง

แน่นอนว่าถ้าเราประกาศเป็น open ก็สามารถ override ใน subclass ได้ด้วย โดยตัว extension receiver จะ resolve statically ส่วน dispatching receiver จะ resolve dynamically เหมือนปกติทุกอย่าง (ดูเพิ่มที่ Declaring Extensions as Members)

ตัว Android Studio/intellij เองก็มี intentions (alt+enter) ช่วยสลับระหว่าง 2 สไตล์นี้มาด้วย ยิ่งอุ่นใจ

extension property as a member

นอกจากนี้ extension property ก็ประกาศแบบ member ได้เหมือนกัน

ส่งท้าย

หวังว่าจะอ่านรู้เรื่องนะครับ (แหะๆ) เผื่อเจอจังหวะดีๆก็เอาไปใช้กัน มีอะไรสงสัยทักมาได้ ช่วยกันแชร์ด้วยนะครับ

ผมขอปิดบทความด้วยมิวสิคและคุณไข่ที่เป็นแรงบันดาลใจให้ผมปิ๊งไอเดียนี้ขึ้นมา

--

--

Travis P
Black Lens

Android Developer, Kotlin & Flutter Enthusiast and Gamer