조용한 담장

코틀린(Kotlin) 클래스(Class) : 프로퍼티 와 필드 (Properties and Fields) 본문

kotlin

코틀린(Kotlin) 클래스(Class) : 프로퍼티 와 필드 (Properties and Fields)

iosroid 2019. 12. 31. 16:30

코틀린 클래스의 프로퍼티(property) 와 필드(field) 에 대해 살펴보자.

원문 https://kotlinlang.org/docs/reference/properties.html 을 보며 정리.

Declaring Properties

property 는 var 또는 var 로 선언된다.

class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
    var state: String? = null
    var zip: String = "123456"
}
fun copyAddress(address: Address): Address {
    val result = Address() // there's no 'new' keyword in Kotlin
    result.name = address.name // accessors are called
    result.street = address.street
    // ...
    return result
}

Getters and Setters

property 선언 문법:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

property_initializer 는 property 의 데이터를 초기화 하기 위한 것이다.

getter, setter 는 property 의 데이터 얻거나 변경하기 위해 사용되며 접근자(accessor) 라 한다.

initializer, getter, setter 는 생략이 가능하며, 암묵적으로 기본적인 기능이 구현된다.

initializer 로 부터 property 의 타입이 추론이 가능한 경우에는 type 을 생략할 수 있다.

var allByDefault: Int? // 명시적으로 `initializer` 가 필요한 경우, default getter and setter implied
var initialized = 1 // has type Int, default getter and setter

var initialized 의 타입은 initializer 값 1 로 부터 타입 Int 가 추론되어 지정된다.

property 에 접근하기 위해 사용되는 getter 는 기본 할당되는 코드 말고 직접 구현해서 쓸 수도 있다.

val isEmpty: Boolean
    get() = this.size == 0

setter 도 직접 구현할 수 있다.

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // parses the string and assigns values to other properties
    }

getter 를 통해 property 의 타입이 추론이 가능한 경우에도 type 선언을 생략할 수 있다.

val isEmpty get() = this.size == 0  // has type Boolean

isEmpty 의 데이터를 접근하면 항상 this.size == 0 의 값을 가지므로 boolean 타입으로 추론된다.

접근자(accessor)의 코드 변경없이 visibility 를 설정하거나 annotate 할 수 있다.

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 과 Java 의 클래스 내부 변수에 대한 차이점에 대한 내용이다. 문서의 설명을 먼저 보면...

kotlin 클래스 에서는 field 를 바로 선언할 수 없고 property 가 backing field 가 필요하면 알아서 제공한다. backing field 는 접근자 안에서 field 로 참조할 수 있다.

코드로 이해해 보자.

class A {
    var counter = 0 // Note: the initializer assigns the backing field directly
        set(value) {
            if (value >= 0) field = value
        }
}

fun main() {
    var a = A()
    a.counter = 1
    println(a.counter)
}

위 예제는 property counter 를 생성하고 initializer 로 초기값을 선언하고 접근자 set 를 구현하여 저장하는 코드를 작성했다.

a.counter = 1 에서 set 함수가 호출되어 값을 변경하게 될 것이다.

이를 자바로 변환해보면,

public final class A {
   private int counter;

   public final int getCounter() {
      return this.counter;
   }

   public final void setCounter(int value) {
      if (value >= 0) {
         this.counter = value;
      }

   }
}

public static final void main() {
    A a = new A();
    a.setCounter(1);
    int var1 = a.getCounter();
    boolean var2 = false;
    System.out.println(var1);
}

Java 코드에서는 class A 내에 counter field 가 생성되어 있고 두개의 접근자 함수가 따로 생성되었다.

즉, kotlin 의 property 의 값 저장이 Java 의 field 를 통해 이루어 지는 것이다.

하지만 kotlin 코드에서 이 field 를 직접 설정하는 것이 아니라 property 를 선언하면 아래의 상황에 따라서 자동으로 backing field 를 만들어 준다.

  • field 를 통해 custom 생성자에서 값을 참조하는 경우
  • 적어도 하나의 생성자가 기본 생성자를 사용하는 경우

위 상황에 해당이 안되는 경우, 생성자를 모두 따로 선언하고 field 를 사용하지 않는 다면 backing field 를 생성하지 않는다.

class A {
    var size = 0
    val isEmpty: Boolean
        get() = this.size == 0
}
public final class A {
   private int size;

   public final int getSize() {
      return this.size;
   }

   public final void setSize(int var1) {
      this.size = var1;
   }

   public final boolean isEmpty() {
      return this.size == 0;
   }
}

isEmpty property 는 실제로 메모리에 아무값도 저장을 하지 않는, 상태만 리턴하는 getter 만 존재하는 코드여서 field 가 생성되지 않는다.

참조 : What's Kotlin Backing Field For?

결론은 kotlin 이 알아서 하는 일이니 큰 신경쓸 필요가 없다...

Backing Properties

궂이 자동으로 backing field 를 만들어주는 것을 피해 무엇인가를 해보고자 한다면 backing property 를 만들 수도 있다.

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // Type parameters are inferred
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

Compile-Time Constants

compile time 에 값을 알 수 있는 property 는 const 를 써서 compile time constant 로 선언할 수 있다.

아래의 조건을 만족해야 한다.

 

annotation 에도 사용될 수 있다.

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

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

Late-Initialized Properties and Variables

보통의 property 는 constructor 에서 null 이 아닌 어떤 값으로 초기화 되어야 한다.
바로 초기화를 하지 않고 특정 조건이 만족된 후에 초기화를 하고싶은 경우에는 lateinit 를 사용할 수 있다.

public class MyTest {
    lateinit var subject: TestSubject

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

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

lateinit 은 클래스의 body 안에서 var property 에 사용될 수 있고, top-level properties 와 지역 변수들에 쓸 수 있다.

property 의 타입이나 변수는 null 이 될 수 없고 primitive type 이 아니어야 한다.

primary constructor 안에서는 사용할 수 없으며 custom 생성자를 쓰지 않는 경우에만 사용할 수 있다.

Checking whether a lateinit var is initialized (since 1.2)

lateinit var 의 초기화 여부를 확인하려면 property 의 레퍼런스의 .isInitialized 를 사용한다.

if (foo::bar.isInitialized) {
    println(foo.bar)
}

 

Comments