Programming Kotlin Applications. Бретт Мак-Лахлин
mutator—in our example, that's the
String
“Bobby.”
The final piece here is understanding why you can't say something like this:
var firstName: String = _firstName set(value) { firstName = value }
This will actually compile, but it will give you a horrible error when you try to run and use this class. The problem is that when Kotlin sees this:
firstName = value
it interprets that as “run the code that mutates firstName
.” But that code is the code already running. So it calls itself—a concept called recursion—and that same line runs again. And again. And again. And… well, you get the idea.
By using the backing field field
, you avoid this recursion, and things behave. Go ahead and create a custom mutator for firstName
and lastName
; things should look like Listing 2.7 when you're finished.
LISTING 2.7: Defining custom mutators for lastName and firstName
package org.wiley.kotlin.person class Person(_firstName: String, _lastName: String, _height: Double, _age: Int, _hasPartner: Boolean) { var fullName: String var firstName: String = _firstName set(value) { field = value } var lastName: String = _lastName set(value) { field = value } var height: Double = _height var age: Int = _age var hasPartner: Boolean = _hasPartner // Set the full name when creating an instance init { fullName = "$firstName $lastName" } override fun toString(): String { return fullName } }
WARNING The good news about Listing 2.7 is that you can literally copy the mutator code from firstName
to use again for lastName
. Since field
applies to whatever backing field is being used, it works for both properties. The bad news is that skimming Kotlin code can sometimes be hairy, because you'll see lots of blocks that look similar—just like these.
CLASSES CAN HAVE CUSTOM BEHAVIOR
What we need is a way to define some custom behavior. Specifically, when a first name or last name of a person is changed, their full name should also be changed. With custom mutators, part of this work is done; we have a place to call custom code, but no custom code to call.
Define a Custom Method on Your Class
What we need, then, is a new class method. Let's call it updateName()
, and have it update the fullName
property. Then, we can call updateName()
every time a name needs to be changed. There's really nothing magical here, as you already have created a method with fun
. Here's what you really want:
fun updateName() { fullName = "$firstName $lastName" }
This is actually the exact same code that already exists in your Person
's init
block. But that code isn't needed anymore! Instead, you can call updateName()
within your init
block. Listing 2.8 shows you what Person
should look like when you're finished.
LISTING 2.8: Creating a new property method to update the fullName variable
package org.wiley.kotlin.person class Person(_firstName: String, _lastName: String, _height: Double, _age: Int, _hasPartner: Boolean) { var fullName: String var firstName: String = _firstName set(value) { field = value } var lastName: String = _lastName set(value) { field = value } var height: Double = _height var age: Int = _age var hasPartner: Boolean = _hasPartner // Set the full name when creating an instance init { updateName() } fun updateName() { fullName = "$firstName $lastName" } override fun toString(): String { return fullName } }
Every Property Must Be Initialized
This doesn't look a lot different. However, there's a problem. Compile this code and you're going to get an error that, by now, is probably becoming a bit familiar:
Error:(5, 5) Kotlin: Property must be initialized or be abstract
What's going on here? Well, it's a little bit of a pain. Remember, any property, must either be assigned an initial value when they are declared (like firstName
and lastName
, for example), or be assigned a value in the init
block.
Now, it looks like that's what is happening, but Kotlin doesn't quite follow your code the same way that you do. While it is possible to see that when init
calls updateName()
, then fullName
will get a value, Kotlin just sees that there's no assignment to fullName
in init
and throws an error.
Assign an Uninitialized Property a Dummy Value
The easiest fix here is actually quite … easy. You can simply get around Kotlin's insistence on property initialization by assigning it an empty value at creation, such as an empty String
: “”
. Just add this to your class:
class Person(_firstName: String, _lastName: String, _height: Double, _age: Int, _hasPartner: Boolean) { var fullName: String = ''
Kotlin will now stop complaining! You've initialized fullName
, so there's not a problem. However, this is a bit hacky. It defeats the purpose of Kotlin's checking (something we'll come back to in a moment). It also builds a hidden dependency in your code. Look at the init
method again:
// Set the full name when creating an instance init { updateName() }
Even with the comment, there's nothing except your own memory and understanding of how Person
works—and this chapter—that tells you that you have to call updateName()
in init
. If you don't, then fullName
will not get initialized, and that, in turn, will mess up the toString()
function. Bad news!
If this seems unlikely, it's not. When you're writing code, you know what that code is supposed to do. But leave that code for a few weeks, or months, and you often forget how it worked, let alone why you wrote it that way! Worse, but just as likely, someone else comes back to it later and has no idea how it works.
In both cases, it would be quite plausible that someone well-meaning forgets that updateName()
must be called in init
, removes or moves it, and problems ensue. This is really a fragile solution.
Tell Kotlin You'll Initialize a Property Later
Another easy solution is to explicitly tell Kotlin that you are going to initialize a property later. It's sort of the equivalent of saying, “Look, Kotlin, trust me. I promise I'll take care of this.” To do this, you preface a property declaration with lateinit
, a keyword that means what it sounds like—you'll initialize the property in question later:
class