Học ngôn ngữ lập trình Kotlin

Kotlin là một ngôn ngữ lập trình được các nhà phát triển Android sử dụng rộng rãi ở mọi lĩnh vực. Chủ đề này đóng vai trò là khoá học nhanh về Kotlin để giúp bạn thiết lập và sử dụng nhanh chóng.

Khai báo biến

Kotlin sử dụng hai từ khoá để khai báo các biến: valvar.

  • Sử dụng val cho biến có giá trị không bao giờ thay đổi. Bạn không thể gán lại giá trị cho biến đã khai báo bằng val.
  • Sử dụng var cho biến có giá trị có thể thay đổi.

Trong ví dụ bên dưới, count là biến thuộc loại Int được gán giá trị ban đầu là 10:

var count: Int = 10

Int là loại dữ liệu biểu thị một số nguyên, là một trong nhiều loại dạng số có thể biểu thị bằng Kotlin. Giống như các ngôn ngữ khác, bạn cũng có thể sử dụng Byte, Short, Long, FloatDouble tuỳ thuộc vào dữ liệu dạng số của mình.

Từ khoá var có nghĩa là bạn có thể gán lại các giá trị cho count nếu cần. Ví dụ: bạn có thể thay đổi giá trị của count từ 10 thành 15:

var count: Int = 10
count = 15

Tuy nhiên, bạn không được thay đổi một số giá trị. Hãy cân nhắc dùng một String có tên là languageName. Nếu muốn đảm bảo rằng languageName luôn có giá trị "Kotlin", thì bạn có thể khai báo languageName bằng cách sử dụng từ khoá val:

val languageName: String = "Kotlin"

Các từ khoá này cho phép bạn nêu rõ những gì có thể thay đổi. Hãy tận dụng các từ khoá này nếu cần. Nếu một tham chiếu biến phải có khả năng chỉ định lại giá trị, thì hãy khai báo tham chiếu đó là var. Nếu không, hãy sử dụng val.

Suy luận loại dữ liệu

Tiếp tục ví dụ trước, khi bạn chỉ định giá trị ban đầu cho languageName, trình biên dịch Kotlin có thể suy ra loại dữ liệu dựa trên loại giá trị được chỉ định.

Vì giá trị của "Kotlin" là thuộc loại String, nên trình biên dịch suy ra rằng languageName cũng là một String. Xin lưu ý rằng Kotlin là một ngôn ngữ loại tĩnh. Điều này có nghĩa là loại dữ liệu này sẽ được phân giải tại thời điểm biên dịch và không bao giờ thay đổi.

Trong ví dụ sau, languageName được suy ra là String. Vì vậy, bạn không thể gọi bất kỳ hàm nào không thuộc lớp String:

val languageName = "Kotlin"
val upperCaseName = languageName.toUpperCase()

// Fails to compile
languageName.inc()

toUpperCase() là một hàm chỉ có thể được gọi trên các biến thuộc loại String. Vì trình biên dịch Kotlin đã suy ra languageNameString, nên bạn có thể gọi toUpperCase() một cách an toàn. Tuy nhiên, inc() là hàm toán tử Int nên không thể gọi hàm này trên String. Phương pháp suy luận loại dữ liệu của Kotlin giúp mã của bạn súc tích và an toàn cho loại dữ liệu.

Kiểm tra an toàn của giá trị rỗng

Ở một số ngôn ngữ, bạn có thể khai báo biến thuộc loại tham chiếu mà không cung cấp giá trị rõ ràng ban đầu. Trong những trường hợp này, các biến thường chứa một giá trị rỗng. Theo mặc định, các biến Kotlin không thể chứa giá trị rỗng. Điều này có nghĩa là đoạn mã sau đây không hợp lệ:

// Fails to compile
val languageName: String = null

Để chứa được giá trị rỗng, biến phải thuộc loại có thể nhận giá trị rỗng (nullable). Bạn có thể chỉ định một biến là có thể mang giá trị rỗng bằng cách thêm ? vào hậu tố của loại dữ liệu đó, như trong ví dụ sau:

val languageName: String? = null

Với loại String?, bạn có thể gán giá trị String hoặc null cho languageName.

Bạn phải xử lý cẩn thận các biến có thể nhận giá trị rỗng hoặc có nguy cơ bị lỗi NullPointerException. Ví dụ: trong Java, nếu bạn cố gọi một phương thức trên một giá trị rỗng, thì chương trình của bạn sẽ gặp sự cố.

Kotlin cung cấp một số cơ chế để xử lý an toàn các biến có thể nhận giá trị rỗng. Để biết thêm thông tin, hãy xem Các mẫu Kotlin phổ biến trên Android: Tính chất rỗng.

