Giới thiệu về Data Classes trong Kotlin
Hầu như tất cả các dự án phần mềm mà chúng tôi tạo ra có một số lớp tồn tại chỉ để lưu trữ dữ liệu/trạng thái nhưng hầu như không có chức năng thực tế về hoạt động. Trong các ứng dụng phức tạp hơn, con số này có thể khá cao (các ứng dụng có cách tiếp cận kiến trúc sạch thường có 2-3 lần do sự tách ...
Hầu như tất cả các dự án phần mềm mà chúng tôi tạo ra có một số lớp tồn tại chỉ để lưu trữ dữ liệu/trạng thái nhưng hầu như không có chức năng thực tế về hoạt động. Trong các ứng dụng phức tạp hơn, con số này có thể khá cao (các ứng dụng có cách tiếp cận kiến trúc sạch thường có 2-3 lần do sự tách các thực thể giữa các lớp). Nhưng thông thường chúng đều có cùng một khái niệm cấu trúc:]
- A constructor
- Fields to store data
- Getter and setter functions
- hashCode(), equals() and toString() functions
Nếu chúng ta muốn lưu trữ một số dữ liệu về một trò chơi điện tử bằng Java, chúng ta thường tạo ra một lớp tương tự như sau:
public class VideoGame { private String name; private String publisher; private int reviewScore; public VideoGame(String name, String publisher, int reviewScore) { this.name = name; this.publisher = publisher; this.reviewScore = reviewScore; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPublisher() { return publisher; } public void setPublisher(String publisher) { this.publisher = publisher; } public int getReviewScore() { return reviewScore; } public void setReviewScore(int reviewScore) { this.reviewScore = reviewScore; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; VideoGame that = (VideoGame) o; if (reviewScore != that.reviewScore) return false; if (name != null ? !name.equals(that.name) : that.name != null) { return false; } return publisher != null ? publisher.equals(that.publisher) : that.publisher == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (publisher != null ? publisher.hashCode() : 0); result = 31 * result + reviewScore; return result; } @Override public String toString() { return "VideoGame{" + "name='" + name + ' + ", publisher='" + publisher + ' + ", reviewScore=" + reviewScore + '}'; } }
Đó là rất nhiều mã chỉ để lưu trữ 3 trường dữ liệu cho một trò chơi video!
May mắn cho chúng tôi, đoạn mã trên không còn cần thiết nữa trong Kotlin do khái niệm data class hữu ích được cung cấp bởi ngôn ngữ này. Một data class là một lớp trong Kotlin tạo ra để đóng gói tất cả các chức năng trên một cách ngắn gọn. Để tạo lại lớp VideoGame ở Kotlin, chúng ta chỉ cần viết:
data class VideoGame(val name: String, val publisher: String, var reviewScore: Int)
Trông có vẻ tốt hơn nhiều!
Khi chúng ta chỉ định từ khóa data trong khai báo class, Kotlin tự động định nghĩa các field accessors, hashCode(), equals(), toString(), cũng như các tính năng hữu ích copy() và componentN(). Bất kỳ phương thức nào ở trên được chúng tôi xác định bằng tay trong lớp sẽ không được tạo ra.
Khởi tạo một thể hiện của data class
Các lớp dữ liệu được khởi tạo theo cách tương tự như một lớp chuẩn:
val game: VideoGame = VideoGame("Gears of War", "Epic Games", 8)
Và chúng ta có thể truy cập các thuộc tính của game:
print(game.name) // "Gears of War" print(game.publisher) // "Epic Games" print(game.reviewScore) // 8 game.reviewScore = 7
và in nội dung của lớp:
print(game.toString()) // prints // "Game(name=Gears Of War, publisher=Epic Games, reviewScore=7)"
Visibility Modifiers
Chúng ta có thể điều khiển visibility modifiers của getters/setters tạo ra bằng cách cung cấp chúng trong constructor:
data class VideoGame(private val name: String, val publisher: String, private var reviewScore: Int
Read-Only Fields
Nếu chúng ta chỉ muốn để lộ getters và không setters, chúng ta chỉ cần cung cấp val thay vì var cho mỗi trường. Trong ví dụ dưới đây, tên và reviewScore có quyền đọc/ghi, trong khi publisher là chỉ đọc:
data class VideoGame(var name: String, val publisher: String, private var reviewScore: Int
copy() Function
Vì các lớp dữ liệu của chúng tôi là không thay đổi, chúng ta phải tạo một bản sao nếu muốn thay đổi một số dữ liệu. Chúng tôi cũng có thể chỉ định nếu chúng ta chỉ muốn thay đổi các thuộc tính cụ thể cho bản sao mới. Ví dụ: nếu chúng ta muốn thay đổi điểm số đánh giá của một trò chơi, điều này có thể được thực hiện bằng cách viết:
val game: VideoGame = VideoGame("Gears of War", "Epic Games", 8) val betterGame = game.copy(reviewScore = 10)
Destructuring Declarations
Đây là tên của cú pháp được cung cấp bởi Kotlin cho phép chúng ta ánh xạ một đối tượng vào các trường riêng lẻ. Đây là nơi các hàm componentN() được nêu ở trên đóng lại. Đối với mỗi thuộc tính mà chúng ta chỉ định cho lớp dữ liệu của chúng ta (ví dụ trò chơi video của chúng ta có 3), Kotlin sẽ tạo ra một hàm componentN() để ánh xạ tới thuộc tính đó, trong đó 'N' đại diện cho thuộc tính của định nghĩa. Vì vậy, trong trường hợp của chúng tôi, chúng tôi có những điều sau đây:
game.component1() // name game.component2() // publisher game.component3() // reviewScore
Các chức năng tạo ra này cho phép chúng ta sử dụng các khai báo phá hoại để làm một số thứ mát mẻ:
Chúng ta có thể phá hủy một đối tượng để tạo ba thuộc tính val cùng một lúc:
val game: VideoGame = VideoGame("Gears of War", "Epic Games", 8) val (theName, thePublisher, theReviewScore) = game // val theName == "Gears of War" // val thePublisher == "Epic Games" // val theReviewScore == 8
Chúng ta cũng có thể hủy cấu trúc dữ liệu trực tiếp từ một hàm:
// function which returns a new video game fun getNamePublisherAndReviewScore() = VideoGame("Street Fighter", "Capcom", 10) val (anotherName, anotherYear, anotherReviewScore) = getNamePublisherAndReviewScore() // anotherName == "Street Fighter" // anotherYear == "Capcom" // anotherReviewScore == 10
Trên thực tế, destructuring declarations có thể được sử dụng trong hầu hết các trường hợp phía bên tay phải declaration có thể được chia thành các chức năng của componentN(). Chúng ta có thể huỷ dữ liệu trực tiếp bên trong một vòng lặp qua maps/collections:
val listOfGame: List<VideoGame> = listOf(game, betterGame) for ((gameName, gamePublisher, gameScore) in listOfGame) { print(gameName) // print the gameName // do something else with the gamePublisher // share the gameScore }
Điều này rất hữu ích khi sử dụng với các cặp key/value (from the Kotlin documentation):
for ((key, value) in map) { // do something with the key and the value }
Một số data classes được xây dựng sẵn
Kotlin cũng cung cấp các data classes Pair và Triple cho các xử lý chung
val pair: Pair<Int, String> = Pair(10, "Ten") val triple: Triple<Int, String, Boolean> = Triple(1, "One", true)
Mặc dù với tư cách là tài liệu, trong hầu hết các trường hợp, tốt nhất là tạo lớp dữ liệu của riêng bạn (ngay cả khi nó có 2 hoặc 3 thuộc tính) có tên mô tả/có liên quan hơn cho trường hợp sử dụng của bạn.
Các quy tắc để tạo data classes
Tài liệu Kotlin về các lớp dữ liệu lưu ý rằng có một số hạn chế cơ bản để duy trì tính nhất quán / hành vi của mã được tạo ra:
- Constructor chính phải được tạo ra với ít nhất 1 tham số
- Tham số của constructor nên được đánh dấu là val hoặc var
- Data class không thể là abstract, open, sealed hoặc inner
- Data class không nên kế thừa từ lớp khác (nhưng vẫn có thể implement interfaces)
Giống như hầu hết các khía cạnh khác của Kotlin, các lớp dữ liệu nhằm giảm số lượng mã boilerplate mà bạn viết trong dự án của bạn (và làm như vậy để có hiệu quả tuyệt vời!). Để tìm hiểu thêm, vui lòng truy cập các trang Data Classes và Destructuring Declarations trong Tài liệu Kotlin chính thức.