Ghi đè getter cho lớp dữ liệu Kotlin


94

Cho lớp Kotlin sau:

data class Test(val value: Int)

Làm cách nào để ghi đè Intgetter để nó trả về 0 nếu giá trị âm?

Nếu điều này là không thể, một số kỹ thuật để đạt được kết quả phù hợp là gì?


13
Vui lòng xem xét việc thay đổi cấu trúc mã của bạn để các giá trị âm được chuyển đổi thành 0 khi lớp được khởi tạo chứ không phải trong getter. Nếu bạn ghi đè getter như được mô tả trong câu trả lời bên dưới, tất cả các phương thức được tạo khác như bằng (), toString () và quyền truy cập thành phần sẽ vẫn sử dụng giá trị âm ban đầu, điều này có thể dẫn đến hành vi đáng ngạc nhiên.
yole 21-07-16

Câu trả lời:


138

Sau khi dành gần một năm để viết Kotlin hàng ngày, tôi thấy rằng cố gắng ghi đè các lớp dữ liệu như thế này là một việc làm không tốt. Có 3 cách tiếp cận hợp lệ cho điều này và sau khi tôi trình bày chúng, tôi sẽ giải thích tại sao cách tiếp cận mà các câu trả lời khác đã đề xuất là không tốt.

  1. Yêu cầu logic nghiệp vụ của bạn tạo ra data classthay đổi giá trị thành 0 hoặc lớn hơn trước khi gọi hàm tạo có giá trị xấu. Đây có lẽ là cách tiếp cận tốt nhất cho hầu hết các trường hợp.

  2. Không sử dụng a data class. Sử dụng một classIDE thông thường và để IDE của bạn tạo ra các equalshashCodephương thức cho bạn (hoặc không, nếu bạn không cần chúng). Có, bạn sẽ phải tạo lại nó nếu bất kỳ thuộc tính nào bị thay đổi trên đối tượng, nhưng bạn có toàn quyền kiểm soát đối tượng.

    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. Tạo một thuộc tính an toàn bổ sung trên đối tượng thực hiện những gì bạn muốn thay vì có một giá trị riêng tư bị ghi đè hiệu quả.

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

Một cách tiếp cận tồi mà các câu trả lời khác đang đề xuất:

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

Vấn đề với cách tiếp cận này là các lớp dữ liệu không thực sự dành cho việc thay đổi dữ liệu như thế này. Chúng thực sự chỉ để giữ dữ liệu. Trọng các getter cho một lớp dữ liệu như thế này sẽ có nghĩa là Test(0)Test(-1)sẽ không equallẫn nhau và sẽ có khác nhau hashCodes, nhưng khi bạn gọi .value, họ sẽ có kết quả tương tự. Điều này không nhất quán và mặc dù nó có thể hiệu quả với bạn, nhưng những người khác trong nhóm của bạn xem đây là một lớp dữ liệu, có thể vô tình sử dụng sai mà không nhận ra bạn đã thay đổi nó như thế nào / khiến nó không hoạt động như mong đợi (nghĩa là cách tiếp cận này sẽ không ' t hoạt động chính xác trong a Maphoặc a Set).


những gì về các lớp dữ liệu được sử dụng để tuần tự hóa / giải mã hóa, làm phẳng một cấu trúc lồng nhau? Ví dụ như tôi vừa viết data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }, và tôi coi nó khá tốt cho trường hợp của tôi, tbh. Bạn nghĩ gì về điều này? (có OFC các lĩnh vực khác và do đó tôi tin rằng nó làm cho không có ý nghĩa với tôi để tái cấu trúc json lồng nhau trong mã của tôi)
Antek

