แทนที่ getter สำหรับคลาสข้อมูล Kotlin


99

รับคลาส Kotlin ต่อไปนี้:

data class Test(val value: Int)

ฉันจะแทนที่Intgetter อย่างไรเพื่อให้มันกลับมาเป็น 0 ถ้าค่าเป็นลบ

หากไม่สามารถทำได้มีเทคนิคอะไรบ้างเพื่อให้ได้ผลลัพธ์ที่เหมาะสม


14
โปรดพิจารณาเปลี่ยนโครงสร้างของโค้ดของคุณเพื่อให้ค่าลบถูกแปลงเป็น 0 เมื่อคลาสถูกสร้างอินสแตนซ์ไม่ใช่ใน getter หากคุณลบล้าง getter ตามที่อธิบายไว้ในคำตอบด้านล่างเมธอดอื่น ๆ ที่สร้างขึ้นทั้งหมดเช่น equals (), toString () และการเข้าถึงส่วนประกอบจะยังคงใช้ค่าเชิงลบเดิมซึ่งอาจนำไปสู่พฤติกรรมที่น่าแปลกใจ
yole

คำตอบ:


148

หลังจากใช้เวลาเกือบปีเต็มในการเขียน Kotlin ทุกวันฉันพบว่าการพยายามลบล้างคลาสข้อมูลเช่นนี้เป็นการปฏิบัติที่ไม่ดี มีแนวทางที่ถูกต้อง 3 วิธีในการนี้และหลังจากที่ฉันนำเสนอไปแล้วฉันจะอธิบายว่าทำไมคำตอบอื่น ๆ จึงแนะนำว่าไม่ดี

  1. มีตรรกะทางธุรกิจของคุณที่สร้างการdata classเปลี่ยนแปลงค่าให้เป็น 0 ขึ้นไปก่อนที่จะเรียกตัวสร้างด้วยค่าที่ไม่ถูกต้อง นี่อาจเป็นแนวทางที่ดีที่สุดสำหรับกรณีส่วนใหญ่

  2. อย่าใช้ไฟล์data class. ใช้ปกติclassและให้ IDE ของคุณสร้างequalsและhashCodeวิธีการสำหรับคุณ (หรือไม่ถ้าคุณไม่ต้องการ) ใช่คุณจะต้องสร้างมันขึ้นมาใหม่หากมีการเปลี่ยนแปลงคุณสมบัติใด ๆ ในวัตถุ แต่คุณจะเหลือการควบคุมทั้งหมดของวัตถุ

    class Test(value: Int) {
      val value: Int = value
        get() = if (field < 0) 0 else field
    
      override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Test) return false
        return true
      }
    
      override fun hashCode(): Int {
        return javaClass.hashCode()
      }
    }
    
  3. สร้างคุณสมบัติปลอดภัยเพิ่มเติมบนวัตถุที่ทำในสิ่งที่คุณต้องการแทนที่จะมีค่าส่วนตัวที่ลบล้างได้อย่างมีประสิทธิภาพ

    data class Test(val value: Int) {
      val safeValue: Int
        get() = if (value < 0) 0 else value
    }
    

แนวทางที่ไม่ดีที่คำตอบอื่นแนะนำ:

data class Test(private val _value: Int) {
  val value: Int
    get() = if (_value < 0) 0 else _value
}

ปัญหาของแนวทางนี้คือคลาสข้อมูลไม่ได้มีไว้สำหรับการเปลี่ยนแปลงข้อมูลเช่นนี้จริงๆ พวกเขามีไว้เพื่อเก็บข้อมูลเท่านั้น เอาชนะทะเยอทะยานสำหรับชั้นข้อมูลเช่นนี้จะหมายความว่าTest(0)และTest(-1)จะไม่ได้equalอีกคนหนึ่งและจะมีแตกต่างกันhashCodes แต่เมื่อคุณเรียกว่า.valueพวกเขาจะมีผลเหมือนกัน สิ่งนี้ไม่สอดคล้องกันและแม้ว่าอาจได้ผลสำหรับคุณ แต่คนอื่น ๆ ในทีมของคุณที่เห็นว่านี่เป็นคลาสข้อมูลอาจนำไปใช้ในทางที่ผิดโดยไม่ได้ตั้งใจโดยไม่ทราบว่าคุณแก้ไขอย่างไร / ทำให้ไม่ได้ผลตามที่คาดไว้ (เช่นวิธีนี้จะไม่ ' t ทำงานได้อย่างถูกต้องใน a Mapหรือ a Set)


สิ่งที่เกี่ยวกับคลาสข้อมูลที่ใช้สำหรับการทำให้เป็นอนุกรม / deserialisation ทำให้โครงสร้างซ้อนกันแบน เช่นฉันเพิ่งเขียนdata class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }และฉันคิดว่ามันค่อนข้างดีสำหรับกรณีของฉัน tbh คุณคิดอย่างไรเกี่ยวกับเรื่องนี้? (มีฟิลด์อื่น ๆ ของ ofc และด้วยเหตุนี้ฉันจึงเชื่อว่ามันไม่มีเหตุผลสำหรับฉันที่จะสร้างโครงสร้าง json ที่ซ้อนกันขึ้นใหม่ในรหัสของฉัน)
Antek

