Mở rộng lớp dữ liệu trong Kotlin


176

Các lớp dữ liệu dường như là sự thay thế cho các POJO lỗi thời trong Java. Điều khá mong đợi là các lớp này sẽ cho phép kế thừa, nhưng tôi có thể thấy không có cách nào thuận tiện để mở rộng một lớp dữ liệu. Những gì tôi cần là một cái gì đó như thế này:

open data class Resource (var id: Long = 0, var location: String = "")
data class Book (var isbn: String) : Resource()

Đoạn mã trên không thành công vì đụng độ các component1()phương thức. Để lại datachú thích chỉ trong một trong các lớp cũng không thực hiện công việc.

Có lẽ có một thành ngữ khác để mở rộng các lớp dữ liệu?

CẬP NHẬT: Tôi có thể chỉ chú thích lớp con con, nhưng datachú thích chỉ xử lý các thuộc tính được khai báo trong hàm tạo. Đó là, tôi sẽ phải khai báo tất cả các thuộc tính của cha mẹ openvà ghi đè lên chúng, điều này thật xấu xí:

open class Resource (open var id: Long = 0, open var location: String = "")
data class Book (
    override var id: Long = 0,
    override var location: String = "",
    var isbn: String
) : Resource()

3
Kotlin ngầm tạo ra các phương thức componentN()trả về giá trị của thuộc tính N-th. Xem tài liệu về Đa tuyên bố
Dmitry

Để mở các thuộc tính, bạn cũng có thể tạo Resource trừu tượng hoặc sử dụng plugin trình biên dịch. Kotlin nghiêm ngặt về nguyên tắc mở / đóng.
Željko Trogrlić

@Dmitry Vì chúng tôi không thể mở rộng một lớp dữ liệu, "giải pháp" của bạn về việc giữ biến lớp cha mở và chỉ đơn giản ghi đè chúng trong lớp con có hoạt động "ok" không?
Archie G. Quiñones

Câu trả lời:


163

Sự thật là: các lớp dữ liệu không chơi quá tốt với tính kế thừa. Chúng tôi đang xem xét việc cấm hoặc hạn chế nghiêm ngặt việc kế thừa các lớp dữ liệu. Ví dụ, người ta biết rằng không có cách nào để thực hiện equals()chính xác trong một hệ thống phân cấp trên các lớp không trừu tượng.

Vì vậy, tất cả những gì tôi có thể cung cấp: không sử dụng tính kế thừa với các lớp dữ liệu.


Này, làm thế nào để bằng () khi nó được tạo trên các lớp dữ liệu hoạt động bây giờ? Liệu nó chỉ phù hợp nếu loại chính xác và tất cả các trường phổ biến là bằng nhau, hoặc chỉ khi các trường bằng nhau? Có vẻ như, vì giá trị của thừa kế lớp để xấp xỉ các kiểu dữ liệu đại số, nên có thể đưa ra giải pháp cho vấn đề này. Thật thú vị, một tìm kiếm chữ thảo đã tiết lộ cuộc thảo luận này về chủ đề của Martin Oderky
orospakr

3
Tôi không tin có nhiều giải pháp cho vấn đề này. Ý kiến ​​của tôi cho đến nay là các lớp dữ liệu không được có các lớp con dữ liệu.
Andrey Breslav 30/03/2015

3
Điều gì xảy ra nếu chúng ta có một mã thư viện như một số ORM và chúng ta muốn mở rộng mô hình của nó để có mô hình dữ liệu liên tục của chúng ta?
Krupal Shah

3
@AndreyBreslav Tài liệu về các lớp Dữ liệu không phản ánh trạng thái sau Kotlin 1.1. Làm thế nào để các lớp dữ liệu và kế thừa chơi với nhau kể từ 1.1?
Eugen Pechanec

2
@EugenPechanec Xem ví dụ này: kotlinlang.org/docs/reference/ory
Andrey Breslav

114

Khai báo các thuộc tính trong siêu lớp bên ngoài hàm tạo là trừu tượng và ghi đè chúng trong lớp con.

abstract class Resource {
    abstract var id: Long
    abstract var location: String
}

data class Book (
    override var id: Long = 0,
    override var location: String = "",
    var isbn: String
) : Resource()