@Antek Cho rằng bạn không thay đổi dữ liệu, tôi không thấy có gì sai với cách tiếp cận này. Tôi cũng sẽ đề cập rằng lý do bạn làm điều này là vì mô hình phía máy chủ mà bạn đang được gửi không thuận tiện để sử dụng trên máy khách. Để đối phó với những tình huống như vậy, nhóm của tôi tạo một mô hình phía máy khách mà chúng tôi dịch mô hình phía máy chủ thành sau khi giải không khí. Chúng tôi gói gọn tất cả những điều này trong một api phía máy khách. Khi bạn bắt đầu nhận được các ví dụ phức tạp hơn những gì bạn đã trình bày, cách tiếp cận này rất hữu ích vì nó bảo vệ máy khách khỏi các quyết định / apis mô hình máy chủ xấu.
spierce

Tôi không đồng ý với những gì bạn cho là "cách tiếp cận tốt nhất". Tôi thấy vấn đề là rất phổ biến khi muốn đặt một giá trị trong một lớp dữ liệu và không bao giờ thay đổi nó. Ví dụ: phân tích cú pháp một chuỗi thành một int. Bộ nhận / thiết lập tùy chỉnh trên một lớp dữ liệu không chỉ hữu ích mà còn cần thiết; nếu không, bạn còn lại với các POJO của Java bean không làm gì cả và hành vi + xác thực của chúng được chứa trong một số lớp khác.
Abhijit Sarkar,

Những gì tôi đã nói là "Đây có lẽ là cách tiếp cận tốt nhất cho hầu hết các trường hợp". Trong hầu hết các trường hợp, trừ khi phát sinh một số trường hợp nhất định, các nhà phát triển nên có sự tách biệt rõ ràng giữa mô hình của họ và thuật toán / logic nghiệp vụ, trong đó mô hình kết quả từ thuật toán của họ thể hiện rõ ràng các trạng thái khác nhau của kết quả có thể có. Kotlin thật tuyệt vời cho việc này, với các lớp được niêm phong và các lớp dữ liệu. Đối với ví dụ của parsing a string into an intbạn, rõ ràng bạn đang cho phép logic nghiệp vụ của việc phân tích cú pháp và xử lý lỗi các Chuỗi không phải số vào lớp mô hình của bạn ...
spierce

... Việc làm xáo trộn ranh giới giữa mô hình và logic nghiệp vụ luôn dẫn đến mã ít có thể bảo trì hơn, và tôi cho rằng đó là một mô hình chống. Có lẽ 99% các lớp dữ liệu tôi tạo là các bộ định tuyến không thay đổi / thiếu. Tôi nghĩ rằng bạn thực sự thích dành chút thời gian để đọc về những lợi ích của việc nhóm của bạn giữ cho các mô hình của họ không thay đổi. Với các mô hình bất biến, tôi có thể đảm bảo các mô hình của mình không bị sửa đổi ngẫu nhiên ở một số vị trí ngẫu nhiên khác trong mã, điều này làm giảm tác dụng phụ và một lần nữa, dẫn đến mã có thể bảo trì được. tức là Kotlin đã không tách biệt ListMutableListkhông có lý do.
spierce

31

Bạn có thể thử một cái gì đó như sau:

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
  • Trong một lớp dữ liệu, bạn phải đánh dấu các tham số của hàm tạo chính bằng valhoặc var.

  • Tôi gán giá trị của _valueđể valueđể sử dụng tên mong muốn cho tài sản.

  • Tôi đã xác định một trình truy cập tùy chỉnh cho thuộc tính với logic bạn đã mô tả.


1
Tôi gặp lỗi trên IDE, nó cho biết "Không cho phép khởi tạo ở đây vì thuộc tính này không có trường hỗ trợ"
Cheng

6

Câu trả lời phụ thuộc vào những khả năng bạn thực sự sử dụng datacung cấp. @EPadron đã đề cập đến một thủ thuật tiện lợi (phiên bản cải tiến):

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

Điều đó sẽ hoạt động như mong đợi, ei nó có một lĩnh vực, một tầng, đúng equals, hashcodecomponent1. Cái bắt là vậy toStringcopythật kỳ lạ:

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

Để khắc phục sự cố, toStringbạn có thể xác định lại nó bằng tay. Tôi không biết cách nào để sửa việc đặt tên tham số nhưng hoàn toàn không sử dụng data.


