4-3. 클래스와 객체 다루기 : 단순한 변수 이상의 프로퍼티
4장
4-3. 클래스와 객체 다루기 : 단순한 변수 이상의 프로퍼티
4-3. 단순한 변수 이상의 프로퍼티
1) 최상위 프로퍼티
클래스나 함수와 마찬가지로 최상위 수준에 프로퍼티를 정의할 수 있다.
이 경우 프로퍼티는 전역 변수나 상수와 비슷한 역할을 한다.
val prefix = "hello"
fun main() {
val name = readLine() ?: return
println("$prefix $name")
}
2) 늦은 초기화
클래스를 인스턴스화할 때 프로퍼티를 초기화해야 한다는 요구 사항이 불필요하게 엄격할 때가 있다.
어떤 프로퍼티는 클래스 인스턴스가 생성된 뒤이지만 해당 프로퍼티가 사용되는 시점보다는
이전에 초기화돼야 할 수도 있다.
예를 들면 단위 테스트를 준비하는 코드, 의존 관계 주입에 의해 대입되는 프로퍼티가 있다.
이 경우 생성자에서는 초기화되지 않은 상태를 의미하는 디폴트 값을 대입하고 실제 필요할 때 값을 대입할 수 있다.
import java.io.File
fun main() {
var text: String? = null
fun loadFile(file: File){
text = file.readText()
}
}
fun getContentSize(content: Content) = content.text?.length ?: 0
여기서 loadFile()은 다른 곳에서 호출되며 어떤 파일의 내용을 모두 문자열로 가져온다고 가정하자.
위 코드의 단점은 실제 값이 항상 사용 전에 초기화되므로 절대 널이 될수 없는 값이지만,
늘 널 가능성을 처리해야 한다는 점 이다.
코틀린은 이런 문제를 해결하기 위해 lateinit 키워드를 제공한다.
import java.io.File
fun main() {
lateinit var text: String
fun loadFile(file: File){
text = file.readText()
}
}
fun getContentSize(content: Content) = content.text.length()
lateinit 표시가 붙은 프로퍼티는 값을 읽으려고 시도할 때 프로그램이 프로퍼티가 초기화 여부를 검사해
초기화되지 않은 경우 UninitializedPropertyAccessException을 던진다는 차이를 제외하면 일반 프로퍼티와 같다.
프로퍼티를 lateinit으로 만들기 위해서는 만족해야 하는 조건이 이다.
1. 프로퍼티가 코드에서 변경될 수 있는 지점이 여러 곳일 수 있으므로 가변 프로퍼티(var)로 정의해야 한다.
2. 프로퍼티의 타입은 not-null 타입이어야 하고, Int나 Boolean 같은 원시 값을 표현하는 타입이 아니어야 한다.
3. lateinit 프로퍼티를 정의하면서 초기화 식을 지정해 바로 대입할 수 없다(이 경우 애초에 lateinit 사용 의미가 없으므로)
lateinit var text: String
fun readText(){
text = readLine()!!
}
fun main() {
readText()
println(text)
}
3) 커스텀 접근자 사용하기
커스텀 접근자(custom accessor)를 이용하면 변수와 함수의 동작을 한 선언 안에 조합할 수 있다.
커스텀 접근자는 프로퍼티 값을 읽거나 쓸 때 호출되는 특별한 함수다.
class Person(val firstName: String, val lastName: String){
val fullName: String
get(): String{
return "$firstName $lastName"
}
}
게터는 프로퍼티 정의(위 코드에서 fullName) 끝에 붙으며
기본적으로 이름 대신 get 이라는 키워드가 붙은 함수처럼 보인다.
하지만 이런 프로퍼티를 읽으면 프로그램이 자동으로 게터를 호출한다.
fun main() {
val person = Person("김", "자반")
println(person.fullName) //김 자반
}
식이 본문인 형태를 사용할 수 있으며
val fullName: String
get()= "$firstName $lastName"
프로퍼티 타입을 생략하고 타입 추론에 의존해 사용할 수 있다.
val fullName
get()= "$firstName $lastName"
게터에는 파라미터가 없다. 게터의 반환 타입은(만약 반환 타입을 지정한다면) 프로퍼티의 타입과 같아야 한다.
val fullName
//get():Int = "$firstName $lastName"
//error : Type mismatch Required:Int Found:String
get():String = "$firstName $lastName"
위 코드처럼 사용하는 프로퍼티는 fullName 프로퍼티를 읽을 때 마다 다시 계산된다.
이후에 뒷받침하는 필드(backing field)에 대한 내용이 있는데, 책 내용만 봐서는 이해가 안가서 공식 문서를 참고했다https://kotlinlang.org/docs/properties.html#backing-fields
코틀린에서 필드는 프로퍼티의 일부로, 메모리에 프로퍼티에 값을 유지시키는 용도로만 사용된다.
필드는 직접 선언될 수 없지만, 프로퍼티가 backing field가 필요한 경우 코틀린이 자동으로 제공한다.
이 backing field는 식별자 field를 이용해 참조할 수 있다
class Person(val firstName: String, val lastName: String, age: Int){
var counter = 0 // the initializer assigns the backing field directly
set(value) {
if (value >= 0)
field = value
// counter = value
// ERROR StackOverflow: Using actual name 'counter' would make setter recursive
}
}
fun main() {
val person = Person("김", "자반", 20)
person.counter = 20
}
위 코드에서 counter = value를 사용하면 무한 루프에 빠지고,
field = value를 사용하면 정상적으로 작동한다.
field 식별자는 프로퍼티의 접근자(accesser)로써만 사용될 수 있다.
만약 하나 이상의 접근자를 기본 구현하거나 field 식별자를 이용한
커스텀 접근자로 참조할 때 backing-field는 프로퍼티가 생성된다
예를 들어 밑에 코드는 backing-field가 없다.
val isEmpty: Boolean
get() = this.size == 0
var로 정의하는 가변 프로퍼티에는 값을 읽기 위한 getter, 값을 설정하기 위한 setter 두 가지 접근자가 있다.
class Person(val firstName: String, val lastName: String, age: Int){
var age: Int? = null
set(value){
if(value != null && value <= 0)
throw IllegalArgumentException("Invalid age: $value")
field = value
}
}
fun main() {
val person = Person("김", "자반", 20)
person.age = 20 // call custom setter
println(person.age) // call custom getter
}
위 코드는 자동적으로 backing-field를 생성한다.
프로퍼티 세터의 파라미터는 단 하나이며 타입은 프로퍼티 자체의 타입과 같아야 한다.
관습적으로 파라미터 이름을 value로 정하는 경우가 많지만, 다른 이름도 사용 가능하다.
프로퍼티를 초기화하면 값을 바로 backing-field에 쓰기 때문에 프로퍼티 초기화는 setter를 호출하지 않는다.
가변 프로퍼티에는 두 가지 접근자가 있으므로 두 접근자를 모두 커스텀화하고,
두 접근자가 모두 field 키워드를 통해 backing-field를 사용하지 않는 경우를 제외하면 항상 backing-field가 생긴다.
아래 코드는 backing-field가 생기지 않는다.
class Person(var firstName: String, var lastName: String, age: Int){
var fullName: String
get(): String = "$firstName $lastName"
set(value){
val names = value.split(" ")
if(names.size != 2)
throw IllegalArgumentException("invalid full name : $value")
firstName = names[0]
lastName = names[1]
}
}
프로퍼티 접근자에 가시성 변경자(visibility modifiers)를 붙일 수 있다.
프로퍼티가 포함된 클래스 외부에서는 프로퍼티의 값을 변경 못하게 해서
외부에서 볼 때 객체가 불변인 것 처럼 하고싶을 떄 이 방식을 사용할 수 있다.
단순한 접근자 구현(backing-field를 바로 돌려주는 getter와 field에 값을 바로 대입하는 setter)만 필요한 경우
그냥 get이나 set 키워드만 사용해 getter와 setter를 정의할 수 있다.
import java.util.Date
class Person(name: String){
var lastChanged: Date? = null
private set // Person 클래스 밖에서 변경 x
var name: String = name
set(value){
lastChanged = Date()
field = value
}
}
fun main() {
val person = Person("김자반")
println(person.lastChanged) // null
person.name = "유퀴즈"
println(person.lastChanged) // Fri Sep 30 23:39:42 KST 2022
// person.lastChanged = "10:11:12"
// Error : Cannot assign to 'lastChanged': the setter is private in 'Person'
}
4) 지연 계산 프로퍼티와 위임
어떤 프로퍼티를 처음 읽을 때까지 그 값에 대한 계산을 미뤄두고 싶을 때 lazy 프로퍼티를 이용하면 된다.
import java.io.File
val text by lazy {
File("data.txt").readText()
}
fun main() {
while(true){
when (val command = readLine() ?: return){
"print data" -> println(text)
"exit" -> return
}
}
}
위 코드에서 text 프로퍼티를 lazy로 정의했다. lazy 다음에 오는 블록 안에는 프로퍼티를 초기화 하는 코드를 지정한다.
main()함수에서 사용자가 적절한 명령으로 프로퍼티 값을 읽기 전 까지 프로그램은 lazy 프로퍼티의 값을 계산하지 않는다.
초기화가 된 이후 프로퍼티의 값은 필드에 저장되고, 그 이후에는 값을 읽을 때 마다 저장된 값을 읽게 된다.
일반적인 경우 프로그램이 시작될 떄 바로 파일을 읽는다.
val text = File("data.txt").readText()
앞에서 배운 getter를 사용하면, 프로퍼티 값을 읽을 때 마다 파일을 매번 다시 읽어온다.
val text get() = File("data.txt").readText()
필요하면 프로퍼티 타입을 명시할 수 있다.
val text: String by lazy { File("data.txt").readText() }
lateinit 프로퍼티와 달리 lazy 프로퍼티는 가변 프로퍼티가 아니다.
lazy 프로퍼티는 초기화된 다음에는 변경되지 않는다.
var text1 by lazy {"hello"}
// error: Type 'Lazy<String>' has no method
// 'setValue(Nothing?, KProperty<*>, String)' and
// thus it cannot serve as a delegate for var (read-write property)
val text2 by lazy {"hello"} // OK
lazy 프로퍼티는 스레드 안전(thread-safe)하다.
다중 스레드 환경에서도 값을 한 스레드 안에서만 계산하기 때문에 lazy 프로퍼티에 접근하려는
모든 스레드는 궁극적으로 같은 값을 얻게 된다.
함수 본문에서도 지연 변수를 정의할 수 있다.
fun getName() = readLine()!!
fun main() {
val name by lazy { getName() }
println(name)
}
다음글