Tuesday, May 23, 2017

Tìm hiểu căn bản về Kotlin

Giới thiệu

Bây giờ, khi Apple đã thay thế Objective-C bằng Swift cho iOS, việc thiếu một ngôn ngữ hiện đại hơn để phát triển ứng dụng Android đã trở nên rõ ràng hơn.
Vì vậy, Kotlin - một ngôn ngữ dựa trên JVM đã được JetBrains cho ra đời. Bài viết sau đây sẽ tìm hiểu những điều căn bản nhất về một ngôn ngữ lập trình Kotlin.

Cú pháp cơ bản

Định nghĩa package

Đặc tả package được đặt ở đầu của file source code
package my.demo

import java.util.*

// ...
Không bắt buộc phải match thư mục và package: file source code có thể được đặt tuỳ tiện trong hệ thống file.

Định nghĩa function

Function có 2 tham số kiểu Int và trả về kết quả kiểu Int:
fun sum(a: Int, b: Int): Int {
    return a + b
}

fun main(args: Array<String>) {
    print("sum of 3 and 5 is ")
    println(sum(3, 5))
}
-----------
Kết quả: sum of 3 and 5 is 8
Function có kiểu kết quả trả về được suy ra từ body biểu thức:
fun sum(a: Int, b: Int) = a + b

fun main(args: Array<String>) {
    println("sum of 19 and 23 is ${sum(19, 23)}")
}
----------
Kết quả: sum of 19 and 23 is 42
Function trả về kết quả không có ý nghĩa:
fun printSum(a: Int, b: Int): Unit {
    println("sum of $a and $b is ${a + b}")
}

fun main(args: Array<String>) {
    printSum(-1, 8)
}
----------
Kết quả: sum of -1 and 8 is 7
Kiểu trả về Unit có thể được bỏ qua:
fun printSum(a: Int, b: Int) {
    println("sum of $a and $b is ${a + b}")
}

fun main(args: Array<String>) {
    printSum(-1, 8)
}
----------
Kết quả: sum of -1 and 8 is 7

Định nghĩa biến cục bộ (local)

Biến local chỉ gán giá trị một lần(biến chỉ đọc):
fun main(args: Array<String>) {
    val a: Int = 1  // immediate assignment
    val b = 2   // `Int` type is inferred
    val c: Int  // Type required when no initializer is provided
    c = 3       // deferred assignment
    println("a = $a, b = $b, c = $c")
}
----------
Kết quả: a = 1, b = 2, c = 3
Biến có thể thay đổi:
fun main(args: Array<String>) {
    var x = 5 // `Int` type is inferred
    x += 1
    println("x = $x")
}
----------
Kết quả: x = 6

Comment

Giống như Java và JavaScript, Kotlin hỗ trợ các kiểu end-of-line và block comments.
// This is an end-of-line comment

/* This is a block comment
   on multiple lines. */
Không giống Java, các block comment trong Kotlin có thể lồng nhau.

Sử dụng String template

fun main(args: Array<String>) {
    var a = 1
    // simple name in template:
    val s1 = "a is $a" 

    a = 2
    // arbitrary expression in template:
    val s2 = "${s1.replace("is", "was")}, but now is $a"
    println(s2)
}
----------
Kết quả: a was 1, but now is 2

Sử dụng các biểu thức có điều kiện

fun maxOf(a: Int, b: Int): Int {
    if (a > b) {
        return a
    } else {
        return b
    }
}

fun main(args: Array<String>) {
    println("max of 0 and 42 is ${maxOf(0, 42)}")
}
----------
Kết quả: max of 0 and 42 is 42
Sử dụng if như một biểu thức:
fun maxOf(a: Int, b: Int) = if (a > b) a else b

fun main(args: Array<String>) {
    println("max of 0 and 42 is ${maxOf(0, 42)}")
}
----------
Kết quả: max of 0 and 42 is 42

Sử dụng các giá trị nullable và check null

