-
4-3. 클래스와 객체 다루기 : 단순한 변수 이상의 프로퍼티Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 10. 1. 00:18
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) }
다음글
반응형'Study(종료) > Kotlin 22.09.13 ~ 12.18' 카테고리의 다른 글
5-1. 고급 함수와 함수형 프로그래밍 활용하기 : 코틀린을 활용한 함수형 프로그래밍 (0) 2022.10.08 4-4. 클래스와 객체 다루기 : 객체 (0) 2022.10.01 4-2. 클래스와 객체 다루기 : 널 가능성 (1) 2022.10.01 4-1. 클래스와 객체 다루기 : 클래스 정의하기 (2) 2022.09.29 3-4. 함수 정의하기 : 예외 처리 (2) 2022.09.26