Property Accessors
To read a property, use its name. To assign a value to a mutable property, use the assignment operator
=.
This reads and writes the property i:
// PropertyAccessors/Data.kt
package propertyaccessors
import atomictest.eq
class Data(var i: Int)
fun main() {
val data = Data(10)
data.i eq 10 // Read the 'i' property
data.i = 20 // Write to the 'i' property
}
This appears to be straightforward access to the piece of storage named i.
However, Kotlin calls functions to perform the read and write operations. As
you expect, the default behavior of those functions reads and writes the data
stored in i. In this atom you’ll learn to write your own property accessors
to customize the reading and writing actions.
The accessor used to get the value of a property is called a getter. You
create a getter by defining get() immediately after the property definition.
The accessor used to modify a mutable property is called a setter. You create
a setter by defining set() immediately after the property definition.
The property accessors defined in the following example imitate the default
implementations generated by Kotlin. We display additional information so you
can see that the property accessors are indeed called during reads and writes.
We indent get() and set() to visually associate them with the property, but
the actual association happens because get() and set() are defined
immediately after that property (Kotlin doesn’t care about the indentation):
// PropertyAccessors/Default.kt
package propertyaccessors
import atomictest.*
class Default {
var i: Int = 0
get() {
trace("get()")
return field // [1]
}
set(value) {
trace("set($value)")
field = value // [2]
}
}
fun main() {
val d = Default()
d.i = 2
trace(d.i)
trace eq """
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 returns its stored value from a getter and
modifies 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.
This next example uses the default implementation of the getter and adds a
setter to trace changes to the property n:
// PropertyAccessors/LogChanges.kt
package propertyaccessors
import atomictest.*
class LogChanges {
var n: Int = 0
set(value) {
trace("$field becomes $value")
field = value
}
}
fun main() {
val lc = LogChanges()
lc.n eq 0
lc.n = 2
lc.n eq 2
trace eq "0 becomes 2"
}
If you define a property as private, both accessors become private. You can
also 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() {
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.
Normal properties store their 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 take(): Hamster =
hamsters.removeAt(0)
}
fun main() {
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.take()
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 define them as such:
// PropertyAccessors/Hamsters2.kt
package propertyaccessors
class Cage2(private val maxCapacity: Int) {
private val hamsters =
mutableListOf<Hamster>()
fun capacity(): Int =
maxCapacity - hamsters.size
fun isFull(): Boolean =
hamsters.size == maxCapacity
}
In this case, using 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 for each invocation as long as the object state hasn’t changed.
Property accessors provide a kind of protection for properties. Many
object-oriented languages rely on making a physical field private to control
access to that property. With property accessors you can add code to control or
modify that access, while allowing anyone to use a property.
Exercises and solutions can be found at www.AtomicKotlin.com.