Trong kotlin, một tham chiếu phải được đánh dấu là nullable khi giá trị null là có thể.
Trả về giá trị null nếu str không giữ một số nguyên:
fun parseInt(str: String): Int? {
    // ...
}
Sử dụng function trả về giá trị nullable:
fun parseInt(str: String): Int? {
    return str.toIntOrNull()
}

fun printProduct(arg1: String, arg2: String) {
    val x = parseInt(arg1)
    val y = parseInt(arg2)

    // Using `x * y` yields error because they may hold nulls.
    if (x != null && y != null) {
        // x and y are automatically cast to non-nullable after null check
        println(x * y)
    }
    else {
        println("either '$arg1' or '$arg2' is not a number")
    }    
}

fun main(args: Array<String>) {
    printProduct("6", "7")
    printProduct("a", "7")
    printProduct("a", "b")
}
----------
Kết quả: 
42
either 'a' or '7' is not a number
either 'a' or 'b' is not a number

Kiểm tra và tự động ép kiểu

Toán tử is kiểm tra nếu một biểu thức là một thể hiện của một kiểu. Nếu một biến địa phương loại chỉ đọc hay một thuộc tính được kiểm tra cho một kiểu cụ thể thì không cần ép kiểu một cách rõ ràng:
fun getStringLength(obj: Any): Int? {
    if (obj is String) {
        // `obj` is automatically cast to `String` in this branch
        return obj.length
    }

    // `obj` is still of type `Any` outside of the type-checked branch
    return null
}


fun main(args: Array<String>) {
    fun printLength(obj: Any) {
        println("'$obj' string length is ${getStringLength(obj) ?: "... err, not a string"} ")
    }
    printLength("Incomprehensibilities")
    printLength(1000)
    printLength(listOf(Any()))
}
----------
Kết quả: 
'Incomprehensibilities' string length is 21 
'1000' string length is ... err, not a string 
'[java.lang.Object@6b884d57]' string length is ... err, not a string 

Sử dụng vòng lặp for

fun main(args: Array<String>) {
    val items = listOf("apple", "banana", "kiwi")
    for (item in items) {
        println(item)
    }
}
----------
Kết quả: 
apple
banana
kiwi
hoặc:
fun main(args: Array<String>) {
    val items = listOf("apple", "banana", "kiwi")
    for (index in items.indices) {
        println("item at $index is ${items[index]}")
    }
}
----------
Kết quả: 
item at 0 is apple
item at 1 is banana
item at 2 is kiwi

Sử dụng vòng lặp while

fun main(args: Array<String>) {
    val items = listOf("apple", "banana", "kiwi")
    var index = 0
    while (index < items.size) {
        println("item at $index is ${items[index]}")
        index++
    }
}
----------
Kết quả: 
item at 0 is apple
item at 1 is banana
item at 2 is kiwi

Sử dụng biểu thức when

fun describe(obj: Any): String =
when (obj) {
    1          -> "One"
    "Hello"    -> "Greeting"
    is Long    -> "Long"
    !is String -> "Not a string"
    else       -> "Unknown"
}

fun main(args: Array<String>) {
    println(describe(1))
    println(describe("Hello"))
    println(describe(1000L))
    println(describe(2))
    println(describe("other"))
}
----------
Kết quả: 
One
Greeting
Long
Not a string
Unknown

Sử dụng ranger (dãy ..)

Để kiểm tra một số có thuộc một dãy, sử dụng toán tử in:
fun main(args: Array<String>) {
    val x = 10
    val y = 9
    if (x in 1..y+1) {
        println("fits in range")
    }
}
----------
Kết quả: fits in range
Kiểm tra một số nằm ngoài dãy:
fun main(args: Array<String>) {
    val list = listOf("a", "b", "c")

    if (-1 !in 0..list.lastIndex) {
        println("-1 is out of range")
    }
    if (list.size !in list.indices) {
        println("list size is out of valid list indices range too")
    }
}
----------
Kết quả: 
-1 is out of range
list size is out of valid list indices range too

