Property Accessors

To read a property, simply refer to its name. To assign a value to a mutable property, use the assignment operator =.

Here, we read and write the property i:

// PropertyAccessors/Data.kt package propertyaccessors import atomictest.eq class Data(var i: Int) fun main(args: Array<String>) { val data = Data(10) data.i eq 10 // Read the 'i' property data.i = 20 // Write to the 'i' property }

Although this looks like straightforward access to the piece of storage named i, the compiler is actually calling functions to perform the read and write operations. The default behavior of those functions is to simply read and write the data stored in i. In this atom you’ll learn to write your own property accessors which change the actions that occur during reading and writing.

The accessor used to get the value of a property is called a getter; you create your own getter by defining get(), directly after the property declaration. The accessor used to modify a mutable property is called a setter; you create your own setter by defining set(), directly after the property declaration.

The property accessors defined in the following example imitate the default implementations generated by the compiler, displaying additional information so you can see that the property accessors are indeed called during reads and writes. We indent the get() and set() functions to visually associate them with the property, but the actual association happens because they are defined directly after that property:

// PropertyAccessors/Default.kt class Default { var i: Int = 0 get() { println("get()") return field // [1] } set(value) { println("set($value)") field = value // [2] } } fun main(args: Array<String>) { val d = Default() d.i = 2 println(d.i) } /* Output: set(2) get() 2 */

The definition order for get() and set() is unimportant. You can define get() without defining set(), and vice-versa.

The default behavior for a property is to return its stored value from a getter and modify it with a setter—the actions of [1] and [2]. Inside the getter and setter, the stored value is manipulated indirectly using the field keyword, which is only accessible within these two functions.

Here’s an example that uses the default implementation of the getter, but adds a setter that traces changes to the property n:

// PropertyAccessors/LogChanges.kt import atomictest.eq class LogChanges { var n: Int = 0 set(value) { println("$field becomes $value") field = value } } fun main(args: Array<String>) { val lc = LogChanges() lc.n eq 0 lc.n = 2 lc.n eq 2 } /* Output: 0 0 becomes 2 2 */

If you declare a private property, both accessors become private. You can make the setter private and the getter public. Then you can read the property outside the class, but only change its value inside the class:

// PropertyAccessors/Counter.kt package propertyaccessors import atomictest.eq class Counter { var value: Int = 0 private set fun inc() = value++ } fun main(args: Array<String>) { val counter = Counter() repeat(10) { counter.inc() } counter.value eq 10 }

Using private set, we control the value property so it can only be incremented by one.

Most properties use the typical approach of storing the data in a field. You can also create a property that doesn’t have a field:

// PropertyAccessors/Hamsters.kt package propertyaccessors import atomictest.eq class Hamster(val name: String) class Cage(private val maxCapacity: Int) { private val hamsters = mutableListOf<Hamster>() val capacity: Int get() = maxCapacity - hamsters.size val full: Boolean get() = hamsters.size == maxCapacity fun put(hamster: Hamster): Boolean = if (full) false else { hamsters += hamster true } fun takeHamster(): Hamster = hamsters.removeAt(0) } fun main(args: Array<String>) { val cage = Cage(2) cage.full eq false cage.capacity eq 2 cage.put(Hamster("Alice")) eq true cage.put(Hamster("Bob")) eq true cage.full eq true cage.capacity eq 0 cage.put(Hamster("Charlie")) eq false cage.takeHamster() cage.capacity eq 1 }

The properties capacity and full contain no underlying state—they are computed at the time of each access. Both capacity and full are similar to functions, and you can alternatively define them as such:

// PropertyAccessors/Hamsters2.kt package propertyaccessors class Cage2(private val maxCapacity: Int) { private val hamsters = mutableListOf<Hamster>() fun getCapacity(): Int = maxCapacity - hamsters.size fun isFull(): Boolean = hamsters.size == maxCapacity }

In this case, creating them as properties improves readability, because capacity and fullness are properties of the cage. However, don’t just convert all your functions to properties—first, see how they read. The Kotlin style guide prefers properties over functions when the value is cheap to calculate and the property returns the same result between invocations if the object state hasn’t changed.

Notice that property accessors provide a kind of protection for properties. Many object-oriented languages rely on making a physical field private in order to control access to that property. With property accessors you can add code to control or modify that access, while still allowing anyone to use a property.

Previous          Next

©2018 Mindview LLC. All Rights Reserved.