Цель этого поста не в том, чтобы указать на некоторые серьезные недостатки в дизайне классов данных Kotlin и показать вам, как их пропустить. На самом деле, это совсем наоборот. Содержание этого поста четко документировано в документации Kotlin . Я просто здесь, чтобы донести эту информацию до тех, кто не заметил, как именно работают их классы данных.
Классы данных удобны для нас, разработчиков, особенно для тех из нас, кто пришел из Java. Они предоставляют несколько сгенерированных функций, позволяющих вам написать полностью функциональный класс с очень небольшим количеством кода. Классы данных предоставляют следующие сгенерированные функции:
equals
-
hashCode
-
toString
-
copy
- Компонент N () функционирует .
Все вышеперечисленное генерируется для свойств, определенных в первичном конструкторе класса. Все, что определено за пределами этого конструктора, игнорируется. Это потенциальная ловушка . Но это только ловушка, если вы не знаете, как работают классы данных. Как я упоминал ранее, это четко задокументировано , вам нужно только помнить об этом. Кем вы сейчас являетесь, конечно.
Если вы не продумываете, как вы определяете свои классы данных, вы, вероятно, найдете некоторые ошибки в своем приложении. equals
и hashCode
как правило, важные функции. Если они не работают должным образом, ошибки обязательно последуют.
Ниже приведен пример этого:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
data class MyClass(val a: String, val b: Int) { // property defined outside of primary constructor lateinit var c: String } fun main() { // create two equal objects val myClass = MyClass( "abc" , 0 ) val myClass2 = myClass.copy() // check their hashCodes are the same and that they equal each other println( "myClass hashCode: ${myClass.hashCode()}" ) println( "myClass2 hashCode: ${myClass2.hashCode()}" ) println( "myClass == myClass2: ${myClass == myClass2}" ) // set the lateinit variables myClass.c = "im a lateinit var" myClass2.c = "i have a different value" // have their hashCodes changed? println( "myClass hashCode after setting lateinit var: ${myClass.hashCode()}" ) println( "myClass2 hashCode after setting lateinit var: ${myClass2.hashCode()}" ) // are they still equal to each other? println( "myClass == myClass2 after setting lateinit vars: ${myClass == myClass2}" ) // sanity check to make sure I'm not being stupid println( "sanity checking myClass.c: ${myClass.c}" ) println( "sanity checking myClass2.c: ${myClass2.c}" ) } |
Выполнение этого примера выводит:
1
2
3
4
5
6
7
8
|
myClass hashCode: 2986974 myClass2 hashCode: 2986974 myClass == myClass2: true myClass hashCode after setting lateinit var: 2986974 myClass2 hashCode after setting lateinit var: 2986974 myClass == myClass2 after setting lateinit vars: true sanity checking myClass.c: im a lateinit var sanity checking myClass2.c: i have a different value |
Как вы можете видеть, hashCode
каждого объекта одинаков, и они оба равны друг другу, хотя их свойства c
различны. Если вы попытались использовать MyClass
внутри Map
или Set
, вероятность столкновения записей друг с другом увеличивается. Это сказанное, это действительно зависит от того, чего вы пытаетесь достичь. Может быть, это именно то, что вы хотите, чтобы это произошло. В этом случае, больше власти для вас.
Помещение c
в конструктор MyClass
повлияет на hashCode
и equals
. Затем c
будет участвовать в любых вызовах hashCode
, equals
и остальных сгенерированных функций.
Другое решение — вручную реализовать сгенерированные функции. Переписать класс как:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
data class MyClass(val a: String, val b: Int) { lateinit var c: String override fun equals(other: Any?): Boolean { if ( this === other) return true if (javaClass != other?.javaClass) return false other as MyClass if (a != other.a) return false if (b != other.b) return false if (c != other.c) return false return true } override fun hashCode(): Int { var result = a.hashCode() result = 31 * result + b result = 31 * result + c.hashCode() return result } override fun toString(): String { return "MyClass(a='$a', b=$b, c='$c')" } } |
Эти реализации были любезно предоставлены Intellij ?. Указывая все свойства в каждой из переопределенных функций, теперь используются любые свойства, не включенные в основной конструктор (в данном случае c
). hashCode
и equals
теперь лучше представляют класс и улучшают его использование внутри Map
или Set
.
Но и большой, но ?. По крайней мере, в коде, который я написал. Ошибка была введена. c
— это lateinit var
и теперь каждая из переопределенных функций пытается получить к ней доступ. Если какая-либо из этих функций вызывается до установки c
вы получите исключение:
1
2
3
4
|
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property c has not been initialized at dev.lankydan.MyClass.hashCode(DataClasses.kt: 60 ) at dev.lankydan.DataClassesKt.main(DataClasses.kt: 72 ) at dev.lankydan.DataClassesKt.main(DataClasses.kt) |
Перезапись equals
, hashCode
и toString
для размещения lateinit var
разрешит эту ошибку:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
data class MyClass(val a: String, val b: Int) { lateinit var c: String override fun equals(other: Any?): Boolean { if ( this === other) return true if (javaClass != other?.javaClass) return false other as MyClass if (a != other.a) return false if (b != other.b) return false if ( this ::c.isInitialized && (other as MyClass)::c.isInitialized && c != other.c) return false return true } override fun hashCode(): Int { var result = a.hashCode() result = 31 * result + b if ( this ::c.isInitialized) { result = 31 * result + c.hashCode() } return result } override fun toString(): String { return if ( this ::c.isInitialized) "MyClass(a='$a', b=$b, c='$c')" else "MyClass(a='$a', b=$b)" } } |
Эта реализация безопасна в использовании, даже если значение lateinit var
не установлено.
Хотите ли вы сделать это или нет, зависит от требований вашего класса. Использование класса данных, как у меня здесь, внутри Map
, вероятно, не рекомендуется. Если вы хотите сделать это, вы можете. Просто знайте, как все это работает.
Если вы еще этого не сделали, я рекомендую вам быстро взглянуть на документацию по этому вопросу. Выделение этой информации было целью этого поста. Это не какой-то причудливый код, который делает что-то волшебное. Вместо этого это нечто более фундаментальное и фундаментальное для того, как работает Котлин. Знание того, как работают классы данных в этом аспекте, может иметь жизненно важное значение для уменьшения количества ошибок в вашем приложении.
Опубликовано на Java Code Geeks с разрешения Дэна Ньютона, партнера нашей программы JCG . Смотрите оригинальную статью здесь: потенциальные ловушки в классах данных Kotlin Мнения, высказанные участниками Java Code Geeks, являются их собственными. |