在Kotlin中扩展数据类

353

数据类似乎是Java中老式POJO的替代品。这些类允许继承是非常可预期的,但我却找不到方便的方法来扩展数据类。我需要像这样的东西:

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

上面的代码出现问题是因为存在component1()方法冲突。在一个类中仅保留一个类的data注释也行不通。

也许有另一种习惯用法可以扩展数据类?

更新:我可能只能对子类进行注释,但是data注释只处理在构造函数中声明的属性。也就是说,我必须将所有父类属性声明为open并覆盖它们,这很丑陋:

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 隐式创建了方法 componentN() 来返回第 N 个属性的值。请参阅多重声明上的文档。 - Dmitry
打开属性时,您还可以使资源抽象或使用编译器插件。Kotlin 对于开放/封闭原则非常严格。 - Željko Trogrlić
@Dmitry 由于我们无法扩展数据类,您是否认为在子类中保持父类变量开放并简单地覆盖它们是一个“可行”的解决方法? - Archie G. Quiñones
不能有一个开放数据的课程。 - Rohit gupta
14个回答

327
真相是:数据类与继承不太兼容。我们正在考虑禁止或严格限制对数据类的继承。例如,众所周知,在非抽象类层次结构中无法正确实现equals()方法。
因此,我能提供的全部建议是:不要在数据类中使用继承。

12
我认为这个问题没有很好的解决方案。到目前为止,我的观点是数据类根本不应该有数据子类。 - Andrey Breslav
10
如果我们有一个类似ORM的库代码,我们想要扩展它的模型以拥有我们自己的持久化数据模型,该怎么办? - Krupal Shah
3
自 Kotlin 1.1 版本以后,@AndreyBreslav 的《数据类文档》中的内容已经不再适用。请问从 Kotlin 1.1 开始,如何处理数据类与继承之间的关系? - Eugen Pechanec
4
@EugenPechanec 请看这个例子:https://kotlinlang.org/docs/reference/whatsnew11.html#sealed-and-data-classes - Andrey Breslav
12
如果我们不能在数据类中使用继承,那么当逻辑相同但数据不同时就会出现大量重复代码...因为缺少继承支持,我不得不复制很多代码,非常非常糟糕。 - S.Bozzoni
显示剩余12条评论

205

在超类中将属性声明为抽象,并在子类中覆盖它们,而不是在构造函数中声明。

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()

41
这似乎是最灵活的。不过,我非常希望我们可以让数据类彼此继承... - Adam
2
我差点失去了希望。谢谢! - Michał Powłoka
7
复制参数似乎是实现继承的一种不好的方式。从技术上讲,由于 Book 继承自 Resource,它应该知道 id 和 location 存在。其实没有必要再次指定这些参数。 - Johann
1
@AndroidDev它们并不存在,因为它们是抽象的。 - Željko Trogrlić
2
这适用于Kotlin,但如果我需要从Java调用数据类构造函数,则会出现“无法继承自final [class]”编译错误。为什么会这样,能否解决? - Chisko
显示剩余2条评论

51

使用抽象类的上述解决方案实际上会生成相应的类,并使数据类从中继承。

如果您不喜欢抽象类,那么考虑使用接口呢?

Kotlin中的接口可以具有属性,如本文所示。

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

我很好奇 Kotlin 是如何编译这段代码的。以下是等同的 Java 代码(使用 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(), ...
}

如您所见,它的工作方式与普通数据类完全相同!


6
遗憾的是,对于一个数据类来说,实现接口模式与Room的架构不兼容。 - AdamHurwitz
@AdamHurwitz 真遗憾.. 我没有注意到! - Tura
1
@Adam Hurwitz 刚刚遇到了这个问题,你能解释一下为什么吗? - Sam Chen

26

Kotlin的特质可以帮助。

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

数据类

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

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

示例用法

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

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

这种方法也可以解决@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

1
这个能和 Room 搭配使用吗? - Chisko
在我看来,这是最好的答案,以下是我认为适用于问题中原始示例的解决方案:interface IsResource { var id: Long var location: String } data class Resource (var id: Long = 0, var location: String = ""): IsResource data class Book (var isbn: String, val resource: Resource) : IsResource by resource - Robert Pazurek

