หนึ่งในความสามารถน่ารักๆ ของภาษา Kotlin คือการใช้ lambda (หรือก็คือ Closure / Anonymous Function นั่นเอง ) จัดการกับข้อมูล Collection ประเภท List
ซึ่งคำสั่งที่ตัวภาษา build-in มาให้ที่ใช้ได้นั้นมีมากมายสุดๆ บล๊อกนี้เลยจะรวบรวมคำสั่งที่ใช้กับ List พร้อมตัวอย่างเอาไว้
คำสั่งหมวด Element
forEach / forEachIndexed
เป็นการวนลูปข้อมูลทุกตัวใน list ซึ่ง iterator ของข้อมูลแต่ละตัวจะแทนด้วยคีย์เวิร์ด it (ตามมาตราฐานของภาษา Kotlin)
สำหรับ forEach จะเป็นการวน element ทุกตัว แต่ถ้าต้องการ counter หรือตัวนับ index ด้วยสามารถเปลี่ยนไปใช้ forEachIndexed แทนได้ โดยการเขียนจะต้องรับ parameter 2 ตัวคือ key กับ value มา
val data = listOf(10, 20, 30, 40, 50) data.forEach { println(it) } //print: 10 20 30 40 50 val data = listOf(10, 20, 30, 40, 50) data.forEachIndexed { key, value -> println("$key = $value") } //print: 0 - 10 1 - 20 2 - 30 3 - 40 4 - 50
ในภาษา Kotlin การใช้ forEach ไม่จำกัดว่าจะต้องเขียนแบบ pure function (ฟังก์ชันที่ห้ามยุ่งกับตัวแปรหรือค่าภายนอก) ทำให้เราสามารถดึงค่านอก lambda มาใช้ด้วยได้ (แต่ก็ไม่ควรทำนะ ถึงมันจะทำได้ก็เถอะ)
val data = listOf(10, 20, 30, 40, 50) var x = 0 data.forEach { x += it } println(x) //150
elementAt / elementArOrElse / elementArOrNull
เป็นการขอข้อมูลในตัวแหน่งที่ระบุ จริงๆ มันก็เหมือนกับการ access List ด้วยการใช้ [ ] แบบธรรมดา แต่เรามักจะใช้กับกรณีที่ไม่แน่ใจว่าข้อมูลตำแหน่งนั้นมีอยู่จริงหรือเปล่า
val data = listOf(10, 20, 30, 40, 50) //elementAt จะได้คำตอบเหมือนการใช้ [ ] แบบปกติกับ array หรือ list เลย data.elementAt(0) //10 data[0] //10 //elementAtOrElse เป็นการใส่เงื่อนไขเพิ่มว่าถ้า index ที่ขอไปไม่มีอยู่ มันจะตอบ else กลับมาแทน //โดยการเขียน else จะอยู่ในรูป lambda สำหรับกรณีที่ต้องการใช้ index ด้วยสามารถอ้างได้ผ่านตัวแปร it data.elementAtOrElse(0) {-10} //10 data.elementAtOrElse(20) {-10} //-10 data.elementAtOrElse(25) { it + 5 } //25 //elementAtOrNull จะคล้ายๆ กับ OrElse แต่ถ้าไม่มี index นั้นอยู่ จะตอบเป็น null กลับมาแทน data.elementAtOrNull(0) //10 data.elementAtOrNull(20) //null
first / firstOrNull / last / lastOrNull
ใช้ในการขอข้อมูลตัวแรกสุด (หรือตัวหลังสุด) ของ List โดยข้อแตกต่างระหว่าง first กับ firstOrNull (และ last กับ lastOrNull) คือถ้าใช้แบบไม่มี OrNull แล้วไม่มีข้อมูลตัวนั้นขึ้นมาจะเจอ NoSuchElementException ตอนรันจริง
แต่นอกจากการข้อ first กับ last แบบธรรมดา เราสามารถใส่ lambda ลงไปได้ด้วย เช่น "ขอตัวแรกที่มีค่ามากกว่า 25"
val data = listOf(10, 20, 30, 40, 50) data.first() //10 data.last() //50 data.first { it > 25 } //30 data.last { it > 25 } //50 //แต่ถ้า list ไม่มีข้อมูลอยู่เลย เช่น val data = listOf<Int>() data.firstOrNull() //null data.firstOrNull() //null data.first() //Runtime Error: NoSuchElementException data.last() //Runtime Error: NoSuchElementException
indexOf / indexOfFirst / indexOfLast
คล้ายๆ กับ indexOf ในภาษา Javaคือค้นหาตำแหน่งของข้อมูลใน list (ตอบกลับเป็น index ตำแหน่งที่หาเจอ) ... ในกรณีที่หาข้อมูลตัวนั้นไม่เจอเลยจะ return ค่าเป็น -1
ส่วนข้อแตกต่างระหว่าง indexOf กับ indexOfFirst กับ indexOfLast คือ indexOf จะหาข้อมูลตัวนั้นตรงๆ เลย ... แต่ indexOfFirst กับ indexOfLast จะหาด้วยเงื่อนไข lambda
val data = listOf(10, 20, 30, 40, 50) data.indexOf(30) //2 data.indexOf(60) //-1 //หา index ของข้อมูลตัวแรกที่มากกว่า 35: ในที่นี้คือเจอ 40 (index: 3) data.indexOfFirst { it > 35 } //3 //หา index ของข้อมูลตัวแรกที่มากกว่า 35 จากด้านหลังของ list: ในที่นี้คือเจอ 50 (index: 4) data.indexOfLast { it > 35 } //4
single / singleOrNull
ใช้สำหรับการขอข้อมูลที่ต้องมีเพียงตัวเดียวใน list ถ้าข้อมูลที่ค้นหามีมากกว่า 1 ตัวจะ throw IllegalArgumentException ออกมา
listOf(100).single() //100 listOf(100, 200).single() //Runtime Error: IllegalArgumentException listOf(100, 200).single{ it > 150 } //200 listOf(100, 200, 300).single{ it > 150 } //Runtime Error: IllegalArgumentException
คำสั่งหมวด Aggregate
any
ใช้สำหรับเช็กว่าข้อมูลที่อยู่ในลิสต์มีอย่างน้อย 1 ตัวที่ตรงกับเงื่อนไขของเรา เราจะต้องเขียน lambda ในรูปของ condition true/false
val data = listOf(10, 20, 30, 40, 50) data.any { it > 25 } //true data.any { it > 100 } //false
all
เหมือนกับคำสั่ง any แต่เป็นการเช็กว่าข้อมูลในลิสต์ทุกตัวต้องตรงกับเงื่อนไข
val data = listOf(10, 20, 30, 40, 50) data.all { it > 5 } //true data.all { it > 25 } //false
none
เป็นส่วนกลับของ all คือแทนที่จะเช็กว่าข้อมูลทุกตัวตรงกับเงื่อนไข อันนี้จะเป็นการเช็กว่าไม่มีข้อมูลตัวไหนตรงกับเงื่อนไขแทน
val data = listOf(10, 20, 30, 40, 50) data.none{ it > 100 } //true data.none{ it > 10 } //false
count
นับจำนวนข้อมูลที่ตรงกับเงื่อนไข
val data = listOf(10, 20, 30, 40, 50) //นับข้อมูลทุกตัวที่ตรงกับเงื่อนไข data.count { it > 25 } //3 //แค่นับทุกตัวทั้งหมดเลย มีค่าเท่ากับการใช้ size data.count() //5 data.size //5
contains / containsAll
เช็กว่าใน list มีข้อมูลตัวนั้นๆ หรือเปล่า ถ้าเป็น contains จะหาแค่ 1 ตัว ส่วน containsAll จะหาแบบหลายตัวและต้องเจอทุกตัวถึงจะตรงเงื่อนไข
val data = listOf(10, 20, 30, 40, 50) data.contains(10) //true data.contains(100) //false data.containsAll( listOf(10, 20, 30) ) //true data.containsAll( listOf(40, 50, 60) ) //false
max / maxBy / min / minBy
เป็นการหาค่ามากที่สุด (หรือน้อยที่สุด) ใน List โดยเราสามารถใส่เงื่อนไขการหาแบบ lambda ได้ด้วย maxBy (และminBy)
val data = listOf(10, 20, 30, 40, 50) data.min() //10 data.max() //50 data.minBy{ -it } //50 data.maxBy{ -it } //10
หรือถ้า List ของเราเป็นตัวแปรชนิดอื่น min กับ max จะทำงานเมื่อ class นั้น implements มาจาก Comparable (แบบเดียวกับ Java เลย)
data class Foo(var x:Int = 0): Comparable<Foo>{ override fun compareTo(other: Foo): Int { return x - other.x } } val data = listOf(Foo(1), Foo(2). Foo(3)) data.min() //Foo(x=1) data.max() //Foo(x=3
sum / sumBy
หาผลรวมค่าทั้งหมดใน List หรือใช้แบบระบุตัวที่จะให้ sum ผ่าน lambda ด้วย sumBy
val data = listOf(10, 20, 30, 40, 50) data.sum() //150 //หรือถ้า list เป็น object data class Foo(var x:Int, var y:Int) val data = listOf( Foo(1, 10), Foo(2, 20), Foo(3, 30) ) //sum ด้วยค่า x data.sumBy{ it.x } //6 //sum ด้วยค่า y data.sumBy{ it.y } //60
หมวด Map
map / mapIndexed
เป็นการแปลงข้อมูลทุกตัวด้วยเงื่อนไขแบบเดียวกัน ถ้าต้องการ index ด้วยให้ใช้ mapIndexed
val data = listOf(10, 20, 30, 40, 50) data.map{ it / 10 } //[1, 2, 3, 4, 5] data.map{ it + 2 } //[12, 22, 32, 42, 52] data.mapIndexed{ key, value -> key } //[0, 1, 2, 3, 4]
mapNotNull
ใช้ในกรณีที่ List นั้นมีสมาชิกบางตัวที่เป็น null แล้วไม่ต้องการยุ่งกับค่าพวกนั้น
val data = listOf(10, 20, 30, null, 50, null, 70) data.mapNotNull{ it } //[10, 20, 30, 50, 70]
flatMap
ปกติการ map จะมีจำนวนข้อมูล input เท่ากับ output เช่นลิสต์เดิมมี 10 ตัว ลิสต์ใหม่ที่ได้หลัง map ก็จะได้ 10 ตัว
แต่ถ้าเป็น flatMap จะเป็นการขยายข้อมูล output เช่นถ้าข้อมูลเข้า 10 ตัวอาจจะทำให้มีข้อมูลออก 20 ตัว (ถ้าต้องการให้ output มีจำนวนน้อยลงจะใช้ filter)
val data = listOf(1, 2, 3) data.flatMap{ listOf(it, it * 10) } //[1, 10, 2, 20, 3, 30] //map ข้อมูลแต่ละตัวเป็น [it, it *10] คือถ้าข้อมูลเป็น 1, 2, 3 //ผลก็จะออกมาเป็น [1, 10] และ [2, 20] และ [3, 30] แล้วค่อยเอามาต่อกันเป็นลิสต์ใหม่
หมวด Filter และการ Sub-list
filter / filterNot
ใช้ในการกรองข้อมูลใน List ให้เหลือแค่ตัวที่ตรงตามเงื่อนไข ส่วน filterNot จะเป็นตัวที่ไม่ตรงเงื่อนไขแทน
val data = listOf(10, 20, 30, 40, 50) //เลือกเฉพาะข้อมูลที่มากกว่า 25 data.filter{ it > 25 } //[30, 40, 50] //เลือกเฉพาะข้อมูลที่'ไม่'มากกว่า 25 data.filterNot{ it > 25 } //[10, 20] data.filter{ it <= 25 } //[10, 20]
filterNotNull
เหมือนกับ filter แต่จะเลือกเฉพาะตัวที่ไม่ใช่ null มาคิด
val data = listOf(10, 20, 30, null, 50) //เลือกเฉพาะข้อมูลที่มากกว่า 25 data.filter{ it > 25 } //[30, 50]
subList
ใช้ตัดส่วนของ List ออกมา parameter แรกคือจุดเริ่ม ส่วน parameter ที่สองคือจุดสิ้นสุด (ไม่รวมตัวนั้น)
val data = listOf(10, 20, 30, 40, 50) data.subList(0, 1) //[10] data.subList(0, 2) //[10, 20] data.subList(3, 5) //[40, 50] data.subList(0, 6) //Runtime Error: IndexOutOfBoundsException
take / takeLast / takeWhile
เป็นรูปย่อของ subList ในกรณีที่ต้องการแค่ x ตัว take จะเริ่มนับจากหัวแถว, takeLast จะเริ่มนับจากท้ายแถว, ส่วน takeWhile จะเอาไปเรื่อยๆ ถ้ายังตรงเงื่อนไขใน lambda
val data = listOf(10, 20, 30, 40, 50) data.take(3) //[10, 20, 30] data.takeLast(3) //[30, 40, 50] data.takeWhile{ it < 25} //[10, 20]
drop / dropLast / dropWhile
คล้ายๆ กับ take แต่อันนี้จะเป็นการลบข้อมูลออกแทน (พูดง่ายๆ คือส่วนกลับของ take)
val data = listOf(10, 20, 30, 40, 50) data.drop(3) //[40, 50] data.dropLast(3) //[10, 20] data.dropWhile{ it < 25} //[30, 40, 50]
slice
ใช้สำหรับเลือกข้อมูลออกมา จัดตามลำดับ index ที่ระบุให้ไป
val data = listOf(10, 20, 30, 40, 50) //ขอข้อมูลตัวที่ 2, 3, 0 ตามลำดับ data.slice( listOf(2, 3, 0) ) //[30, 40, 10]
หมวด Reduce
fold / foldRight
ใช้เพื่อลดข้อมูลทั้ง List ให้เหลือแค่ค่าเดียวด้วยวิธีการที่ระบุใน lambda โดย fold จะเริ่มคำนวณค่าทีละคู่จากด้านหัวแถวไปจนถึงท้ายแถว (parameter แรกคือค่าเริ่มต้น) ส่วน foldRight จะคิดเหมือนกันแค่คิดจากด้านท้ายแถวมาด้านหน้า
val data = listOf(1, 2, 3) data.fold(0) { total, x -> total + x } //6 //เริ่มที่ 0 จากนั้นไล่บวกค่าทีละตัว data.fold(100) { total, x -> total - x } //94 //เริ่มที่ 100 จากนั้นไล่ลบตัวเลขทีละตัวใน list
reduce / reduceRight
เหมือนกับ fold แต่จะไม่มีการกำหนดค่าเริ่มต้น ... reduce จะใช้ค่าตัวแรกใน List เป็นค่าเริ่มต้นแทน
val data = listOf(10, 5, 1) data.reduce{ total, x -> total + x } //16 //ไล่บวกค่าทีละตัว 10+5 = 15 แล้ว 15+1 = 16 data.reduce{ total, x -> total - x } //4 //ไล่ลบเลขทีละตัวจากด้านหัวแถว เริ่มจาก 10-5 และเอาไป -1 ต่อ data.reduceRight{ total, x -> total - x } //-14 //ไล่ลบตัวเลขทีละตัวใน list เริ่มจากด้านขวา 1-5 = -4 แล้ว -4-10 = -14
หมวดการจัดกลุ่ม List
partition
สำหรับใช้จัดกลุ่มข้อมูล แยกตามเงื่อนไขใน lambda (ต้องเป็นเงื่อนไข boolean เท่านั้น) ... โดยจะแยกข้อมูลออกเป็นข้อมูลชนิด Pair ข้อมูลที่ตรงกับ true จะอยู่ในกลุ่มแรก นอกนั้นจะอยู่กลุ่มที่สอง
val data = listOf(1, 2, 3, 4, 5) val separate = data.partition{ it % 2 == 0 } //([2, 4], [1, 3, 5]) //separate จะเป็นข้อมูลประเภท Pair เข้าถึงได้ด้วย .first และ .second separate.first //[2,4] separate.second //[1,3,5]
groupBy
สำหรับใช้จัดกลุ่มข้อมูล เหมือนกับ partition แต่สามารถจัดกลุ่มได้มากกว่า 2 กลุ่ม
val data = listOf(1, 2, 3, 4, 5) //จัดกลุ่มที่มากกว่า 2 (ได้2กลุ่มคือ true, false) data.groupBy{ it > 2 } //(true=[1, 2], false=[3, 4, 5]) //จัดกลุ่มตามผลหาร (ได้3กลุ่มคือ 0, 1, 2) data.groupBy{ it / 2 } //(0=[1], 1=[2, 3], 2=[4, 5])
concat
การต่อ List ใน Kotlin นั้นทำได้ง่ายๆ ด้วยเครื่องหมาย +
val data = listOf(1, 2, 3) data + listOf(4, 5, 6) //[1, 2, 3, 4, 5, 6]
zip
ใช้เพื่อผสาน List 2 ตัวเข้าด้วยกันแบบ "ตัว-ต่อ-ตัว" ถ้าลิสต์ 2 ตัวมีขนาดไม่เท่ากัน จะยึดตัวสั้นกว่าเป็นเกณฑ์ ... ข้อมูลในลิสต์ทั้ง 2 ตัวไม่จำเป็นต้องเป็นชนิดเดียวกันก็ได้ ผลที่ได้จะออกมาเป็น Pair เสมอ
val data = listOf(1, 2, 3, 4) data.zip( listOf(5, 6, 7, 8) ) //[(1, 5), (2, 6), (3, 7), (4, 8)] data.zip( listOf("A", "B") ) //[(1, A), (2, B)]
ส่วนใหญ่ zip ไม่หากรณีใช้งานค่อยข้างยาก มักใช้กับ foreach ที่ต้องการวนลูป List 2 ตัวพร้อมๆ กันทีละ indexๆ
val dataA = listOf("A", "B", "C", "D") val dataB = listOf(1, 2, 3, 4) for( (a, b) in dataA.zip(dataB) ){ println("$a $b") } //print: A 1 B 2 C 3 D 4
reverse
ใช้เพื่อกลับ List จากหัวไปท้าย
val data = listOf(10, 20, 30, 40, 50) data.reverse() //[50, 40, 30, 20, 10]
หมวดการเรียงข้อมูล
sorted / sortedDecending
ใช้เพื่อเรียงข้อมูล จากน้อยไปมาก (และใช้ sortedDecending เมื่ออยากเรียงจากมากไปน้อย) ถ้าข้อมูลเป็น object จะเรียงได้เมื่อ class นั้น implements มาจาก Comparable (แบบเดียวกับ Java เลย)
val data = listOf(5,3,4,1,2) data.sorted() //[1, 2, 3, 4, 5] data.sortedDecending() //[5, 4, 3, 2, 1] //หรือถ้าเป็น object data class Foo(var x:Int): Comparable<Foo>{ override fun compareTo(other: Foo): Int { return x - other.x } } val data = listOf( Foo(5), Foo(3), Foo(8) ) data.sorted() //[Foo(x=3), Foo(x=3), Foo(x=8)]
sortedBy / sortedDecendingBy
เป็นการเรียงแบบใส่เงื่อนไขว่าจะใช้ attribute อะไรเป็นหลักในการเรียง
val data = listOf(5, 3, 4, 1, 2) data.sortedBy{ -it } //[5, 4, 3, 2, 1] //หรือถ้าเป็น object data class Foo(var x:Int, var y:Int, var z:Int) val data = listOf( Foo(1,2,3), Foo(2,1,3), Foo(3,2,1) ) data.sortedBy{ it.x } //[Foo(x=1, y=2, z=3), Foo(x=2, y=1, z=3), Foo(x=3, y=2, z=1)] data.sortedBy{ it.y } //[Foo(x=2, y=1, z=3), Foo(x=3, y=2, z=1), Foo(x=1, y=2, z=3)]
sortWith
เรียงข้อมูลโดยกำหนดว่าให้เรียงตามฟิลด์ไหนบ้างตามลำดับ ส่วนใหญ่จะใช้กับฟังก์ชันช่วยเหลือของ Kotlin คือ compareBy จากนั้นเราจะกำหนดว่าจะให้เทียบข้อมูลด้วยฟิลด์ไหน (ถ้าค่าเท่ากับ จะใช้ฟิลด์ต่อไปเช็กแทน) ถ้าเทียบกับ Java คือการใช้คลาส Compactor ในการเปรียบเทียบค่อ
data class Foo(var x:Int, var y:Int, var z:Int) val data = listOf( Foo(1,2,3), Foo(1,2,4), Foo(2,3,5) ) data2.sortedWith(compareBy( {it.x}, {it.y}, {it.z} )) //[Foo(x=1, y=2, z=3), Foo(x=1, y=2, z=4), Foo(x=2, y=3, z=5)]