Programming Kotlin Applications. Бретт Мак-Лахлин
Читать онлайн книгу._lastName: String, _height: Double, _age: Int, _hasPartner: Boolean) { lateinit var fullName: String
Remove the assignment to “”
and add lateinit
. Now, your class will compile again! No problem.
WARNING lateinit
has some pretty specific limitations. It only works with var
, not val
, and the properties can't be declared within the constructor itself. You also can't create a custom accessor or mutator for a lateinit
-declared property. So it's not a fix-all. In fact, it's not even a fix in this case, really.
While it might seem this solves your problem, that's not actually true. You still have the same problem as assigning an empty string to fullName
: you've built a silent dependence on updateName()
being called in init
. Kotlin is no longer checking for you, because you said, “I'll take care of it.”
So this is still a fragile solution, and in some cases, even more dangerous than just using a dummy value to start with.
Assign Your Property the Return Value from a Function
It's worth taking a minute to remember what the actual problem here is. You need to ensure that Kotlin sees an assignment of a value to fullName
in one of three legal places:
In the class's primary constructor
In the property's declaration
In the class's init block
You've also seen that simply calling another function—like updateName()
—and letting that function do the assignment won't cut it.
The first thing we need to do is change our function. Why? Because it currently doesn't return a value—and that means that you can't use the function as the right side of an assignment. In other words, you can't get the all-important:
var fullName: String = // something here... like a function!
So change updateName()
to actually return a value. While you're at it, let's also change the name to better reflect what it does now:
fun combinedNames(): String { return "$firstName $lastName" }
Now, you need a little cleanup:
1 Remove the reference to updateName() from init.
2 Assign fullName the return value from this function.
3 While you're at it, remove the init block entirely.
4 Move the declaration of the fullName property from the first line in Person to after all the other properties are declared.
WARNING Don't miss that last item! Now that you're using the values of firstName
and lastName
to initialize fullName
, the order of property declaration matters. You need firstName
and lastName
to get assigned the values from the primary constructor before fullName
is declared and combinedNames()
is run.
You can see this all together in Listing 2.9.
Listing 2.9 Getting the fullName property correct (partially)
package org.wiley.kotlin.person class Person(_firstName: String, _lastName: String, _height: Double, _age: Int, _hasPartner: Boolean) { 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 var fullName: String = combinedNames() fun combinedNames(): String { return "$firstName $lastName" } override fun toString(): String { return fullName } }
Sometimes You Don't Need a Property!
Unbelievably, after all this work, the output from your code is still incorrect if a last name is changed after object instance creation. One simple fix here is to update the two custom mutators, for firstName
and lastName
, to update fullName
, like this:
var firstName: String = _firstName set(value) { field = value // Update the full name fullName = combinedNames() } var lastName: String = _lastName set(value) { field = value // Update the full name fullName = combinedNames() }
Now, finally, things will work! The rather unexciting output after all this work should look like this:
Brian Truesby Rose Bushnell Rose Bushnell-Truesby
But before you pop the champagne, take a minute to think this through. Is this really a good solution? You have several problems here, all of which are nontrivial. See if these make sense; by now, they all should:
You've got the same piece of code—the assignment to fullName—in both the firstName and lastName mutator. In general, anytime you see the same code twice, that's not great. Try to avoid repeating code, as it becomes error-prone.
You're going to a lot of work to set the value of a property that has no special behavior. It's just the combination of the first and last names of the instance!
You already have a function that gives you the full name; it's called combinedNames().
A much better approach, believe it or not, is to just remove fullName
altogether. In fact, there's a lot that can be removed here.
Let's use combinedNames()
to handle the combination of firstName
and lastName
. Then you can remove the declaration of the fullName
property altogether. But if you do that, then you can actually remove the custom mutator for firstName
and lastName
, too!
And go one further; rename combinedNames()
to fullName()
, and then update toString()
to just use that. After all that, you should have something like Listing 2.10, which is pretty minimal!
LISTING 2.10: A much simpler version of Person
package org.wiley.kotlin.person class Person(_firstName: String, _lastName: String, _height: Double, _age: Int, _hasPartner: Boolean) { var firstName: String = _firstName var lastName: String = _lastName var height: Double = _height var age: Int = _age var hasPartner: Boolean = _hasPartner fun fullName(): String { return "$firstName $lastName" } override fun toString(): String { return fullName() } }
Compile and then run PersonApp
and you should get correct results—both before and after updating the last name for the rose
instance.
TYPE SAFETY CHANGES EVERYTHING
The big takeaway here is that when a language seeks to really enforce type safety—as Kotlin does—then nearly everything in the language gets affected. Properties are required to have values, and have them early, so that Kotlin can ensure the correct types are used. The mutability