@Antek ระบุว่าคุณไม่ได้แก้ไขข้อมูลฉันไม่เห็นอะไรผิดปกติกับแนวทางนี้ ฉันจะพูดถึงเหตุผลที่คุณทำเช่นนี้เนื่องจากโมเดลฝั่งเซิร์ฟเวอร์ที่คุณกำลังถูกส่งไม่สะดวกที่จะใช้กับไคลเอนต์ เพื่อตอบโต้สถานการณ์ดังกล่าวทีมของฉันสร้างแบบจำลองฝั่งไคลเอ็นต์ที่เราแปลโมเดลฝั่งเซิร์ฟเวอร์เป็นหลังจาก deserialization เรารวมทั้งหมดนี้ไว้ใน API ฝั่งไคลเอ็นต์ เมื่อคุณเริ่มได้รับตัวอย่างที่ซับซ้อนกว่าที่คุณแสดงวิธีนี้จะมีประโยชน์มากเนื่องจากช่วยปกป้องไคลเอ็นต์จากการตัดสินใจ / apis โมเดลเซิร์ฟเวอร์ที่ไม่ดี
spierce7

ฉันไม่เห็นด้วยกับสิ่งที่คุณอ้างว่าเป็น "แนวทางที่ดีที่สุด" ปัญหาที่ฉันเห็นคือเป็นเรื่องปกติมากที่จะต้องการกำหนดค่าในคลาสข้อมูลและไม่เคยเปลี่ยนแปลง ตัวอย่างเช่นการแยกสตริงเป็น int ตัวรับ / ตัวตั้งค่าที่กำหนดเองบนคลาสข้อมูลไม่เพียง แต่มีประโยชน์เท่านั้น แต่ยังจำเป็นด้วย มิฉะนั้นคุณจะเหลือ Java bean POJO ที่ไม่ทำอะไรเลยและการตรวจสอบพฤติกรรม + มีอยู่ในคลาสอื่น ๆ
Abhijit Sarkar

สิ่งที่ฉันพูดคือ "นี่อาจเป็นแนวทางที่ดีที่สุดสำหรับกรณีส่วนใหญ่" ในกรณีส่วนใหญ่นักพัฒนาควรมีการแยกที่ชัดเจนระหว่างโมเดลและอัลกอริทึม / ตรรกะทางธุรกิจโดยที่โมเดลผลลัพธ์จากอัลกอริทึมแสดงสถานะต่างๆของผลลัพธ์ที่เป็นไปได้อย่างชัดเจน Kotlin ยอดเยี่ยมมากสำหรับสิ่งนี้ด้วยคลาสปิดผนึกและคลาสข้อมูล สำหรับตัวอย่างของparsing a string into an intคุณคุณอนุญาตให้ใช้ตรรกะทางธุรกิจในการแยกวิเคราะห์และข้อผิดพลาดในการจัดการสตริงที่ไม่ใช่ตัวเลขในคลาสโมเดลของคุณอย่างชัดเจน ...
spierce7

... การปฏิบัติในการบดบังเส้นแบ่งระหว่างโมเดลและตรรกะทางธุรกิจมักนำไปสู่รหัสที่บำรุงรักษาน้อยกว่าและฉันขอยืนยันว่าเป็นการต่อต้านรูปแบบ อาจเป็น 99% ของคลาสข้อมูลที่ฉันสร้างนั้นไม่เปลี่ยนรูป / ขาดตัวตั้งค่า ฉันคิดว่าคุณคงสนุกกับการอ่านเกี่ยวกับประโยชน์ของทีมของคุณที่ทำให้โมเดลไม่เปลี่ยนรูป ด้วยโมเดลที่ไม่เปลี่ยนรูปฉันสามารถรับประกันได้ว่าโมเดลของฉันจะไม่ถูกแก้ไขโดยไม่ได้ตั้งใจในตำแหน่งสุ่มอื่น ๆ ในโค้ดซึ่งจะช่วยลดผลข้างเคียงและอีกครั้งนำไปสู่รหัสที่บำรุงรักษาได้ คือ Kotlin ไม่ได้แยกออกจากกันListและMutableListไม่มีเหตุผล
spierce7