Sử dụng collection

Iterating trên một collection:
fun main(args: Array<String>) {
    val items = listOf("apple", "banana", "kiwi")
    for (item in items) {
        println(item)
    }
}
----------
Kết quả: 
apple
banana
kiwi
Kiểm tra nếu một collection có chứa một đối tượng, sử dụng toán tử in:
fun main(args: Array<String>) {
    val items = setOf("apple", "banana", "kiwi")
    when {
        "orange" in items -> println("juicy")
        "apple" in items -> println("apple is fine too")
    }
}
----------
Kết quả: apple is fine too
Sử dụng biểu thức lamda để filter và map collection:
fun main(args: Array<String>) {
    val fruits = listOf("banana", "avocado", "apple", "kiwi")
    fruits
    .filter { it.startsWith("a") }
    .sortedBy { it }
    .map { it.toUpperCase() }
    .forEach { println(it) }
}
----------
Kết quả:
APPLE
AVOCADO

Idioms

Tạo các DTOs (POJOs/POCOs)

data class Customer(val name: String, val email: String)
cung cấp một class Customer với các function sau:
  • getters (và setters trong trường hợp các biến var) cho tất cả các thuộc tính
  • equals()
  • hashCode()
  • toString()
  • copy()
  • component1()component2(), .. cho tất cả các thuộc tính

Giá trị mặc định cho các tham số của function

fun foo(a: Int = 0, b: String = "") { ... }

Lọc một danh sách

val positives = list.filter { x -> x > 0 }
hoặc cách khác ngắn gọn hơn:
val positives = list.filter { it > 0 }

String Interpolation

println("Name $name")

Check các thể hiện

when (x) {
    is Foo -> ...
    is Bar -> ...
    else   -> ...
}

Thuộc tính lazy

val p: String by lazy {
    // compute the string
}

Mở rộng function

fun String.spaceToCamelCase() { ... }

"Convert this to camelcase".spaceToCamelCase()

Tạo một singleton

object Resource {
    val name = "Name"
}

Check null ngắn gọn

val files = File("Test").listFiles()

println(files?.size)

Biểu thức try/catch

fun test() {
    val result = try {
        count()
    } catch (e: ArithmeticException) {
        throw IllegalStateException(e)
    }

    // Working with result
}

Biểu thức if

fun foo(param: Int) {
    val result = if (param == 1) {
        "one"
    } else if (param == 2) {
        "two"
    } else {
        "three"
    }
}

Kết luận

Trên đây là những điều cơ bản về cú pháp và các idiom của Kotlin. Hy vọng sẽ giúp các bạn tiếp cận, làm quen với ngôn ngữ lập trình mới này một cách căn bản và sơ khai nhất.

Những tính năng tuyệt vời làm tôi chọn Kotlin thay vì Java

Kotlin là chủ đề được nhắc đến nhiều nhất kể từ khi Google công bố việc hỗ trợ ngôn ngữ này trở thành 1 trong những ngôn ngữ chính thức để phát triển ứng dụng Android bên cạnh Java. Tuy đã được Google "bảo kê", tuy vậy chắc hẳn nhiều lập trình viên/PM vẫn còn do dự trong việc quyết định có sử dụng Kotlin vào những dự án của mình hay không. Qua bài viết này, tôi hi vọng các bạn sẽ thấy được những ưu thế vượt trội mà Kotlin mang đến cho chúng ta so với Java, qua đó sẽ đưa ra được lựa chọn cho bản thân mình.

Data class

Trong Java, chúng ta thường tạo ra các class chỉ để chứa data (JavaBean). Những class này được Kotlin thể hiện bằng data class:
data class Money(val amount: Int, val currency: String)
Chỉ với 1 dòng code như trên sẽ tương đương với 1 class trong Java như sau:
public class JavaMoney {
    private int amount;
    private String currency;

