Programming Kotlin Applications. Бретт Мак-Лахлин

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.

      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.

      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" }

      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


Скачать книгу