Câu lệnh có điều kiện

Kotlin có một số cơ chế để triển khai logic có điều kiện. Cách phổ biến nhất trong số này là câu lệnh if-else. Nếu một biểu thức được bao bọc trong dấu ngoặc vuông bên cạnh một từ khoá if có giá trị là true, thì mã trong nhánh đó (tức là mã đứng sau được bọc trong dấu ngoặc nhọn) sẽ thực thi. Nếu không, mã trong nhánh else sẽ được thực thi.

if (count == 42) {
    println("I have the answer.")
} else {
    println("The answer eludes me.")
}

Bạn có thể biểu thị nhiều điều kiện bằng cách sử dụng else if. Cách này cho phép bạn biểu thị logic chi tiết và phức tạp hơn trong một câu lệnh đơn có điều kiện, như trong ví dụ sau:

if (count == 42) {
    println("I have the answer.")
} else if (count > 35) {
    println("The answer is close.")
} else {
    println("The answer eludes me.")
}

Câu lệnh có điều kiện rất hữu ích trong việc thể hiện logic trạng thái, nhưng có thể bạn sẽ nhận ra mình phải lặp lại mã khi viết. Trong ví dụ trên, bạn chỉ cần xuất String ở mỗi nhánh ra màn hình. Để tránh phải lặp lại mã, Kotlin cung cấp các biểu thức có điều kiện. Bạn có thể viết lại ví dụ trước như sau:

val answerString: String = if (count == 42) {
    "I have the answer."
} else if (count > 35) {
    "The answer is close."
} else {
    "The answer eludes me."
}

println(answerString)

Mỗi nhánh có điều kiện sẽ ngầm trả về kết quả của biểu thức trên dòng cuối cùng, vì vậy, bạn không cần sử dụng từ khoá return. Do kết quả của cả 3 nhánh đều thuộc loại String, nên kết quả của biểu thức if-else cũng thuộc loại String. Trong ví dụ này, answerString được gán một giá trị ban đầu từ kết quả của biểu thức if-else. Bạn có thể dùng thông tin suy luận kiểu để bỏ mục khai báo rõ ràng cho kiểu của answerString, nhưng bạn nên đưa thông tin đó vào cho minh bạch.

Khi độ phức tạp của câu lệnh có điều kiện tăng lên, bạn có thể cân nhắc thay thế biểu thức if-else bằng biểu thức when, như trong ví dụ sau:

val answerString = when {
    count == 42 -> "I have the answer."
    count > 35 -> "The answer is close."
    else -> "The answer eludes me."
}

println(answerString)

Mỗi nhánh trong biểu thức when được biểu thị bằng một điều kiện, một mũi tên (->) và một kết quả. Nếu điều kiện ở phía bên trái của mũi tên đạt giá trị true, thì kết quả của biểu thức ở bên phải sẽ được trả về. Xin lưu ý rằng quá trình thực thi không nói tiếp từ nhánh này sang nhánh kia. Mã trong ví dụ về biểu thức when có chức năng tương đương với mã trong ví dụ trước nhưng được đánh giá là dễ đọc hơn.

Các câu điều kiện của Kotlin làm nổi bật một tính năng mạnh mẽ hơn của ngôn ngữ này, đó là smart casting (truyền thông minh). Thay vì sử dụng toán tử an toàn cho lệnh gọi hoặc toán tử xác nhận khác rỗng để làm việc với các giá trị có thể rỗng, bạn có thể kiểm tra xem một biến có chứa tham chiếu đến giá trị rỗng hay không bằng cách sử dụng câu lệnh có điều kiện, như trong ví dụ sau:

val languageName: String? = null
if (languageName != null) {
    // No need to write languageName?.toUpperCase()
    println(languageName.toUpperCase())
}

Trong nhánh có điều kiện, languageName có thể được coi là không thể nhận giá trị rỗng. Kotlin đủ thông minh để nhận ra rằng điều kiện để thực thi nhánh là languageName không chứa giá trị rỗng. Vì vậy, bạn không phải coi languageName là có thể rỗng trong nhánh đó. Smart casting hoạt động để kiểm tra giá trị rỗng, kiểm tra loại hoặc bất kỳ điều kiện nào đáp ứng contract.

Hàm

Bạn có thể nhóm một hoặc nhiều biểu thức vào một hàm. Thay vì lặp lại cùng một chuỗi biểu thức mỗi khi cần kết quả, bạn có thể bao bọc biểu thức trong một hàm và gọi hàm đó.

