Kotlin 데이터 클래스 용 getter 재정의


98

다음 Kotlin 클래스가 주어지면 :

data class Test(val value: Int)

Int값이 음수이면 0을 반환하도록 getter를 어떻게 재정의 합니까?

이것이 가능하지 않다면 적절한 결과를 얻기위한 몇 가지 기술은 무엇입니까?


14
클래스가 인스턴스화 될 때 음수 값이 getter가 아닌 0으로 변환되도록 코드의 구조를 변경하는 것을 고려하십시오. 아래 답변에 설명 된대로 getter를 재정의하면 equals (), toString () 및 구성 요소 액세스와 같은 다른 모든 생성 된 메서드는 여전히 원래 음수 값을 사용하므로 놀라운 동작이 발생할 수 있습니다.
yole jul.

답변:


148

매일 Kotlin을 작성하는 데 거의 1 년을 보낸 후 이와 같은 데이터 클래스를 재정의하려는 시도가 나쁜 습관이라는 것을 알게되었습니다. 이에 대한 세 가지 유효한 접근 방식이 있으며, 제시 한 후에 다른 답변이 제안한 접근 방식이 왜 나쁜지 설명하겠습니다.

  1. data class잘못된 값으로 생성자를 호출하기 전에 값을 0 이상으로 변경하는 비즈니스 논리를 만드십시오 . 이것은 아마도 대부분의 경우에 가장 좋은 방법 일 것입니다.

  2. 를 사용하지 마십시오 data class. 일반을 사용하고 classIDE에서 equalshashCode메서드를 생성하도록 합니다 (또는 필요하지 않은 경우 생성하지 않음). 예, 개체의 속성이 변경된 경우 다시 생성해야하지만 개체를 ​​완전히 제어 할 수 있습니다.

    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서로 다른 것 hashCode들,하지만 당신이 전화했을 때 .value, 그들은 같은 결과를 가질 것이다. 이것은 일관성이 없으며 이것이 당신에게는 효과가있을 수 있지만 이것이 데이터 클래스라고 생각하는 팀의 다른 사람들은 당신이 그것을 어떻게 변경했는지 / 예상대로 작동하지 않게 만들 었는지 깨닫지 못한 채 실수로 그것을 오용 할 수 있습니다. t Map또는 a Set) 에서 올바르게 작동합니다 .


직렬화 / 역 직렬화에 사용되는 데이터 클래스는 어떻습니까? 예를 들어 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 데이터를 변경하지 않는다는 점을 감안할 때이 접근 방식에 문제가있는 것은 아닙니다. 또한이 작업을 수행하는 이유는 전송되는 서버 측 모델이 클라이언트에서 사용하기 편리하지 않기 때문이라고 언급합니다. 이러한 상황에 대응하기 위해 우리 팀은 역 직렬화 후 서버 측 모델을 변환하는 클라이언트 측 모델을 만듭니다. 우리는이 모든 것을 클라이언트 측 API로 포장합니다. 표시된 것보다 더 복잡한 예제를 얻기 시작하면이 접근 방식은 클라이언트를 잘못된 서버 모델 결정 / api로부터 보호하므로 매우 유용합니다.
spierce7

나는 당신이 "최상의 접근"이라고 주장하는 것에 동의하지 않습니다. 내가 보는 문제 는 데이터 클래스에 값을 설정하고 절대 변경하지 않으려 는 것이 매우 일반적이라는 것입니다. 예를 들어 문자열을 int로 구문 분석합니다. 데이터 클래스에 대한 사용자 정의 getter / setter는 유용 할뿐만 아니라 필요합니다. 그렇지 않으면 아무것도하지 않는 Java bean POJO가 남고 동작 + 유효성 검사가 다른 클래스에 포함됩니다.
Abhijit Sarkar

내가 말한 것은 "이것은 아마도 대부분의 경우에 가장 좋은 접근 방법"입니다. 대부분의 경우 특정 상황이 발생하지 않는 한 개발자는 모델과 알고리즘 / 비즈니스 로직을 명확하게 구분해야합니다. 여기서 알고리즘의 결과 모델은 가능한 결과의 다양한 상태를 명확하게 나타냅니다. Kotlin은 봉인 된 클래스와 데이터 클래스를 통해이를 위해 환상적입니다. 당신의 예를 들어 parsing a string into an int, 당신은 명확하게 분석하고 오류가 모델 클래스에 숫자가 아닌 문자열을 처리하는 비즈니스 로직을 허용하고 ...
spierce7

... 모델과 비즈니스 로직 사이의 경계를 혼란스럽게하는 관행은 항상 유지 관리가 덜한 코드로 이어지며, 안티 패턴이라고 생각합니다. 아마도 내가 만드는 데이터 클래스의 99 %는 불변이거나 setter가 부족합니다. 모델을 변경 불가능하게 유지하는 팀의 이점에 대해 읽는 데 시간을 할애하는 것이 정말 즐겁다 고 생각합니다. 불변 모델을 사용하면 내 모델이 코드의 다른 임의의 위치에서 실수로 수정되지 않도록 보장하여 부작용을 줄이고 다시 유지 관리 가능한 코드로 이어집니다. 즉, 코 틀린은 분리되지 않았다 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
  • 데이터 클래스에서 기본 생성자의 매개 변수를 val또는 로 표시해야합니다 var.

  • 나는 값 할당하고 있습니다 _value로를 value속성에 원하는 이름을 사용하기 위해.

  • 설명하신 논리로 속성에 대한 사용자 지정 접근자를 정의했습니다.


2
IDE에서 오류가 발생했습니다. "이 속성에는 지원 필드가 없기 때문에 여기에서 초기화 프로그램이 허용되지 않습니다."
Cheng

6

대답은 실제로 사용하는 기능에 따라 다릅니다 data. @EPadron은 멋진 트릭 (개선 버전)을 언급했습니다.

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

그 의지는 예상대로 EI가있다, 작동 하나 개의 권리, 필드, 하나 게터를 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() 다른 게터들 처럼 부르지 말고 이렇게 불러야 할 것입니다 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 ( ), equals (), toString (), copy () 및 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 2017-09-23

0

중단하지 않고 필요한 것을 달성하는 가장 좋은 방법은 다음 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())

하나,

첫째, _valuevar아니라는 점에 유의하십시오. val반면에 개인용이고 데이터 클래스를 상속 할 수 없기 때문에 클래스 내에서 수정되지 않도록하는 것이 상당히 쉽습니다.

둘째, 라는 이름의 toString()경우와 약간 다른 결과를 생성 하지만 일관성 있고 ._valuevalueTestData(0).toString() == TestData(-1).toString()


@ spierce7 아니요, 그렇지 않습니다. _value초기화 블록으로 변형되고 있고 equalshashCode 파손되지 않는다.
schatten

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.