ขยายคลาสข้อมูลใน Kotlin


176

คลาสข้อมูลดูเหมือนจะแทนที่ POJO ที่ล้าสมัยใน Java ค่อนข้างคาดหวังว่าคลาสเหล่านี้จะอนุญาตให้มีการสืบทอด แต่ฉันไม่เห็นวิธีที่สะดวกในการขยายคลาสข้อมูล สิ่งที่ฉันต้องการคือสิ่งนี้:

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

รหัสข้างต้นล้มเหลวเนื่องจากการปะทะกันของcomponent1()วิธีการ การเพิ่มdataคำอธิบายประกอบทิ้งไว้ในคลาสใดคลาสหนึ่งเท่านั้นก็ไม่ทำงานเช่นกัน

อาจมีสำนวนอื่นในการขยายคลาสข้อมูลหรือไม่

UPD: ฉันอาจใส่คำอธิบายประกอบเป็นคลาสย่อยของเด็กเท่านั้น แต่dataคำอธิบายประกอบจะจัดการกับคุณสมบัติที่ประกาศใน Constructor เท่านั้น นั่นคือฉันจะต้องประกาศคุณสมบัติของผู้ปกครองทั้งหมด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-th โดยปริยาย ดูเอกสารเกี่ยวกับMulti-Declarations
Dmitry

สำหรับการเปิดคุณสมบัติคุณยังสามารถสร้างบทคัดย่อทรัพยากรหรือใช้ปลั๊กอินคอมไพเลอร์ Kotlin เข้มงวดเกี่ยวกับหลักการเปิด / ปิด
ŽeljkoTrogrlić

@Dmitry เนื่องจากเราไม่สามารถขยายคลาสข้อมูลได้ "การแก้ปัญหา" ของคุณในการทำให้ตัวแปรระดับผู้ปกครองเปิดอยู่และจะแทนที่พวกเขาในชั้นเรียนของเด็ก ๆ ว่า "ตกลง" ทำงานได้ไหม?
Archie G. Quiñones

คำตอบ:


163

ความจริงก็คือ: คลาสข้อมูลไม่เล่นดีเกินไปกับการสืบทอด เรากำลังพิจารณาที่จะห้ามหรือ จำกัด การสืบทอดคลาสข้อมูลอย่างรุนแรง ตัวอย่างเช่นเป็นที่ทราบกันดีว่าไม่มีวิธีที่จะใช้equals()อย่างถูกต้องในลำดับชั้นของคลาสที่ไม่เป็นนามธรรม

ดังนั้นสิ่งที่ฉันสามารถทำได้: อย่าใช้การสืบทอดกับคลาสข้อมูล


เฮ้อันเดรย์แล้วมันจะเท่ากับ () ตามที่มันถูกสร้างในคลาสข้อมูลได้อย่างไร? มันจะตรงกับถ้าประเภทที่แน่นอนและเขตข้อมูลทั่วไปทั้งหมดเท่ากันหรือเฉพาะถ้าเขตข้อมูลเท่ากัน? ดูเหมือนว่าเนื่องจากคุณค่าของการสืบทอดคลาสสำหรับการประมาณชนิดข้อมูลเกี่ยวกับพีชคณิตมันอาจคุ้มค่าที่จะเกิดขึ้นกับวิธีแก้ไขปัญหานี้ น่าสนใจการค้นหาคร่าวๆเปิดเผยการสนทนาในหัวข้อโดย Martin Odersky: artima.com/lejava/articles/equality.html
orospakr

3
ฉันไม่เชื่อว่ามีวิธีแก้ไขปัญหานี้มากมาย ความคิดเห็นของฉันจนถึงขณะนี้คือคลาสข้อมูลต้องไม่มีคลาสย่อยข้อมูลเลย
Andrey Breslav

3
จะเกิดอะไรขึ้นถ้าเรามีรหัสห้องสมุดเช่น ORM บางส่วนและเราต้องการขยายแบบจำลองเพื่อให้มีแบบจำลองข้อมูลถาวรของเรา
Krupal Shah

3
@AndreyBreslav เอกสารบนคลาส Dataไม่สะท้อนสถานะหลังจาก Kotlin 1.1 คลาสข้อมูลและการสืบทอดเล่นด้วยกันได้อย่างไรตั้งแต่ 1.1
Eugen Pechanec

2
@EugenPechanec ดูตัวอย่างนี้: kotlinlang.org/docs/reference/ ......
Andrey Breslav

114

ประกาศคุณสมบัติใน super-class นอกนวกรรมิกเป็นนามธรรมและแทนที่พวกเขาใน sub-class

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
สิ่งนี้ดูเหมือนจะยืดหยุ่นที่สุด ฉันหวังอย่างยิ่งว่าเราจะได้รับคลาสข้อมูลที่สืบทอดมาจากกัน ...
Adam

