Semester
is a simple enum class
.
data class YearSemester(
val year: Int,
val semester: Semester
) {
constructor(string: String) : this(
string.split("-")[0].toInt(),
Semester.valueOf(string.split("-")[1])
)
}
The point is that I don't want to split the String string twice.
So to say:
data class YearSemester(
val year: Int,
val semester: Semester
) {
constructor(string: String){
val splitted = string.split("-")
this(splitted[0].toInt(), Semester.valueOf(splitted[1]))
}
}
However, the Kotlin syntax does not permit this code as the primary constructor must be called at the first of the secondary constructors.
Is there a more elegant way to handle situations like this? (Performing some complex operation before calling the primary constructor from secondary constructor)
3 Answers 3
Here are two more options:
- Write a factory function that has the same name as the class. They do this a lot in the standard library, although mostly for interfaces. The default lint rules warn about functions starting with capital letters, but not if the name matches the type it returns. For this reason, this maybe should be considered the conventional approach.
data class YearSemester(
val year: Int,
val semester: Semester
)
fun YearSemester(string: String): YearSemester {
val splitted = string.split("-")
return YearSemester(splitted[0].toInt(), Semester.valueOf(splitted[1]))
}
- Add an
invoke
operator function to its companion object. Then it can be called with the same syntax as a constructor. I've seen people do this, but it seems kind of awkward to me.
data class YearSemester(
val year: Int,
val semester: Semester
) {
companion object {
operator fun invoke(string: String): YearSemester {
val splitted = string.split("-")
return YearSemester(splitted[0].toInt(), Semester.valueOf(splitted[1]))
}
}
-
\$\begingroup\$ For future visitors: proandroiddev.com/… \$\endgroup\$Hyeonseo Yang– Hyeonseo Yang2022年02月23日 05:20:53 +00:00Commented Feb 23, 2022 at 5:20
The reason Kotlin enforces primary constructor to be called as the first thing, is to ensure that constructors don't hide lot of logic before the construction of the object.
So where do the complex operations go? Perhaps abstracted in functions which can be named accordingly to promote code readability. The function in this case could be an extension function on String
as shown below:
fun String.toYearSemester(): YearSemester {
val splitParts = this.split("-")
return YearSemester(splitParts[0].toInt(), Semester.valueOf(splitParts[1]))
}
Alternatively the logic could be implemented as a factory function as in the answer by Tenfour4, which promotes readability as well, though maybe not as much as specifically named functions relevant to logic that they are abstracting within.
There are a few options:
- (My favorite) use a factory method. That is, put the logic in a method outside your class, and call your primary constructor from that method.
fun createYearSemester(string: String): YearSemester {
val splitted = string.split("-")
return YearSemester(splitted[0].toInt(), Semester.valueOf(splitted[1]))
}
- Don't use a
data class
, use a regular class and just overrideequals
,hashCode
, andtoString
by yourself.
class YearSemester(string: String) {
val year: String
val semester: Semester
init {
val splitted = string.split("-")
this.year = splitted[0].toInt()
this.semester = Semester.valueOf(splitted[1])
}
override fun equals(): String { ... }
override fun hashCode(): Int { ... }
override fun toString(): String { ... }
}