2

Tôi biết đây là một câu hỏi cũ nhưng có vẻ như không ai đề cập đến khả năng đặt giá trị riêng tư và viết getter tùy chỉnh như thế này:

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

Điều này phải hoàn toàn hợp lệ vì Kotlin sẽ không tạo getter mặc định cho trường riêng tư.

Nhưng nếu không, tôi chắc chắn đồng ý với spierce7 rằng các lớp dữ liệu là để lưu giữ dữ liệu và bạn nên tránh mã hóa logic "nghiệp vụ" ở đó.


Tôi đồng ý với giải pháp của bạn nhưng so với trong mã, bạn sẽ phải gọi nó như thế này val value = test.getValue() và không giống như các getters khác val value = test.value
gori

Đúng. Đúng rồi. Có một chút khác biệt nếu bạn gọi nó từ Java vì nó luôn ở đó.getValue()
bio007

1

Tôi đã xem câu trả lời của bạn, tôi đồng ý rằng các lớp dữ liệu chỉ dùng để lưu trữ dữ liệu, nhưng đôi khi chúng ta cần tạo ra một cái gì đó từ chúng.

Đây là những gì tôi đang làm với lớp dữ liệu của mình, tôi đã thay đổi một số thuộc tính từ val thành var và ghi lại chúng trong hàm tạo.

như vậy:

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

Làm cho các trường có thể thay đổi chỉ để bạn có thể sửa đổi chúng trong quá trình khởi tạo là một việc làm không tốt. Tốt hơn là đặt hàm tạo là riêng tư, rồi tạo một hàm hoạt động như một hàm tạo (tức là fun Recording(...): Recording { ... }). Ngoài ra, có thể một lớp dữ liệu không phải là thứ bạn muốn, vì với các lớp không phải dữ liệu, bạn có thể tách các thuộc tính của mình khỏi các tham số phương thức khởi tạo. Tốt hơn là bạn nên rõ ràng với ý định thay đổi trong định nghĩa lớp của bạn. Nếu các trường đó cũng có thể thay đổi được, thì một lớp dữ liệu vẫn ổn, nhưng hầu như tất cả các lớp dữ liệu của tôi là bất biến.
spierce7

@ spierce7 có thực sự tệ đến mức đáng bị bỏ phiếu không? Dù sao, giải pháp này cũng phù hợp với tôi, nó không yêu cầu quá nhiều mã hóa và nó giữ nguyên hàm băm và giá trị bằng.
Simou

0

Đây dường như là một trong những nhược điểm khó chịu của Kotlin.

Có vẻ như giải pháp hợp lý duy nhất, hoàn toàn giữ được tính tương thích ngược của lớp là chuyển đổi nó thành một lớp thông thường (không phải lớp "dữ liệu") và triển khai bằng tay (với sự hỗ trợ của IDE) các phương thức: hashCode ( ), bằng (), toString (), copy () và 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)
    }

}

Không chắc tôi sẽ gọi đây là một nhược điểm. Nó chỉ đơn thuần là một hạn chế của tính năng lớp dữ liệu, đây không phải là một tính năng mà Java cung cấp.
spierce 7

0

Tôi nhận thấy những điều sau đây là cách tiếp cận tốt nhất để đạt được những gì bạn cần mà không bị phá vỡ equalshashCode:

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

Tuy nhiên,

Đầu tiên, lưu ý rằng _valuevar, khôngval , nhưng mặt khác, vì nó tin và các lớp dữ liệu không thể được thừa hưởng từ, nó là khá dễ dàng để chắc chắn rằng nó không được sửa đổi trong lớp.

Thứ hai, toString()tạo ra một kết quả hơi khác so với nó nếu _valueđược đặt tên value, nhưng nó nhất quán và TestData(0).toString() == TestData(-1).toString().


@ spierce7 Không, không phải đâu. _valueđang được sửa đổi trong khối init equalshashCode không bị hỏng.
schatten
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.