Kotlinデータクラスのゲッターをオーバーライドする


94

次のKotlinクラスがあるとします。

data class Test(val value: Int)

Int値が負の場合、0を返すようにゲッターをどのようにオーバーライドしますか?

これが不可能な場合、適切な結果を得るにはどのようなテクニックがありますか?


13
ゲッター内ではなく、クラスがインスタンス化されるときに負の値が0に変換されるように、コードの構造を変更することを検討してください。以下の回答で説明されているようにゲッターをオーバーライドすると、equals()、toString()、コンポーネントアクセスなどの他のすべての生成されたメソッドは元の負の値を引き続き使用するため、予期しない動作が発生する可能性があります。
yole

回答:


138

Kotlinを毎日ほぼ1年間執筆した後、このようなデータクラスをオーバーライドしようとすることは悪い習慣であることがわかりました。これには3つの有効なアプローチがあります。それらを紹介した後、他の回答が示唆しているアプローチがなぜ悪いのかを説明します。

  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
}

このアプローチの問題は、データクラスがこのようにデータを変更するためのものではないことです。それらは本当にデータを保持するためだけのものです。このようなデータクラスのgetterをオーバーライドすることを意味するだろうTest(0)Test(-1)考えていないequal互いに異なるだろうhashCodeSを、しかし、あなたが呼ばれたとき.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。これについてどう思う?(他にも多くのフィールドがあったため、コードにネストされたjson構造を再作成することは私には意味がなかったと思います)
Antek

@Antekあなたがデータを変更していないことを考えると、私はこのアプローチで何も問題はないと思います。また、これを行う理由は、送信されるサーバー側モデルがクライアントでの使用に不便であるためです。このような状況に対処するために、私のチームは、サーバー側モデルを逆シリアル化後に変換するクライアント側モデルを作成します。これらすべてをクライアント側のAPIでラップします。これまでに示した例よりも複雑な例の取得を開始すると、このアプローチは、サーバーモデルの不適切な決定やAPIからクライアントを保護するのに非常に役立ちます。
spierce7 2018

私があなたが「最善のアプローチ」であると主張することに同意しません。私が目にする問題は、データクラスに値を設定し、決して変更しないことが非常に一般的であるということです。たとえば、文字列をintに解析します。データクラスのカスタムゲッター/セッターは便利なだけでなく、必要です。そうしないと、何もしないJava Bean POJOが残り、その動作+検証は他のクラスに含まれます。
Abhijit Sarkar

私が言ったことは、「これはおそらくほとんどの場合に最適なアプローチです」です。ほとんどの場合、特定の状況が発生しない限り、開発者はモデルとアルゴリズム/ビジネスロジックを明確に分離する必要があります。アルゴリズムから結果として得られるモデルは、考えられる結果のさまざまな状態を明確に表します。Kotlinは、シールされたクラスとデータクラスを備えており、これに最適です。の例としてparsing a string into an int、数値クラス以外の文字
列を

...モデルとビジネスロジックの間の境界線を曖昧にする習慣は、常に保守性の低いコードにつながります。私は反パターンであると主張します。おそらく、私が作成するデータクラスの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
  • データクラスでは、プライマリコンストラクタのパラメータをvalまたはでマークする必要がありますvar

  • プロパティに目的の名前を使用するために、_valueto の値を割り当ててvalueいます。

  • 説明したロジックを使用して、プロパティのカスタムアクセサーを定義しました。


1
IDEでエラーが発生しました。「このプロパティにはバッキングフィールドがないため、イニシャライザはここでは許可されていません」
Cheng

6

答えは、実際に使用する機能によって異なりdataます。@EPadronは気の利いたトリック(改良版)について言及しました:

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

その意志が期待通りEIことがあり、作品1つの右、フィールド、1つのゲッターをequalshashcodecomponent1。キャッチはそれでtoStringありcopy、奇妙です:

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

問題を修正するにはtoString、手作業で再定義する必要があります。パラメータの名前を修正する方法はありませんが、まったく使用dataしません。


2

私はこれが古い質問であることを知っていますが、値をプライベートにして、次のようにカスタムゲッターを作成する可能性について誰も言及していないようです:

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

Kotlinはプライベートフィールドのデフォルトのゲッターを生成しないため、これは完全に有効です。

しかし、それ以外の点では、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)
    }

}

これを欠点と呼ぶかどうかはわかりません。これは単なるデータクラス機能の制限であり、Javaが提供する機能ではありません。
spierce7 2017

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

しかしながら、

まず、_valuevar、ではなくval、ですが、プライベートクラスであり、データクラスは継承できないため、クラス内で変更されていないことを確認するのは非常に簡単です。

2つ目は、という名前のtoString()場合と_valueは少し異なる結果を生成しますが、とはvalue一貫していTestData(0).toString() == TestData(-1).toString()ます。


@ spierce7いいえ、違います。_valueinitブロックで変更されておりequalshashCode 壊れていません。
2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.