15
điều này dường như là linh hoạt nhất. Tôi thực sự mong muốn chúng ta có thể có các lớp dữ liệu kế thừa lẫn nhau ...
Adam

Xin chào ngài, cảm ơn vì cách xử lý gọn gàng Kế thừa lớp dữ liệu. Tôi đang phải đối mặt với một vấn đề khi tôi sử dụng lớp trừu tượng làm Loại chung. Tôi gặp Type Mismatchlỗi: "Bắt buộc T, Tìm thấy: Tài nguyên". Bạn có thể vui lòng cho tôi biết làm thế nào nó có thể được sử dụng trong Generics?
ashwin mahajan

Tôi cũng muốn biết nếu thuốc generic có thể qua các lớp trừu tượng. Ví dụ: nếu vị trí là một Chuỗi trong một lớp dữ liệu được kế thừa và một lớp tùy chỉnh (giả sử Location(long: Double, lat: Double))trong một lớp khác thì sao?
Robbie Cronin

2
Tôi gần như mất hết hy vọng. Cảm ơn!
Michał Powłoka

Sao chép các tham số dường như là một cách kém để thực hiện kế thừa. Về mặt kỹ thuật, vì Sách kế thừa từ Tài nguyên, nên biết rằng id và vị trí tồn tại. Thực sự không cần phải xác định những thứ đó.
AndroidDev

23

Giải pháp trên sử dụng lớp trừu tượng thực sự tạo ra lớp tương ứng và để lớp dữ liệu mở rộng từ nó.

Nếu bạn không thích lớp trừu tượng, làm thế nào về việc sử dụng một giao diện ?

Giao diện trong Kotlin có thể có các thuộc tính như trong bài viết này ..

interface History {
    val date: LocalDateTime
    val name: String
    val value: Int
}

data class FixedHistory(override val date: LocalDateTime,
                        override val name: String,
                        override val value: Int,
                        val fixedEvent: String) : History

Tôi tò mò làm thế nào Kotlin biên dịch này. Đây là mã Java tương đương (được tạo bằng tính năng Intellij [Kotlin bytecode]):

public interface History {
   @NotNull
   LocalDateTime getDate();

   @NotNull
   String getName();

   int getValue();
}

public final class FixedHistory implements History {
   @NotNull
   private final LocalDateTime date;
   @NotNull
   private final String name;
   private int value;
   @NotNull
   private final String fixedEvent;

   // Boring getters/setters as usual..
   // copy(), toString(), equals(), hashCode(), ...
}

Như bạn có thể thấy, nó hoạt động chính xác như một lớp dữ liệu bình thường!


3
Thật không may, việc triển khai mẫu giao diện cho lớp dữ liệu không hoạt động với kiến ​​trúc của Room.
Adam Hurwitz

@AdamHurwitz Điều đó thật tệ .. Tôi đã không nhận ra điều đó!
Tura

4

@ Eljko Câu trả lời Trogrlić là chính xác. Nhưng chúng ta phải lặp lại các trường giống như trong một lớp trừu tượng.

Ngoài ra nếu chúng ta có các lớp con trừu tượng bên trong lớp trừu tượng, thì trong một lớp dữ liệu, chúng ta không thể mở rộng các trường từ các lớp con trừu tượng này. Trước tiên chúng ta nên tạo lớp con dữ liệu và sau đó xác định các trường.

abstract class AbstractClass {
    abstract val code: Int
    abstract val url: String?
    abstract val errors: Errors?

    abstract class Errors {
        abstract val messages: List<String>?
    }
}



data class History(
    val data: String?,

    override val code: Int,
    override val url: String?,
    // Do not extend from AbstractClass.Errors here, but Kotlin allows it.
    override val errors: Errors?
) : AbstractClass() {

    // Extend a data class here, then you can use it for 'errors' field.
    data class Errors(
        override val messages: List<String>?
    ) : AbstractClass.Errors()
}

Chúng ta có thể di chuyển History.Errors sang AbstractClass.Errors.Compmate.SimpleErrors hoặc bên ngoài và sử dụng nó trong các lớp dữ liệu thay vì sao chép nó ở mỗi lớp dữ liệu kế thừa?
TWiStErRob