31

คุณสามารถลองสิ่งนี้:

data class Test(private val _value: Int) {
  val value = _value
    get(): Int {
      return if (field < 0) 0 else field
    }
}

assert(1 == Test(1).value)
assert(0 == Test(0).value)
assert(0 == Test(-1).value)

assert(1 == Test(1)._value) // Fail because _value is private
assert(0 == Test(0)._value) // Fail because _value is private
assert(0 == Test(-1)._value) // Fail because _value is private
  • ในชั้นเรียนข้อมูลคุณต้องเพื่อทำเครื่องหมายพารามิเตอร์ตัวสร้างหลักด้วยหรือvalvar

  • ฉันกำลังกำหนดค่าของ_valueto valueเพื่อใช้ชื่อที่ต้องการสำหรับคุณสมบัติ

  • ฉันกำหนดตัวเข้าถึงที่กำหนดเองสำหรับคุณสมบัติด้วยตรรกะที่คุณอธิบายไว้


2
ฉันได้รับข้อผิดพลาดเกี่ยวกับ IDE โดยระบุว่า "ไม่อนุญาตให้ใช้ Initializer ที่นี่เนื่องจากคุณสมบัตินี้ไม่มีฟิลด์สำรอง"
Cheng

6

คำตอบขึ้นอยู่กับสิ่งที่ความสามารถในการที่คุณใช้จริงที่dataให้ @EPadron กล่าวถึงเคล็ดลับที่ดี (เวอร์ชันปรับปรุง):

data class Test(private val _value: Int) {
    val value: Int
        get() = if (_value < 0) 0 else _value
}

ที่จะทำงานตามที่คาด, เนมันมีหนึ่งสาขาหนึ่ง getter ขวาequals, และhashcode component1สิ่งที่จับได้นั้นtoStringและcopyแปลก:

println(Test(1))          // prints: Test(_value=1)
Test(1).copy(_value = 5)  // <- weird naming

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


2

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

data class Test(private val value: Int) {
    fun getValue(): Int = if (value < 0) 0 else value
}

สิ่งนี้ควรจะถูกต้องอย่างสมบูรณ์เนื่องจาก Kotlin จะไม่สร้าง getter เริ่มต้นสำหรับฟิลด์ส่วนตัว

แต่อย่างอื่นฉันเห็นด้วยกับ spierce7 อย่างแน่นอนว่าคลาสข้อมูลมีไว้สำหรับการเก็บข้อมูลและคุณควรหลีกเลี่ยงตรรกะ "ธุรกิจ" แบบฮาร์ดโค้ดที่นั่น


ฉันเห็นด้วยกับวิธีแก้ปัญหาของคุณ แต่ในรหัสคุณจะต้องเรียกมันแบบนี้ val value = test.getValue() และไม่เหมือนกับ getters อื่น ๆ val value = test.value
gori

ใช่. ถูกต้อง. มันแตกต่างกันเล็กน้อยถ้าคุณเรียกมันจาก Java เพราะมันมักจะ.getValue()
bio007

1

ฉันได้เห็นคำตอบของคุณแล้วฉันยอมรับว่าคลาสข้อมูลมีไว้สำหรับการเก็บข้อมูลเท่านั้น แต่บางครั้งเราก็จำเป็นต้องทำบางสิ่งออกไป

นี่คือสิ่งที่ฉันทำกับคลาสข้อมูลของฉันฉันเปลี่ยนคุณสมบัติบางอย่างจาก val เป็น var และทับในตัวสร้าง

ดังนี้:

data class Recording(
    val id: Int = 0,
    val createdAt: Date = Date(),
    val path: String,
    val deleted: Boolean = false,
    var fileName: String = "",
    val duration: Int = 0,
    var format: String = " "
) {
    init {
        if (fileName.isEmpty())
            fileName = path.substring(path.lastIndexOf('\\'))

        if (format.isEmpty())
            format = path.substring(path.lastIndexOf('.'))

    }


    fun asEntity(): rc {
        return rc(id, createdAt, path, deleted, fileName, duration, format)
    }
}