    public JavaMoney(int amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public int getAmount() {
        return amount;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }

    public String getCurrency() {
        return currency;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }

    @Override
    public boolean equals(Object obj) {
        return super.equals(obj);
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "JavaMoney{" +
                "amount=" + amount +
                ", currency='" + currency + '\'' +
                '}';
    }
}
Kotlin sẽ tự động thêm cho chúng ta các hàm utility như equals()hashCode()toString() và copy(). Đến đây có thể bạn sẽ nghĩ là như thế thì có gì đặc biệt trong khi những IDE tiên tiến bây giờ đều hỗ trợ việc tự động generate các hàm này? Đúng là như thế, nhưng mỗi khi bạn thêm 1 field vào trong class thì bạn sẽ phải quay lại và chỉnh sửa rất nhiều chỗ, và như thế thì rất mất công và mất thời gian.

null safety

Mọi thứ trong Kotlin không thể là null trừ khi bạn không muốn vậy. Type system của Kotlin được tạo ra nhằm loại bỏ null reference trong code, từ đó loại bỏ NPE - 1 trong những Exception phổ biến nhất trong Java.
Type system phân biệt những reference có thể null và reference không thể null. Ví dụ 1 biến thuộc kiểu Stringkhông thể null:
var a: String = "abc"
a = null // compilation error
Nếu bạn muốn nó là null, hãy sử dụng dấu ? vào sau kiểu
var b: String? = "abc"
b = null // ok
Safe call
Để tương tác với một object có thể null 1 cách dễ dàng, sử dụng dấu ? cho phép bạn lấy giá trị của object chỉ khi nó tồn tại, nếu không nó sẽ bỏ qua và chương trình sẽ vẫn chạy như thường:
val len = b?.length
Nếu bạn thật sự muốn trải nghiệm NPE thì hãy dùng !!, Kotlin sẽ cho phép code được compile.
val len = b!!.length

infix notation

1 trong những tính năng khá hay trong Kotlin là infix. Ví dụ khi chúng ta có 1 for loop trong java như sau:
for (int i = 0; i < list.size(); i++) {
  //do something
}
Chúng ta có phiên bản tương tự trong Kotlin như thế này:
for (index in 0 until list.size) {
  //do something
}
Như các bạn có thể thấy thì syntax trong đoạn code trên ở Kotlin dễ hiểu hơn rất nhiều: "Với index trong khoảng từ 0 cho đến size của list". until ở đây là 1 function có thêm infix notation được định nghĩa như sau:
infix fun Int.until(to: Int):
Thay vì phải viết là
0.until(list.size)
infix function cho phép chúng ta bỏ dấu . và () giúp code dễ đọc hơn:
0 until list.size

Extension functions - Inline functions

Đây chắc hẳn là 1 trong những tính năng sẽ dễ thuyết phục các bạn chuyển qua Kotlin nhất. Hãy tưởng tượng 1 ngày đẹp trời, bạn muốn loop qua các tất cả các child view trong 1 ViewGroup và làm cho nó biến mất chẳng hạn (invisible). Bình thường thì chúng ta sẽ dùng for loop với infix như trên:
val viewGroup = getViewGroup()
for (index in 0 until viewGroup.childCount) {
  val view = viewGroup.getChildAt(index)
  view.visibility = View.INVISIBLE
}
Tất nhiên là làm như vậy cũng được thôi, nhưng bạn lại muốn dùng forEach của Collection để làm code còn dễ đọc hơn nữa thì phải làm thế nào trong khi ViewGroup không hỗ trợ? Bình thường với Java bạn sẽ phải subclass ViewGroup sau đó thêm 1 hàm là forEach() vào class đó, tuy nhiên đối với Kotlin mọi thứ trở nên đơn giản hơn rất nhiều. Extension function trong Kotlin giúp cho bạn extend bất cứ 1 class hay type nào và thêm hàm vào chúng. Với trường hợp trên chúng ta sẽ làm như sau:

fun ViewGroup.forEach(action: (View) -> Unit) {
    for(index in 0 until childCount) {
        action(getChildAt(index))
    }
}
Và dùng như sau:
viewGroup.forEach { view -> view.visibility = View.INVISIBLE }
Vậy thì bí ẩn đằng sau extension function là gì? Thực chất thì extension function không làm thay đổi class mà nó extend. Mỗi khi bạn định nghĩa 1 extension, thực chất là bạn chỉ tạo ra 1 static method mà trong trường hợp trên thì ViewGroup chính là parameter đầu tiên.
Một điều thú vị khác, nếu bạn để ý thì ví dụ trên chính là 1 high-order function, nghĩa là 1 function nhận vào 1 function khác làm tham số đầu vào (action trong trường hợp trên). Khi hàm này được gọi đến, 1 anonymous class sẽ được khởi tạo để chứa đoạn code nằm trong lambda. Việc này thực chất sẽ gây tốn kém tài nguyên nếu bạn lạm dụng high-order function quá đà vì nó sẽ làm tăng method count.
Chúng ta có thể tránh việc khởi tạo anonymous class bằng cách đánh dấu inline cho extension function.

inline fun ViewGroup.forEach(action: (View) -> Unit) {
    for(index in 0 until childCount) {
        action(getChildAt(index))
    }
}
Vậy thì inline có tác dụng gì trong trường hợp này? Về cơ bản thì nó làm cho bytecode đc tạo ra ở nơi hàm được gọi đến sẽ chứa nội dung của hàm thay vì lời gọi đến hàm đó. Compiler từ đó sẽ cho ra output tương tự như sau khi forEach được gọi đến:
for(index in 0 until viewGroup.childCount) {
    viewGroup.getChildAt(index).visibility = View.INVISIBLE
}
Bởi vì đoạn code đã được compiler di chuyển đến nơi hàm được gọi nên nó sẽ không tạo ra anonymous class nữa, gián tiếp làm tăng performance. Tuy vậy bạn vẫn nên chú ý rằng số code generate ra sẽ tăng lên nếu dùng inline, cho nên đừng sử dụng nó với những function quá lớn nhé.

Operator function

Để tiếp tục nói về extension function, 1 tính năng thú vị khác của Kotlin là định nghĩa hàm có cách gọi đặc biệt. Lấy ví dụ bạn cần tạo 1 hàm trong ViewGroup để lấy ra 1 child view dựa theo index:
operator fun ViewGroup.get(index: Int): View? = getChildAt(index)
Sẽ được gọi ra như sau:
val firstView = viewGroup[0]
Operator function get làm cho hàm khi được dùng sẽ có syntax tương tự như khi ta truy cập biến trong 1 array (dùng [index] để gọi). Tương tự với các operator khác như:
operator fun ViewGroup.plusAssign(child: View) = addView(child)
operator fun ViewGroup.minusAssign(child: View) = removeView(child)
Nhờ operator chúng ta có 1 syntax ngắn gọn hơn:
viewGroup += firstView //Add view
viewGroup -= firstView //Remove view
Nhưng bạn vẫn có thể gọi cả tên hàm ra nếu muốn:
viewGroup.plusAssign(firstView) //Add view
viewGroup.minusAssign(firstView) //Remove view

Kết

Điểm tuyệt vời nhất khi chúng ta nói về Kotlin đó là việc bạn không cần phải commit 100% nếu quyết định chọn Kotlin. Bạn có thể vừa dùng Kotlin vừa dùng Java vì chúng tương thích với nhau hoàn toàn.
Phần lớn các ví dụ trên được tôi lấy từ bài thuyết trình của Hadi Hariri và Jake Wharton tại I/O 2017 vừa rồi. Trong video còn rất nhiều thứ mà tôi chưa đề cập đến trong bài viết nên hãy xem qua nhé.
Ngoài ra để học Kotlin 1 cách hiệu quả thì ngoài việc đọc reference còn 1 số resource mà tôi muốn chia sẻ:
Hẹn gặp lại các bạn vào bài viết sau, chúng ta sẽ thảo luận thêm về các chức năng hay ho khác của Kotlin.