@TWiStErRob, rất vui khi nghe một người nổi tiếng như vậy! Tôi có nghĩa là History.Errors có thể thay đổi trong mỗi lớp, do đó chúng ta nên ghi đè lên nó (ví dụ: thêm các trường).
CoolMind

4

Kotlin Traits có thể giúp đỡ.

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

lớp dữ liệu

data class Base(override val prop:String) : IBase

data class Derived(override val derived_prop:String,
                   private val base:IBase) :  IDerived, IBase by base

sử dụng mẫu

val b = Base("base")
val d = Derived("derived", b)

print(d.prop) //prints "base", accessing base class property
print(d.derived_prop) //prints "derived"

Cách tiếp cận này cũng có thể là một cách giải quyết cho các vấn đề thừa kế với @Parcelize

@Parcelize 
data class Base(override val prop:Any) : IBase, Parcelable

@Parcelize // works fine
data class Derived(override val derived_prop:Any,
                   private val base:IBase) : IBase by base, IDerived, Parcelable

2

Bạn có thể kế thừa một lớp dữ liệu từ một lớp không dữ liệu. Việc thừa kế một lớp dữ liệu từ một lớp dữ liệu khác là không được phép vì không có cách nào để làm cho các phương thức lớp dữ liệu do trình biên dịch hoạt động một cách nhất quán và trực quan trong trường hợp kế thừa.


1

Mặc dù việc triển khai equals()chính xác trong một hệ thống phân cấp thực sự khá khó khăn, nhưng vẫn rất tốt để hỗ trợ kế thừa các phương thức khác, ví dụ : toString().

Để cụ thể hơn một chút, giả sử chúng ta có cấu trúc sau (rõ ràng, nó không hoạt động vì toString()không được kế thừa, nhưng nó sẽ không tốt nếu nó sẽ như vậy?):

abstract class ResourceId(open val basePath: BasePath, open val id: Id) {

    // non of the subtypes inherit this... unfortunately...
    override fun toString(): String = "/${basePath.value}/${id.value}"
}
data class UserResourceId(override val id: UserId) : ResourceId(UserBasePath, id)
data class LocationResourceId(override val id: LocationId) : ResourceId(LocationBasePath, id)

Giả sử UserLocationcác đối tượng quay trở lại ID của họ phù hợp nguồn lực ( UserResourceIdLocationResourceIdtương ứng), gọi toString()trên bất kỳ ResourceIdcó thể dẫn đến một đại diện ít đẹp khá mà nói chung là hợp lệ cho tất cả các phân nhóm: /users/4587, /locations/23, vv Thật không may, bởi vì phi của phân nhóm di truyền để ghi đè toString()phương pháp từ cơ sở trừu tượng ResourceId, gọi toString()thực sự dẫn đến một đại diện ít đẹp hơn : <UserResourceId(id=UserId(value=4587))>,<LocationResourceId(id=LocationId(value=23))>

Có nhiều cách khác để mô hình hóa ở trên, nhưng những cách đó buộc chúng ta phải sử dụng các lớp không dữ liệu (bỏ lỡ rất nhiều lợi ích của các lớp dữ liệu) hoặc cuối cùng chúng ta sao chép / lặp lại việc toString()thực hiện trong tất cả các lớp dữ liệu của mình (không thừa kế).


0

Bạn có thể kế thừa một lớp dữ liệu từ một lớp không dữ liệu.

Lớp cơ sở

open class BaseEntity (

@ColumnInfo(name = "name") var name: String? = null,
@ColumnInfo(name = "description") var description: String? = null,
// ...
)

lớp trẻ

@Entity(tableName = "items", indices = [Index(value = ["item_id"])])
data class CustomEntity(

    @PrimaryKey
    @ColumnInfo(name = "id") var id: Long? = null,
    @ColumnInfo(name = "item_id") var itemId: Long = 0,
    @ColumnInfo(name = "item_color") var color: Int? = null

) : BaseEntity()

Nó đã làm việc.


Ngoại trừ việc bây giờ bạn không thể đặt các thuộc tính tên và mô tả và nếu bạn thêm chúng vào hàm tạo, lớp dữ liệu cần val / var sẽ ghi đè các thuộc tính của lớp cơ sở.
Brill Pappin
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.