สวัสดีครับขอขอบคุณสำหรับวิธีการอย่างเป็นระเบียบในการจัดการ Data Class Inheritance ฉันกำลังเผชิญปัญหาเมื่อฉันใช้คลาสนามธรรมเป็นประเภททั่วไป ฉันได้รับType Mismatchข้อผิดพลาด: "Required T, Found: Resource" คุณช่วยกรุณาบอกฉันว่ามันสามารถใช้ใน Generics ได้อย่างไร?
ashwin mahajan

ฉันยังต้องการที่จะรู้ว่าถ้า generics เป็นไปได้ในชั้นเรียนนามธรรม ตัวอย่างเช่นถ้าตำแหน่งเป็นสตริงในคลาสข้อมูลที่สืบทอดมาหนึ่งคลาสและคลาสที่กำหนดเอง (ให้พูดLocation(long: Double, lat: Double))ในอีกอันหนึ่ง?
Robbie Cronin

2
ฉันเกือบหมดความหวัง ขอบคุณ!
MichałPowłoka

การทำซ้ำพารามิเตอร์ดูเหมือนจะเป็นวิธีที่ไม่ดีในการนำการสืบทอดมาใช้ ในทางเทคนิคเนื่องจาก Book สืบทอดมาจาก Resource จึงควรรู้ว่ามี id และตำแหน่งอยู่ ไม่จำเป็นต้องระบุสิ่งเหล่านั้นจริงๆ
AndroidDev

23

วิธีการแก้ปัญหาข้างต้นโดยใช้คลาสนามธรรมสร้างคลาสที่สอดคล้องกันจริงและปล่อยให้คลาสข้อมูลขยายออกไป

หากคุณไม่ชอบคลาสนามธรรมคุณจะใช้อินเตอร์เฟสได้อย่างไร

อินเทอร์เฟซใน 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(), ...
}

อย่างที่คุณเห็นมันทำงานได้เหมือนคลาสข้อมูลปกติ!


3
น่าเสียดายที่การใช้รูปแบบอินเทอร์เฟซสำหรับคลาสข้อมูลไม่ทำงานกับสถาปัตยกรรมของห้อง
Adam Hurwitz

@ AdamHurwitz แย่มาก .. ฉันไม่ได้สังเกตเลย!
Tura

4

@ ŽeljkoTrogrlićคำตอบถูกต้อง แต่เราต้องทำซ้ำในสาขาเดียวกันในชั้นนามธรรม

นอกจากนี้ถ้าเรามีคลาสย่อยนามธรรมภายในคลาสนามธรรมจากนั้นในคลาสข้อมูลเราไม่สามารถขยายฟิลด์จากคลาสย่อยนามธรรมเหล่านี้ได้ เราควรสร้างคลาสย่อยข้อมูลก่อนแล้วจึงกำหนดฟิลด์

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

ลักษณะ 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

2

คุณสามารถรับคลาสข้อมูลจากคลาสที่ไม่ใช่ข้อมูลได้ ไม่อนุญาตให้ใช้การสืบทอดคลาสข้อมูลจากคลาสข้อมูลอื่นเนื่องจากไม่มีวิธีที่จะทำให้วิธีคลาสข้อมูลที่คอมไพเลอร์สร้างขึ้นทำงานอย่างต่อเนื่องและอย่างสังหรณ์ใจในกรณีของการสืบทอด


1

ในขณะที่การดำเนินการ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)

สมมติว่าเราUserและLocationหน่วยงานของพวกเขากลับมารหัสที่เหมาะสมทรัพยากร ( UserResourceIdและLocationResourceIdตามลำดับ) เรียกtoString()ใด ๆResourceIdอาจส่งผลค่อนข้างเป็นตัวแทนน้อยดีว่าเป็นเรื่องปกติที่ถูกต้องสำหรับเชื้อทั้งหมด: /users/4587, /locations/23ฯลฯ แต่เนื่องจากไม่ใช่เชื้อที่สืบทอดมาเพื่อแทนที่toString()วิธีการจาก ฐานนามธรรมการResourceIdโทรtoString()จริงส่งผลให้เกิดการแทนค่าที่น้อยกว่า: <UserResourceId(id=UserId(value=4587))>,<LocationResourceId(id=LocationId(value=23))>

มีวิธีอื่น ๆ ในการทำโมเดลด้านบน แต่วิธีการเหล่านั้นบังคับให้เราใช้คลาสที่ไม่ใช่ข้อมูล (พลาดประโยชน์จากคลาส data เป็นจำนวนมาก) หรือเราคัดลอก / ทำซ้ำtoString()การนำไปใช้ในคลาสข้อมูลทั้งหมดของเรา (ไม่มีการสืบทอด)


0

คุณสามารถรับคลาสข้อมูลจากคลาสที่ไม่ใช่ข้อมูลได้

คลาสฐาน

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

มันได้ผล


ยกเว้นว่าตอนนี้คุณไม่สามารถตั้งค่าคุณสมบัติชื่อและคำอธิบายและถ้าคุณเพิ่มไปยังตัวสร้างคลาสข้อมูลต้องการ val / var ซึ่งจะแทนที่คุณสมบัติคลาสพื้นฐาน
Brill Pappin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.