คุณสมบัติหลักประการหนึ่งของการรับทำแอพ Kotlin คือการรองรับฟังก์ชันต่างๆ ซึ่งช่วยให้การรับทำแอพสามารถเขียนโค้ดที่นำมาใช้ซ้ำได้ แบบโมดูลาร์ และมีประสิทธิภาพ ในบทความนี้ เราจะสำรวจประเภทของฟังก์ชันต่างๆ ในการทำแอพ Kotlin วิธีการประกาศและใช้งาน และแนวทางปฏิบัติที่ดีที่สุดสำหรับการเขียนฟังก์ชัน
ฟังก์ชั่นใน Kotlin คืออะไร?
ฟังก์ชันใน Kotlin เป็นบล็อกโค้ดที่ใช้ซ้ำได้ซึ่งทำงานบางอย่าง ฟังก์ชันสามารถไม่มีพารามิเตอร์หรือมีมากกว่าหนึ่งก็ได้ และสามารถส่งคืนค่า (return) หรือดำเนินการได้ Kotlin รองรับฟังก์ชันหลายประเภท รวมถึงฟังก์ชันระดับบนสุด ฟังก์ชันสมาชิก (top-level functions) ฟังก์ชันส่วนขยาย (member functions) และฟังก์ชันลำดับที่สูงกว่า (higher-order functions)
การประกาศฟังก์ชั่นใน Kotlin
ในการประกาศฟังก์ชันใน Kotlin ต้องระบุชื่อ พารามิเตอร์ (ถ้ามี) ประเภทการส่งคืน (ถ้ามี) และการใช้งาน ต่อไปนี้คือตัวอย่างฟังก์ชันง่ายๆ ที่รับจำนวนเต็มสองตัวและส่งคืนผลรวม:
fun sum(a: Int, b: Int): Int {
return a + b
}
ในตัวอย่างนี้ ฟังก์ชันมีชื่อว่า “sum” และรับพารามิเตอร์จำนวนเต็มสองตัวคือ “a” และ “b” ประเภทการส่งคืนคือ “Int” ซึ่งหมายถึงฟังก์ชันส่งคืนค่าจำนวนเต็ม การใช้งานฟังก์ชันเพียงแค่เพิ่มพารามิเตอร์สองตัวและส่งคืนผลลัพธ์
การเรียกใช้ฟังก์ชันใน Kotlin
เมื่อประกาศฟังก์ชันใน Kotlin แล้ว สามารถเรียกใช้จากส่วนอื่นๆ ของโค้ดได้ ในการเรียกใช้ฟังก์ชันต้องระบุชื่อและระบุพารามิเตอร์ที่จำเป็น ต่อไปนี้คือตัวอย่างการเรียกใช้ฟังก์ชัน “sum” จากฟังก์ชันอื่น:
fun main() {
val result = sum(3, 4)
println("The result is $result")
}
ในตัวอย่างนี้ ฟังก์ชัน “หลัก” เรียกใช้ฟังก์ชัน “sum” โดยมีอาร์กิวเมนต์จำนวนเต็มสองตัวคือ “3” และ “4” ผลลัพธ์ของฟังก์ชันจะถูกเก็บไว้ในตัวแปรชื่อ “result” ซึ่งจะพิมพ์ไปยังคอนโซลโดยใช้ฟังก์ชัน “println”
ประเภทของฟังก์ชันใน Kotlin
ดังที่ได้กล่าวไว้ก่อนหน้านี้ Kotlin รองรับการทำงานหลายประเภท ลองมาดูแต่ละชนิด:
1. ฟังก์ชัน Top-level
ฟังก์ชัน Top-level คือฟังก์ชันยูทิลิตี้ที่ทำงานเฉพาะและสามารถเรียกได้จากทุกที่ในโค้ด ซึ่งถูกกำหนดไว้นอกคลาสใดๆ และสามารถเข้าถึงได้โดยการ import ไฟล์ที่ถูกกำหนด ต่อไปนี้เป็นตัวอย่างของฟังก์ชัน Top-level ใน Kotlin:
fun greet(name: String) {
println("Hello, $name!")
}
ฟังก์ชันนี้ใช้พารามิเตอร์เดียว name และพิมพ์คำทักทายไปยังคอนโซล ในการเรียกใช้ฟังก์ชันนี้ เพียงแค่ระบุชื่อพารามิเตอร์:
greet("Alice") // Output: Hello, Alice!
ฟังก์ชัน Top-level มักใช้สำหรับฟังก์ชันยูทิลิตี้ที่ทำงานเฉพาะอย่าง เช่น การจัดรูปแบบข้อมูล การคำนวณ หรือการตรวจสอบอินพุต
2. ฟังก์ชัน Member
ฟังก์ชัน Member ถูกกำหนดภายในคลาสและสามารถเข้าถึงคุณสมบัติและเมธอดของคลาสได้ มักใช้เพื่อสรุปพฤติกรรมที่เฉพาะเจาะจงกับ object นี่คือตัวอย่างฟังก์ชัน Member ใน Kotlin
class Person(val name: String) {
fun introduce() {
println("Hi, my name is $name.")
}
}
ฟังก์ชันนี้ถูกกำหนดภายในคลาส Person และสามารถเข้าถึงคุณสมบัติ name ของคลาสได้ ในการเรียกใช้ฟังก์ชันนี้ต้องสร้างอินสแตนซ์ของคลาส Person:
val person = Person("Alice")
person.introduce() // Output: Hi, my name is Alice.
ฟังก์ชัน Member มักใช้เพื่อสรุปลักษณะการทำงานเฉพาะของ object เช่น การคำนวณอายุของบุคคล การจัดรูปแบบข้อมูลที่ติดต่อ หรือการดำเนินการบางอย่างกับข้อมูลของ object
3. ฟังก์ชัน Extension
ฟังก์ชันส่วนขยายถูกกำหนดไว้ภายนอกคลาส แต่สามารถเรียกได้ราวกับว่าฟังก์ชันเหล่านี้เป็นฟังก์ชันสมาชิกของคลาสนั้น มักใช้เพื่อขยายการทำงานของคลาสที่มีอยู่โดยไม่ต้องแก้ไขซอร์สโค้ด ต่อไปนี้เป็นตัวอย่างของฟังก์ชันส่วนขยายใน Kotlin:
fun Int.isEven(): Boolean {
return this % 2 == 0
}
ฟังก์ชันนี้ถูกกำหนดให้อยู่นอกคลาสใดๆ แต่สามารถเรียกได้ราวกับว่ามันเป็นฟังก์ชันสมาชิกของคลาส Int ในการเรียกใช้ฟังก์ชันนี้ เพียงแค่ต้องระบุค่า Int:
val number = 4
println(number.isEven()) // Output: true
ฟังก์ชันส่วนขยายมักใช้เพื่อเพิ่มฟังก์ชันให้กับคลาสที่มีอยู่โดยไม่ต้องแก้ไขซอร์สโค้ด ตัวอย่างเช่น อาจกำหนดฟังก์ชันส่วนขยายในคลาส String เพื่อให้อักษรตัวแรกของสตริงเป็นตัวพิมพ์ใหญ่ หรือฟังก์ชันส่วนขยายในคลาส List เพื่อดำเนินการบางอย่างกับองค์ประกอบของ List ได้
4. ฟังก์ชัน Higher-order
ฟังก์ชันลำดับที่สูงกว่าใช้ฟังก์ชันอื่นเป็นพารามิเตอร์ และมักใช้เพื่อสร้างอัลกอริทึมทั่วไปที่สามารถใช้กับข้อมูลประเภทต่างๆ ต่อไปนี้เป็นตัวอย่างของฟังก์ชันลำดับสูงกว่าใน Kotlin:
fun <T, R> List<T>.map(transform: (T) -> R): List<R> {
val result = mutableListOf<R>()
for (item in this) {
result.add(transform(item))
}
return result
}
ฟังก์ชันนี้ใช้ฟังก์ชันอื่น transform เป็นพารามิเตอร์ และใช้กับแต่ละ item ใน List ประเภท T จากนั้นจะส่งกลับรายการประเภท R ใหม่ที่มีผลลัพธ์ของการใช้ฟังก์ชันการแปลง หากต้องการใช้ฟังก์ชันนี้ ต้องระบุนิพจน์แลมบ์ดาที่กำหนดฟังก์ชันการแปลง
มาแยกย่อยโค้ดเพื่อทำความเข้าใจว่ามันทำอะไร:
fun <T, R> List<T>.map(transform: (T) -> R): List<R> {
บรรทัดนี้กำหนดฟังก์ชันทั่วไปที่เรียกว่า map ซึ่งรับพารามิเตอร์ประเภท T และ R สองตัว ฟังก์ชันนี้เป็นฟังก์ชันเสริมในคลาส List และใช้พารามิเตอร์เดียวที่เรียกว่า transform ซึ่งเป็นนิพจน์แลมบ์ดาที่แมปแต่ละองค์ประกอบในรายการประเภท T เป็นค่าใหม่ของประเภท R ฟังก์ชันส่งคืนรายการใหม่ของประเภท R ที่มีค่าที่แปลงแล้ว
transform: (T) -> R
(T) -> R เป็นประเภทฟังก์ชันใน Kotlin ที่ระบุลายเซ็นของนิพจน์แลมบ์ดา ในกรณีนี้ จะกำหนดแลมบ์ดาที่รับพารามิเตอร์เดียวประเภท T และส่งคืนค่าประเภท R
ในการประกาศฟังก์ชัน map การแปลงเป็นพารามิเตอร์ที่รับฟังก์ชันประเภทนี้ มันแสดงถึงฟังก์ชันที่จะใช้ในการแปลงแต่ละองค์ประกอบในรายการอินพุตของประเภท T เป็นค่าใหม่ของประเภท R เมื่อมีการเรียกใช้ฟังก์ชันแผนที่ นิพจน์แลมบ์ดาที่ถูกส่งผ่านเป็นอาร์กิวเมนต์จะต้องตรงกับ signature นี้
ตัวอย่างเช่น ถ้าเรามี List และต้องการแมปแต่ละอิลิเมนต์กับการแสดงสตริง เราสามารถใช้เมธอด toString() ของคลาส Int เป็นฟังก์ชันการแปลง ในกรณีนี้ signature ของนิพจน์แลมบ์ดาที่เราส่งผ่านจะเป็น (Int) -> string ซึ่งตรงกับลายเซ็น (T) -> R ของพารามิเตอร์การแปลงในฟังก์ชัน map
val result = mutableListOf<R>()
บรรทัดนี้สร้าง mutable list ของประเภท R ซึ่งจะเก็บค่าที่แปลงแล้ว
for (item in this) {
result.add(transform(item))
}
การวนซ้ำนี้จะวนซ้ำแต่ละรายการในรายการที่เรียกใช้ฟังก์ชันแผนที่และใช้นิพจน์แลมบ์ดาการแปลงกับแต่ละรายการ จากนั้นค่าการแปลงที่เป็นผลลัพธ์จะถูกเพิ่มลงในรายการผลลัพธ์
return result
สุดท้าย รายการผลลัพธ์ของค่าที่แปลงแล้วจะถูกส่งกลับเป็นเอาต์พุตของฟังก์ชัน map
ต่อไปนี้คือตัวอย่างการใช้ฟังก์ชัน map เพื่อแปลงรายการสตริงเป็น List ของ ความยาวตัวอักษรในข้อความ:
val words = listOf("apple", "banana", "cherry")
val lengths = words.map { it.length }
println(lengths) // Output: [5, 6, 6]
ในตัวอย่างนี้ เราเริ่มต้นด้วยรายการสตริงที่เรียกว่า words จากนั้นเราจะเรียกใช้ฟังก์ชัน mapในรายการนี้และส่งผ่านนิพจน์แลมบ์ดา { it.length } นิพจน์แลมบ์ดานี้ใช้แต่ละองค์ประกอบในรายการและส่งกลับความยาว จากนั้น ฟังก์ชัน map จะใช้นิพจน์แลมบ์ดานี้กับแต่ละองค์ประกอบใน List words และส่งคืนรายการความยาวใหม่ รายการผลลัพธ์ [5, 6, 6] ถูกกำหนดให้กับตัวแปรความยาวและพิมพ์ไปยังคอนโซล
5. Lambda expressions
นิพจน์แลมบ์ดาเป็นฟังก์ชันที่ไม่ระบุตัวตน (anonymous) ที่สามารถใช้เพื่อกำหนดฟังก์ชันแบบอินไลน์ มักใช้กับฟังก์ชันที่มีลำดับสูงกว่า (higher-order functions) เพื่อให้วิธีการกำหนดฟังก์ชันที่ส่งผ่านเป็นพารามิเตอร์ที่กระชับและอ่านง่าย นี่คือตัวอย่างของแลมบ์ดาใน Kotlin:
val square: (Int) -> Int = { x -> x * x }
นิพจน์แลมบ์ดานี้ใช้พารามิเตอร์เดียว x และส่งคืนกำลังสอง หากต้องการใช้นิพจน์แลมบ์ดานี้ เพียงแค่เรียกมันด้วยพารามิเตอร์จำนวนเต็ม:
println(square(5)) // Output: 25
นิพจน์แลมบ์ดามักใช้เพื่อกำหนดฟังก์ชันที่ส่งผ่านเป็นพารามิเตอร์ไปยังฟังก์ชันที่มีลำดับสูงกว่า เช่น map filter หรือ reduce
6. ฟังก์ชัน Infix
ฟังก์ชัน Infix เป็นฟังก์ชันสมาชิกที่มีพารามิเตอร์เดียวและถูกเรียกโดยใช้เครื่องหมาย infix โดยไม่ต้องใช้วงเล็บหรือจุด มักใช้เพื่อสร้างไวยากรณ์เหมือน DSL (Domain Specific Language) สำหรับการดำเนินการเฉพาะ ต่อไปนี้เป็นตัวอย่างของฟังก์ชัน infix อย่างง่ายใน Kotlin:
infix fun Int.plusTwo(): Int {
return this + 2
}
ฟังก์ชันนี้ใช้พารามิเตอร์จำนวนเต็มเดียวและส่งกลับค่าของมันบวกสอง หากต้องการเรียกใช้ฟังก์ชันนี้โดยใช้เครื่องหมาย infix เพียงแค่วางไว้ระหว่างตัวถูกดำเนินการจำนวนเต็มสองตัว:
val result = 3 plusTwo // same as 3.plusTwo()
println(result) // Output: 5
ฟังก์ชัน Infix มักใช้เพื่อสร้างไวยากรณ์ที่เหมือน DSL (Domain Specific Language) สำหรับการดำเนินการเฉพาะ เช่น การสร้าง API ที่คล่องแคล่วสำหรับการทำงานกับโมเดลโดเมนเฉพาะ
7. ฟังก์ชัน Tail-recursive
ฟังก์ชัน Tail-recursive คือฟังก์ชันที่เรียกตัวเองว่าเป็นการดำเนินการสุดท้ายในเส้นทางการดำเนินการ มักใช้เพื่อเพิ่มประสิทธิภาพอัลกอริทึมแบบเรียกซ้ำและป้องกันข้อผิดพลาด stack overflow ต่อไปนี้เป็นตัวอย่างของฟังก์ชัน tail-recursive อย่างง่ายใน Kotlin:
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n == 0) acc else factorial(n - 1, acc * n)
}
ฟังก์ชันนี้จะคำนวณแฟกทอเรียลของจำนวน n ที่กำหนด โดยใช้พารามิเตอร์ตัวสะสมเพื่อติดตามผลคูณที่กำลังทำงานอยู่ ในการเรียกใช้ฟังก์ชันนี้ เพียงระบุพารามิเตอร์จำนวนเต็ม:
println(factorial(5)) // Output: 120
ฟังก์ชันแบบ Tail-recursive มักใช้เพื่อเพิ่มประสิทธิภาพอัลกอริธึมแบบ recursive ซึ่งอาจส่งผลให้เกิดข้อผิดพลาด stack overflow ด้วยการเรียกตัวเองว่าเป็นการดำเนินการสุดท้ายในเส้นทางการดำเนินการ จะช่วยให้คอมไพลเลอร์เพิ่มประสิทธิภาพการเรียกใช้ฟังก์ชันเป็นลูป ลดปริมาณพื้นที่ stack ที่ต้องการ
ลองเปรียบเทียบฟังก์ชันแฟกทอเรียลใน Kotlin เวอร์ชันย่อยแบบเรียกซ้ำกับฟังก์ชันแฟกทอเรียลที่ไม่ใช่แบบเรียกซ้ำเพื่อดูความแตกต่างของประสิทธิภาพ
fun factorial(n: Int): Int {
return if (n == 0) 1 else n * factorial(n - 1)
}
เวอร์ชันนี้ใช้การดำเนินการคูณก่อนทำการเรียกซ้ำ ซึ่งหมายความว่าฟังก์ชันจำเป็นต้องติดตามผลลัพธ์ระหว่างกลางของการดำเนินการคูณแต่ละครั้งจนกว่าการเรียกซ้ำจะเสร็จสมบูรณ์ ผลก็คือ เวอร์ชันแบบ non-tail-recursive ใช้หน่วยความจำมากกว่าเวอร์ชันแบบ tail-recursive
ตอนนี้เรามาพิจารณาประสิทธิภาพของฟังก์ชันทั้งสองเวอร์ชันกัน เมื่อเราเรียกใช้ฟังก์ชันในเวอร์ชัน tail-recursive ด้วยตัวเลขจำนวนมาก ฟังก์ชันจะดำเนินการอย่างรวดเร็วและไม่ทำให้เกิดการล้นของสแต็ก เนื่องจากการปรับให้เหมาะสมโดยคีย์เวิร์ด tailrec อย่างไรก็ตาม เมื่อเราเรียกใช้ฟังก์ชันเวอร์ชัน non-tail-recursive ด้วยจำนวนมาก อาจทำให้พื้นที่สแต็กหมดลงอย่างรวดเร็วและทำให้เกิดข้อผิดพลาดสแต็กโอเวอร์โฟลว์
โดยรวมแล้ว ฟังก์ชัน tail-recursive สามารถทำงานได้ดีกว่าฟังก์ชัน non-tail-recursive เมื่อทำแอพในการจัดการกับชุดข้อมูลขนาดใหญ่ ฟังก์ชัน tail-recursive สามารถหลีกเลี่ยงข้อผิดพลาดสแต็กโอเวอร์โฟลว์และใช้หน่วยความจำได้อย่างมีประสิทธิภาพ ซึ่งสามารถสร้างความแตกต่างอย่างมีนัยสำคัญในประสิทธิภาพของการทำแอพ ดังนั้นจึงเป็นแนวปฏิบัติที่ดีเสมอที่จะใช้คำหลัก tailrec สำหรับฟังก์ชันเรียกซ้ำเมื่อใดก็ตามที่เป็นไปได้
Kotlin มีตัวเลือกมากมายสำหรับการกำหนดฟังก์ชัน ตั้งแต่ฟังก์ชัน top-level และฟังก์ชัน member ไปจนถึงฟังก์ชันส่วนขยาย (extension) ฟังก์ชัน higher-order นิพจน์ lambda ฟังก์ชัน infix และฟังก์ชัน tail-recursive ด้วยการเลือกประเภทของฟังก์ชันที่เหมาะสมสำหรับแต่ละสถานการณ์ จะสามารถทำแอพที่มีประสิทธิภาพ ใช้ซ้ำได้ และบำรุงรักษาได้ซึ่งอ่านและเข้าใจได้ง่าย