Để khai báo hàm, hãy sử dụng từ khoá fun đứng trước tên hàm. Tiếp theo, hãy xác định các loại dữ liệu đầu vào mà hàm của bạn lấy (nếu có) và khai báo loại dữ liệu đầu ra mà hàm trả về. Phần body của hàm là nơi bạn xác định biểu thức được gọi khi hàm được kích hoạt.

Dựa trên các ví dụ trước, dưới đây là một hàm Kotlin hoàn chỉnh:

fun generateAnswerString(): String {
    val answerString = if (count == 42) {
        "I have the answer."
    } else {
        "The answer eludes me"
    }

    return answerString
}

Hàm trong ví dụ trên có tên generateAnswerString. Hàm này không lấy dữ liệu đầu vào nào. Hàm này xuất ra kết quả thuộc loại String. Để gọi một hàm, hãy sử dụng tên của hàm đó, theo sau là toán tử kích hoạt (()). Trong ví dụ dưới đây, biến answerString được khởi tạo bằng kết quả từ generateAnswerString().

val answerString = generateAnswerString()

Các hàm có thể lấy đối số làm dữ liệu đầu vào, như trong ví dụ sau:

fun generateAnswerString(countThreshold: Int): String {
    val answerString = if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }

    return answerString
}

Khi khai báo một hàm, bạn có thể chỉ định số lượng đối số và loại đối số bất kỳ. Trong ví dụ trên, generateAnswerString() lấy một đối số có tên là countThreshold thuộc loại Int. Trong hàm, bạn có thể tham chiếu đến đối số bằng cách sử dụng tên của đối số đó.

Khi gọi hàm này, bạn phải bao gồm một đối số trong dấu ngoặc đơn của lệnh gọi hàm:

val answerString = generateAnswerString(42)

Đơn giản hoá việc khai báo hàm

generateAnswerString() là một hàm khá đơn giản. Hàm này khai báo một biến rồi trả về ngay lập tức. Khi kết quả của một biểu thức đơn được trả về từ một hàm, bạn có thể bỏ qua việc khai báo biến cục bộ bằng cách trực tiếp trả về kết quả của biểu thức if-else có trong hàm, như trong ví dụ sau:

fun generateAnswerString(countThreshold: Int): String {
    return if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }
}

Bạn cũng có thể thay thế từ khoá trả lại bằng toán tử chỉ định:

fun generateAnswerString(countThreshold: Int): String = if (count > countThreshold) {
        "I have the answer"
    } else {
        "The answer eludes me"
    }

Hàm ẩn danh

Không phải hàm nào cũng cần đặt tên. Một số hàm được xác định trực tiếp bằng dữ liệu đầu vào và đầu ra. Những hàm này được gọi là hàm ẩn danh. Bạn có thể giữ lại tham chiếu đến một hàm ẩn danh bằng cách sử dụng tham chiếu này để gọi hàm ẩn danh sau này. Bạn cũng có thể truyền tham chiếu trong khắp ứng dụng của mình, giống như các loại tham chiếu khác.

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

Giống như các hàm có tên, hàm ẩn danh có thể chứa số lượng biểu thức bất kỳ. Giá trị được trả về của hàm là kết quả của biểu thức cuối cùng.

Trong ví dụ trên, stringLengthFunc chứa tham chiếu đến một hàm ẩn danh. Hàm ẩn danh này lấy String làm dữ liệu đầu vào, rồi trả về dữ liệu đầu ra là độ dài của dữ liệu đầu vào String, thuộc loại Int. Vì vậy, loại hàm được biểu thị là (String) -> Int. Tuy nhiên, mã này không có khả năng kích hoạt hàm đó. Để truy xuất kết quả của hàm, bạn phải kích hoạt hàm này như cách bạn thực hiện với hàm có tên. Bạn phải cung cấp String khi gọi stringLengthFunc, như trong ví dụ sau:

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

val stringLength: Int = stringLengthFunc("Android")

Hàm bậc cao

Một hàm có thể nhận một hàm khác làm đối số. Các hàm sử dụng các hàm khác làm đối số được gọi là hàm bậc cao hơn. Mẫu này rất hữu ích trong việc giao tiếp giữa các thành phần, giống như cách bạn sử dụng giao diện gọi lại trong Java.

Dưới đây là ví dụ về một hàm bậc cao:

fun stringMapper(str: String, mapper: (String) -> Int): Int {
    // Invoke function
    return mapper(str)
}

Hàm stringMapper() lấy String cùng với một hàm lấy giá trị Int từ String mà bạn truyền vào.

Bạn có thể gọi stringMapper() bằng cách truyền String và một hàm thoả mãn thông số đầu vào còn lại, tức là một hàm lấy String làm dữ liệu đầu vào và xuất Int. như trong ví dụ sau:

stringMapper("Android", { input ->
    input.length
})

Nếu hàm ẩn danh là tham số cuối được xác định trong một hàm, thì bạn có thể truyền hàm đó bên ngoài dấu ngoặc đơn dùng để kích hoạt hàm, như trong ví dụ sau:

stringMapper("Android") { input ->
    input.length
}

Các hàm ẩn danh có trong thư viện chuẩn của Kotlin. Để biết thêm thông tin, hãy xem bài viết Các hàm bậc cao và Lambda.

Lớp

Tất cả các loại dữ liệu được đề cập đến nay đều được tích hợp vào ngôn ngữ lập trình Kotlin. Nếu muốn thêm loại của riêng mình, bạn có thể xác định một lớp bằng cách sử dụng từ khoá class như trong ví dụ sau:

class Car

Thuộc tính

Lớp biểu thị trạng thái bằng thuộc tính. Thuộc tính là một biến ở cấp độ lớp, nó có thể bao gồm một phương thức getter, setter và trường dự phòng. Giống như ô tô phải có bánh xe mới chạy được, bạn có thể thêm danh sách các đối tượng Wheel dưới dạng một thuộc tính của Car, như trong ví dụ sau:

class Car {
    val wheels = listOf<Wheel>()
}

Xin lưu ý rằng wheels là một public val, có nghĩa là bạn có thể truy cập vào wheels từ bên ngoài lớp Car và bạn không thể chỉ định lại thuộc tính này. Nếu muốn có một thực thể của Car, trước tiên, bạn phải gọi hàm dựng của thực thể đó. Từ đó, bạn có thể truy cập mọi thuộc tính cho phép truy cập.

val car = Car() // construct a Car
val wheels = car.wheels // retrieve the wheels value from the Car

Nếu muốn điều chỉnh bánh xe của mình, bạn có thể xác định một hàm dựng tuỳ chỉnh có nhiệm vụ chỉ định cách khởi tạo các thuộc tính trong lớp của bạn:

class Car(val wheels: List<Wheel>)

Trong ví dụ ở trên, hàm dựng lớp lấy List<Wheel> làm đối số hàm dựng và sử dụng đối số đó để khởi tạo thuộc tính wheels.

Hàm trong lớp và đóng gói lớp

Lớp sử dụng các hàm để xây dựng mô hình hành vi. Hàm có thể sửa đổi trạng thái, giúp bạn chỉ cung cấp dữ liệu mà bạn muốn cung cấp. Khả năng kiểm soát quyền truy cập này thuộc về một khái niệm rộng hơn trong lập trình hướng đối tượng, đó là đóng gói.

Trong ví dụ sau, thuộc tính doorLock được đặt ở chế độ riêng tư, tách khỏi mọi nội dung bên ngoài lớp Car. Để mở khoá xe ô tô, bạn phải gọi hàm unlockDoor(), hàm này truyền một khoá hợp lệ, như trong ví dụ sau:

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

Nếu muốn tuỳ chỉnh cách tham chiếu một thuộc tính, bạn có thể cung cấp phương thức getter và setter tuỳ chỉnh. Ví dụ: nếu muốn cung cấp phương thức getter của một thuộc tính, đồng thời hạn chế quyền truy cập vào phương thức setter của thuộc tính đó, bạn có thể chỉ định phương thức setter đó là private:

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    var gallonsOfFuelInTank: Int = 15
        private set

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

Với sự kết hợp giữa thuộc tính và hàm, bạn có thể tạo lớp xây dựng mô hình cho tất cả các loại đối tượng.

Khả năng tương thích

Một trong những tính năng quan trọng nhất của Kotlin là khả năng tương tác linh hoạt với Java. Vì mã Kotlin tổng hợp thành mã byte VM, nên mã Kotlin của bạn có thể gọi trực tiếp vào mã Java và ngược lại. Điều này có nghĩa là bạn có thể tận dụng các thư viện Java hiện có ngay trên Kotlin. Ngoài ra, phần lớn API của Android được viết bằng Java và bạn có thể gọi trực tiếp những API đó trên Kotlin.

Các bước tiếp theo

Kotlin là một ngôn ngữ linh hoạt và thực tế với khả năng hỗ trợ và xu hướng phát triển ngày càng tăng. Nếu chưa dùng Kotlin bao giờ, bạn nên thử ngay. Để biết các bước tiếp theo, hãy xem tài liệu chính thức về Kotlin và hướng dẫn cách áp dụng các mẫu Kotlin phổ biến trong ứng dụng Android của bạn.