[Kotlin] 프로퍼티(Property)
👀학습목표
앞에서 배운 내용을 토대로는 프로퍼티는 어떤 클래스 인스턴스나 파일 퍼사드에 묶인 변수이며, 자바 필드와 비슷하다고 설명했다. 하지만 일반적으로 코틀린 프로퍼티는 일반 변수를 넘어서, 프로퍼티 값을 읽거나 쓰는 법을 제어할 수 있는 훨씬 더 다양한 기능을 제공한다.
이번 글에서는 단순하지 않은 프로퍼티의 의미에 대해서 다룰것이다.
👀학습하기 전에 알면 도움 되는 용어 정리
- 필드(field) : 클래스 멤버 변수
- 프로퍼티(Property) : 필드와 게터 세터를 한 번에 묶어서 부르는 단어
📕최상위 프로퍼티
클래스나 함수와 마찬가지로 최상위 수준에 프로퍼티를 정의할 수 있다.
이런 경우 프로퍼티는 전역 변수나 상수와 비슷한 역할을 한다.
import kotlin.*
val prefix="Hello ," //최상위 프로퍼티
fun main(){
val name=readLine()?:return //엘비스 연산자
println("$prefix $name")
}
이런 프로퍼티에 최상위 가시성(public/internal/private)을 지정할 수 있다. 그리고 임포트 디렉티브에서 최상위 프로퍼티를 임포트 할 수 있다.
📕늦은 초기화
클래스를 인스턴스 화할 때 프로퍼티를 초기화해야 한다는 요구 사항이 불필요하게 엄격할 때가 있다. 어떤 프로퍼티는 클래스 인스턴스가 생성된 뒤에, 그러나 해당 프로퍼티가 사용되는 시점보다는 이전에 초기화돼야 할 수 있다.
이런 경우 생성자에서는 초기화되지 않은 상태라는 사실을 의미라는 디폴트 값[ex) null]을 대입하고 실제 값을 필요할 때 대입할 수도 있다.
import java.io.File
class Content{
var text: String? = null
fun loadFile(file : File){
text=file.readText()
}
}
fun getContentSize(content : Content)= content.text?.length?: //널 가능성 체크
여기서 loadFile()은 다른 곳에서 호출되며 어떤 파일의 내용을 모두 문자열로 읽어온다고 가정하자.
이 예제의 단점은 실제 값이 항상 사용 전에 초기화되므로 절대 날이 될 수 없는 값이라는 사실을 알고 있음에도 불구하고 늘 널 가능성을 처리해야 한다는 점이다.
--> 코틀린은 이러한 불편한 점을 개선시키기 위해서 lateinit 키워드를 제공한다.
import java.io.File
class Content{
lateinit var text: String
fun loadFile(file : File){
text=file.readText()
}
}
fun getContentSize(content : Content)= content.text.length?:0
lateinit 표시가 붙은 프로퍼티는 값을 읽으려고 시도할 때 프로그램이 프로퍼티가 초기화됐는지 검사해서 초기화되지 않은 경우 UninitializePropertyAccessException을 던진다는 한 가지 차이점을 제외하면 일반 프로퍼티와 같다.
💡프로퍼티를 lateinit으로 만들기 위한 조건
- 프로퍼티가 코드에서 변경될 수 있는 지점이 여러 곳일 수 있으므로 프로퍼티를 가변 프로퍼티(var)로 정의해야 한다.
- 프로퍼티의 타입은 날이 아닌 타입어아야 하고 Int나 Boolean 같은 원시 값을 표현하는 타입이 아니어야 한다.
- 내부에서 lateinit 프로퍼티는 초기화되지 않은 상태를 표현하기 위해 null을 사용하는 널이 될 수 있는 값으로 표현되기 때문이다.
- lateinit 프로퍼티를 정의하면서 초기화 식을 지정해 값을 바로 대입할 수 없다.
- 이런 대입을 허용하면 lateinit을 지정하는 의미가 없기 때문이다.
- 최상의 프로퍼티와 지역변수에서 늦은 초기화(lateinit)를 사용할 수 있다.
📕커스텀 접근자
변수와 함수의 동작을 선언 안에 조합할 수 있는 기능을 하며 프로퍼티 값을 읽거나 쓸 때 호출되는 특별한 함수
get과 set을 내가 임의로 커스터마이징 해서 사용.
class Person(val firstName : String, val familyName : String){
val fullName: String
get(): String{
return "$firstName $familyName"
}
}
게터는 프로퍼티 정의 끝에 붙으며 기본적으로 이름 대신 get이라는 키워드가 붙은 함수처럼 보인다. 하지만 이런 프로퍼티를 읽으면 자동으로 게터를 호출한다.
함수와 비슷하게 접근자에도 식이 본문인 형태를 사용할 수 있다.
val fullName: String
get()="$firstName $familyName"
게터에는 파라미터가 없다. 반면 게터의 반환 타입은 프로퍼티의 타입과 같아야 한다.
class Person(val firstName : String, val familyName : String){
val fullName: Any
get(): String { //프로퍼티와 게터의 반환타입이 달라서 컴파일 오류가 발생한다.
"$firstName $familyName"
}
}
프로퍼티에 명시적으로 field를 사용하는 디폴트 접근자나 커스텀 접근자가 하나라도 있으면 (프로퍼티의 값이 저장되어 있는) 뒷받침하는 필드(backing field)가 생성된다.
불변 프로퍼티의 접근자는 읽기 접근자 하나뿐이므로 앞 예제에서 fullName은 직접 뒷받침하는 필드인 field를 참조하지 않는다는 사실을 쉽게 알 수 있다.
뒷받침하는 필드 참조는 field라는 키워드를 사용하며 접근자의 본문 안에서만 유용하다.
프로퍼티에 뒷받침하는 필드가 없다면 필드를 초기화할 수 없다.
--> 초기화는 기본적으로 클래스를 인스턴스 화할 때 값을 뒷받침하는 필드에 직접 대입하기 때문이다.
계산에 의해 값을 돌려주는 프로퍼티의 경우 뒷받침하는 필드가 필요하지 않다.
[위 코드에서 fullName]
💡var로 정의하는 가변 프로퍼티에서의 두 가지 접근자
- 게터(get) : 값을 읽어줌.
- 세터(set) : 값을 설정해줌.
fun main(){
val person=Person("Lee","Jaehan")
person.age=24
println(person.age)
}
class Person(val firstName : String, val familyName: String){
var age: Int?=null
set(value){
if(value !=null && value<=0){
throw java.lang.IllegalArgumentException("Invaild age : $value")
}
field=value
}
}
프로퍼티 세터의 파라미터는 단 하나이며, 프로퍼티 자체의 타입과 같아야 한다.
파라미터의 타입을 미리 알 수 있기 때문에 세터에서는 파라미터 타입을 생략한다.
🛑프로퍼티를 초기화하면 값을 바로 뒷받침하는 필드에 쓰기 때문에 프로퍼티 초기화는 세터를 호출하지 않는다.
프로퍼티 접근자에 별도로 가시성 변경자를 붙일 수도 있다.
class Person(name:String) {
var lastChange: Date? = null
private set //Person 클래스 밖에서는 변경할 수 없다.
var Name: String = name
set(value) {
lastChange = Date()
field = value
}
}
lateinit 프로퍼티의 경우 항상 자동으로 접근자가 생성되기 때문에 프로그래머가 직접 커스텀 접근자를 정의할 수 없다.
주생 성자 파라미터로 선언된 프로퍼티에 대한 접근자도 지원하지 않는다.
📕지연 계산 프로퍼티와 위임
💻lazy 프로퍼티
A라는 프로퍼티를 처음 읽을 때까지 그 값에 대한 계산을 미루고 싶을 때 사용
import java.io.*
val text by lazy{
File("data.txt").readText()
}
fun main(){
while(true){
when(val command=readLine() ?: return){
"print data"->println(text)
"exit"->return
}
}
}
- lazy 다음에 오는 블록 안에는 프로퍼티를 초기화하는 코드를 지정함.
- main() 함수에서 사용자가 적절한 명령으로 프로퍼티 값을 읽기 전까지, 프로그램은 lazy 프로퍼티의 값을 계산하지 않는다.
- 초기화가 된 이후 프로퍼티의 값은 필드에 저장되고, 그 이후로는 , 프로퍼티 값을 읽을 때마다 저장된 값을 읽게 된다.
정리
- 최상위 프로퍼티는 전역 변수나 상수와 비슷한 역할을 한다.
- 가시성을 지정할 수 있음.(Public/internal/private)
- 늦은 초기화(lateinit)
- 값을 읽으려고 시도할 때 프로그램이 프로퍼티가 초기화됐는지 검사한다.
- lateinit으로 프로퍼티를 만들기 위한 조건
- var로 정의되어야 함.
- 날이 아닌 타입 이어야 하고 Int나 Boolean 같은 원시 값을 표현하는 타입이 아니어야 함.
- 초기화 식을 지정해 값을 바로 대입할 수 없다.
- 커스텀 접근자
- 값을 읽거나 쓸 때 호출되는 특별한 함수
- val에는 값을 읽기 위한 게터(get)가 있다.
- var에는 값을 읽기 위한 게터(get)와 값을 설정하기 위한 세터(setter)라는 두 가지 접근자가 있다.
- 게터에는 파라미터가 없지만, 세터에는 단 하나의 파라미터를 가진다.
- 둘 다 반환 타입은 프로퍼티의 타입과 같아야 한다.