본문 바로가기

Kotlin

Kotlin - 속성 (Properties)

속성 선언 하기

Kotlin 클래스에서는 val, var 키워드로 속성을 선언 할 수 있다. val 는 read-only, var는 변경가능한 속성을 선언 할때 사용한다.

class Address {
    val name: String = "Holmes, Sherlock"
    var zip: String = "123456"
    var state: String? = null
}

속성을 사용하기 위해서는 속성 명을 사용한다.

fun copyAddress(address: Address): Address {
    val result = Address() // there's no 'new' keyword in Kotlin
    result.name = address.name // accessors are called
    // ...
    return result
}

 

 Getter, Setter

getter, setter 를 작성하는 기본 문법은 아래와 같다. 문법에서 보는 것 과 같이 타입, initializer, getter, setter 모두 옵션 항목이어서 생략이 가능하다.

val 로 선언한 속성에 대해서는 setter를 허용하지 않는다.

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]
//    초기 값에 세팅 되지 않으면 타입을 추측하지 못하기 때문에, 초기값을 세팅하던, 타입을 작성하던 해야 한다.
//    var initialized // 에러 발생

//    값이 할당되지 않았다면, 타입을 명시해야 하고, 초기화 블록을 통해 값을 할당하던, initializer를 통해 값을 할당해야 한다.
    var initialized2: String
    init {
        initialized2 = "abc"
    }


//    값이 할당되었다면, 타입을 추론할 수 있어서 타입을 생략할 수 있다.
    var initialized3 = "abc"

사용자 정의  getter, setter를 정의 할 수 있다. getter, setter 는 해당 속성에 접근할 때마다 실행된다.

val 로 선언한 속성의 경우 당연히도 setter를 가질 수 없다.

class GetterSetterDemo2(val width: Int, val height: Int) {
    // val 로 선언한 경우, setter를 생성 할 수 없다.
    val area: Int
        get() {
            println("getter called")
            return this.width * this.height
        }

    val area2 get() = this.width * this.height // get 에서 타입을 추론 할 수 있다면 속성의 타입을 생략 할 수 있다.

    var name: String = "John"
        set(value) {
            println("setter called")
//            아래 Backing fields 부분 참고            
            field = "Name: $value"
        }
}

fun main() {
    val getterSetterDemo2 = GetterSetterDemo2(3, 2)
    println("area1 : ${getterSetterDemo2.area}")
//    속성에 접근할때 마다 getter가 호출 된다
//    getter called
//    area1 : 6

    println("area2 : ${getterSetterDemo2.area2}")
//    area2 : 6

    getterSetterDemo2.name = "Doe"
//    속성에 접근할때 마다 setter가 호출 된다
//    setter called
    println(getterSetterDemo2.name)
//    Name: Doe
}

 

접근 제한자나 어노테이션을 붙이고 싶다면 아래 예 처럼 get, set 앞에 작성한다.

var setterVisibility: String = "abc"
    private set // the setter is private and has the default implementation

var setterWithAnnotation: Any? = null
    @Inject set // annotate the setter with Inject

Backing fields

Kotlin 에서는 field는 속성의 실제 값이 저장되는 곳을 나타내는 정의자가 이고, getter, setter에서 해당 속성의 값에 접근하고 싶을때 사용한다. 속성이름을 그대로 사용 했을때 무한으로 getter, setter가 호출될 수 있어서 (순환 오류) field 라는 정의자를 사용한다.

class BackingFieldDemo {
    var counter = 0
        get() {
             println("getter called counter: $field")
            return field
        }
        // 음수는 들어 올 수 없다.
        set(value) {
            if (value >= 0) {
                field = value // field 대신에 counter라는 필드명을 사용하면 setter가 무한 호출 된다.
            }
        }
}

fun main() {
    var backingFieldDemo = BackingFieldDemo()
    backingFieldDemo.counter = -1
    println(backingFieldDemo.counter)
//    getter called counter: 0
//    0
    backingFieldDemo.counter = 12
    println(backingFieldDemo.counter)
//    getter called counter: 12
//    12

}

Backing properties

backing field 대신에 속성값을 저장 해 놓는용도로 backing property를 별도로 선언해서 이용할 수도 있다.

private var _table: Map<String, Int>? = null // 실제 속성값이 저장되는 속성
public val table: Map<String, Int> // getter, setter를 정의한 속성, 실제 값은 _table 에 저장됨
    get() {
        if (_table == null) {
            _table = HashMap() // Type parameters are inferred
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

 

Compile-time constants

read-only 속성이 발견되면, const 수정 제한자를 사용하는 컴파일타임 상수로 표시한다. 이러기 위해서는 아래와 같은 요구사항을 만족 시켜야 한다.

  • Top-level 속성이거나 객체 정의(object declaration)나 동반객체(companion object)의 멤버여야 한다.
  • String이나 primitive 타입으로 초기화 돠어야 한다.
  • 사용자 정의 getter를 사용할 수 없다.

컴파일러는 상수를 인라인 하여 상수의 위치에 상수참조를 실제 값으로 변경 해 버린다. 하지만 필드(field)는 제거되지 않으므로 리플렉션을 사용하여 상호 작용 할 수 있다.

이러한 이유로 아래 예 처럼 속성을 어노테이션에 사용 할수 있다.

const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }

 

Late-initialized properties and variables

보통 속성들은 non-null 타입으로 선언되고, 생성자에서 초기화 된다. 하지만 의존성 주입을 통한 초기화나 단위테스트의 셋업 함수를 통한 초기화처럼 객체가 생성된 이후(생성자 호출 이후) 변수가 초기화될 필요가 있다. 

이런경우lateinit 수정자 (mopdifier) 를 이용한다. lateinit은 class 내부에서 var로 선언되어야 하고, non-nullable 타입이며, primitive 타입이어서는 안된다.

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // dereference directly
    }
}

lateinit 로 정의된 속성을 초기화 되기전에 사용하려고 한다면 관련된 예외가 발생 할 것이다. 이를 방지하기 위해 해당 프로퍼티의 참조객체에 있는 isInitialized 함수를 이용해서 해당 속성이 초가화 되었는지 확인한다.

if (foo::bar.isInitialized) { // 해당 속성이 초기화 되었는지 확인
    println(foo.bar)
}

 

반응형

'Kotlin' 카테고리의 다른 글

Kotlin - Functional (SAM) interfaces  (0) 2024.01.24
Kotlin - 인터페이스 (Interface)  (0) 2024.01.24
Kotlin - 상속 (Inheritance)  (0) 2024.01.18
Kotlin - Packages and imports  (0) 2024.01.13
Kotlin - Exceptions  (0) 2024.01.12