6
你可以从一个非数据类继承一个数据类。
基类
open class BaseEntity (

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

子类

@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()

它有效了。


5
现在你不能设置名称和描述属性,如果你将它们添加到构造函数中,数据类需要使用val/var来覆盖基类属性。 - Brill Pappin
6
不幸的是,针对该数据类生成的 equals()hashCode()toString() 方法将不包括从基类继承的属性,这消除了在此处使用数据类的好处。 - Roman_D

5
我是如何做到的。
open class ParentClass {
  var var1 = false
  var var2: String? = null
}

data class ChildClass(
  var var3: Long
) : ParentClass()

它运行良好。


10
如果你想要要求每个ChildClass在构造时传递var1和var2的值,你该如何构建ChildClass? - David
ChildClass 只为 var3 实现了 equalshashCode 方法。 - Grigory Kislin

5
@Željko Trogrlić的回答是正确的。但我们还必须重复抽象类中的相同字段。
此外,如果在抽象类中有抽象子类,则在数据类中无法扩展这些抽象子类的字段。我们应该先创建数据子类,然后定义字段。
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()
}

我们可以将History.Errors移动到AbstractClass.Errors.Companion.SimpleErrors中或者移到外部,并在数据类中使用它,而不是在每个继承的数据类中重复它? - TWiStErRob
@TWiStErRob,很高兴听到这样有名的人!我的意思是,History.Errors 可以在每个类中更改,因此我们应该覆盖它(例如,添加字段)。 - CoolMind

4

您可以从非数据类继承数据类。不允许从另一个数据类继承数据类,因为在继承的情况下,没有办法使编译器生成的数据类方法始终一致且直观地工作。


https://discuss.kotlinlang.org/t/data-class-inheritance/4107/2 - Sercan

2
通常情况下,当继承变得棘手时,解决方案就是组合。请参见优先选择组合而非继承?
如果您只想使用一些额外的字段“扩展”您的类,可以使用组合以及一些额外的getter方法来方便地实现:
data class Book(
  val id: Long,
  val isbn: String,
  val author: String,
)

data class StoredBook(
  val book: Book,
  val version: Long,
  val createdAt: ZonedDateTime,
  val updatedAt: ZonedDateTime,
) {
  // proxy fields for convenience
  val id get() = book.id
  val isbn get() = book.isbn
  val author get() = book.author
}

这将Book属性委托给book实例,因此在大多数情况下,可以像处理Book一样使用StoredBook,但您仍然可以保持某些类型安全性,以确定您是否正在处理中间的Book状态还是已持久化的StoredBook

更进一步,您可以为任何存储在数据库中的条目创建一个StoredResource接口:

interface StoredResource {
  val id: Long
  val version: Long
  val createdAt: ZonedDateTime
  val updatedAt: ZonedDateTime
}

data class Book(
  val id: Long,
  val isbn: String,
  val author: String,
)

data class StoredBook(
  val book: Book,
  override val version: Long,
  override val createdAt: ZonedDateTime,
  override val updatedAt: ZonedDateTime,
) : StoredResource {
  override val id get() = book.id
  val isbn get() = book.isbn
  val author get() = book.author
}

2
在继承体系中正确实现equals()确实很麻烦,但支持继承其他方法会更好,例如:toString()
为了更具体一些,让我们假设我们有以下结构(显然,它不起作用,因为toString()没有被继承,但如果它能这样做,那不是很好吗?):
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)

假设我们的UserLocation实体返回其适当的资源ID(分别为UserResourceIdLocationResourceId),调用任何ResourceId上的toString()可能会导致一个相当不错的表示形式,通常适用于所有子类型:/users/4587/locations/23等。不幸的是,由于没有任何子类型继承或覆盖了来自抽象基类ResourceIdtoString()方法,因此调用toString()实际上会导致一个不太漂亮的表示形式:<UserResourceId(id=UserId(value=4587))><LocationResourceId(id=LocationId(value=23))> 还有其他建模方式,但这些方式要么强制我们使用非数据类(错过了许多数据类的好处),要么我们最终在所有数据类中复制/重复toString()实现(没有继承)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接