การทำให้ฟิลด์ไม่แน่นอนเพื่อให้คุณสามารถแก้ไขได้ในระหว่างการเริ่มต้นถือเป็นแนวทางปฏิบัติที่ไม่ดี จะเป็นการดีกว่าที่จะทำให้ตัวสร้างเป็นแบบส่วนตัวจากนั้นสร้างฟังก์ชันที่ทำหน้าที่เป็นตัวสร้าง (เช่นfun Recording(...): Recording { ... }) นอกจากนี้คลาสข้อมูลอาจไม่ใช่สิ่งที่คุณต้องการเนื่องจากด้วยคลาสที่ไม่ใช่ข้อมูลคุณสามารถแยกคุณสมบัติของคุณจากพารามิเตอร์ตัวสร้างของคุณได้ เป็นการดีกว่าที่จะอธิบายอย่างชัดเจนกับความตั้งใจที่ไม่แน่นอนของคุณในนิยามชั้นเรียนของคุณ หากฟิลด์เหล่านั้นไม่สามารถเปลี่ยนแปลงได้ด้วยเช่นกันคลาสข้อมูลก็ใช้ได้ แต่คลาสข้อมูลของฉันเกือบทั้งหมดไม่เปลี่ยนรูป
spierce7

@ spierce7 มันแย่มากที่สมควรได้รับการโหวตลงหรือไม่? อย่างไรก็ตามโซลูชันนี้เหมาะกับฉันมากไม่ต้องใช้การเข้ารหัสมากนักและยังคงแฮชและเท่ากับเหมือนเดิม
Simou

0

นี่ดูเหมือนจะเป็นข้อเสียที่น่ารำคาญอย่างหนึ่งของ Kotlin

ดูเหมือนว่าวิธีแก้ปัญหาที่สมเหตุสมผลเพียงวิธีเดียวซึ่งช่วยให้ความเข้ากันได้ของคลาสย้อนหลังอย่างสมบูรณ์คือการแปลงเป็นคลาสปกติ (ไม่ใช่คลาส "ข้อมูล") และใช้งานด้วยมือ (ด้วยความช่วยเหลือของ IDE) วิธีการ: hashCode ( ), เท่ากับ (), toString (), คัดลอก () และ componentN ()

class Data3(i: Int)
{
    var i: Int = i

    override fun equals(other: Any?): Boolean
    {
        if (this === other) return true
        if (other?.javaClass != javaClass) return false

        other as Data3

        if (i != other.i) return false

        return true
    }

    override fun hashCode(): Int
    {
        return i
    }

    override fun toString(): String
    {
        return "Data3(i=$i)"
    }

    fun component1():Int = i

    fun copy(i: Int = this.i): Data3
    {
        return Data3(i)
    }

}

1
ไม่แน่ใจว่าฉันจะเรียกสิ่งนี้ว่าข้อเสียเปรียบ เป็นเพียงข้อ จำกัด ของคุณลักษณะคลาสข้อมูลซึ่งไม่ใช่คุณลักษณะที่ Java นำเสนอ
spierce7

0

ฉันพบว่าสิ่งต่อไปนี้เป็นแนวทางที่ดีที่สุดในการบรรลุสิ่งที่คุณต้องการโดยไม่ทำลายequalsและhashCode:

data class TestData(private var _value: Int) {
    init {
        _value = if (_value < 0) 0 else _value
    }

    val value: Int
        get() = _value
}

// Test value
assert(1 == TestData(1).value)
assert(0 == TestData(-1).value)
assert(0 == TestData(0).value)

// Test copy()
assert(0 == TestData(-1).copy().value)
assert(0 == TestData(1).copy(-1).value)
assert(1 == TestData(-1).copy(1).value)

// Test toString()
assert("TestData(_value=1)" == TestData(1).toString())
assert("TestData(_value=0)" == TestData(-1).toString())
assert("TestData(_value=0)" == TestData(0).toString())
assert(TestData(0).toString() == TestData(-1).toString())

// Test equals
assert(TestData(0) == TestData(-1))
assert(TestData(0) == TestData(-1).copy())
assert(TestData(0) == TestData(1).copy(-1))
assert(TestData(1) == TestData(-1).copy(1))

// Test hashCode()
assert(TestData(0).hashCode() == TestData(-1).hashCode())
assert(TestData(1).hashCode() != TestData(-1).hashCode())

อย่างไรก็ตาม

ครั้งแรกที่ทราบว่า_valueเป็นvarไม่ได้valแต่ในมืออื่น ๆ เนื่องจากเป็นส่วนตัวและการเรียนข้อมูลไม่สามารถรับมรดกมาจากมันค่อนข้างง่ายที่จะตรวจสอบให้แน่ใจว่าจะไม่แก้ไขภายในชั้นเรียน

ประการที่สองtoString()ก่อให้ผลแตกต่างกันเล็กน้อยกว่ามันจะถ้า_valueเป็นชื่อแต่มันเป็นเรื่องที่สอดคล้องกันและvalueTestData(0).toString() == TestData(-1).toString()


@ spierce7 ไม่มันไม่ใช่ _valueกำลังถูกแก้ไขในบล็อก init equalsและhashCode ไม่เสีย
schatten

คุณช่วยฉันด้วยstackoverflow.com/questions/64015108/…
WISHY
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.