선생님, 개발을 잘하고 싶어요.

Kotlin in Action - 2부 7장 - 연산자 오버로딩과 기타 관례 (convention) 본문

일상/책 리뷰

Kotlin in Action - 2부 7장 - 연산자 오버로딩과 기타 관례 (convention)

알고싶은 승민 2022. 5. 24. 22:38

2부의 목적

  • 코틀린으로 자신의 API를 만드는 법을 배운다.
  • 프로그램 안에서 상호작용하는 클래스가 둘 이상이라면 다른 클래스에게 API를 제공하는 클래스가 적어도 하나 이상이므로, 라이브러리 개발자가 아니더라도 중요한 내용이다.

다루는 것

  • 특정 함수 이름과 연관된 관례 (convention)
  • 위임 프로퍼티

관례

  • operator 키워드를 붙임으로써 어떤 함수가 관례를 따르는 함수임을 명확히 한다.
  • operator 없이 관례에서 사용하는 함수 이름을 쓴다면 컴파일러가 알려준다.
  • 멤버 함수 뿐 아니라 확장 함수로도 operator 연산자를 정의할 수 있다.

산술 연산자 오버로딩

  • 프로그래머가 직접 연산자를 만들어 사용할 수 없고 언어에서 미리 정해둔 연산자만 오버로딩할 수 있다.
  • 클래스에서 정의해야하는 이름이 연산자별로 정해져 있다.
  • 연산자 우선순위또한 표준 숫자 타입에 대한 연산자 우선순위와 같다.
  • 종류
    • 이항 산술 연산자 (a + b)
      • plus 함수는 새로운 객체를 반환한다
    • 복합 대입 연산자 (a += b)
      • plusAssign 함수는 기존 객체의 내부 상태를 변경시킨다.
    • 단항 연산자 (+a)
      • unaryPlus 함수는 파라미터가 없다.
    • plus 연산과 plusAssign을 동시에 정의하지 마라.
    • 변경이 불가능하다면 plus와 같이 새로운 값을 반환하는 연산만 추가하고, 변경 가능한 클래스를 설계한다면 plusAssign 과 같은 연산을 제공하자.

이항 산술 연산자
복합 대입 연산자
단항 연산자

비교 연산자 오버로딩

  • 동등성 연산자: equals (==)
    • Any에 정의된 equals는 항상 확장함수보다 우선순위가 높으니 확장함수로 동등성 연산자를 정의할 수 없다.
  • 순서 연산자: compareTo (>, <, >=, <=)

동등성 연산자
순서 연산자

인덱스로 원소에 접근: get, set

mutableMap[key] = newValue // 와 같은 코드가 동작한다.

operator fun Point.get(index: Int): Int {
  ..
}
  • 이렇게 각괄호([]) 안에 인자를 기준으로 원소를 받아오는 함수를 만들고 싶다면 get이라는 메서드를 만들고 operator 변경자를 붙이기만 하면 된다.
  • 파라미터가 Int가 아니여도 괜찮다.
  • 여러 파라미터를 사용하는 get을 정의할 수도 있다.
  • 다양한 파라미터 타입에 대해 오버로딩한 get을 제공할 수 도 있다.

get
set

in 관례 (contains)

  • 컬렉션이 지원하는 in은 contains에 해당한다.

in

rangeTo 관례 (..)

  • 범위를 만들려면 .. 구문을 사용해야한다.
  • .. 연산자는 rangeTo 함수를 간략하게 표현하는 방법이다.
  • 어떤 클래스가 Comparable 인터페이스를 구현하면 rangeTo를 정의할 필요가 없다.

.., rangeTo

for 루프를 위한 iterator 관례

for (x in list) { … }
  • 이런 문장은 list.iterator() 호출해서 이터레이터를 얻은 이후, hasNext, next 호출을 반복하는 식으로 변환된다.

구조 분해 선언과 component 함수

  • destructing declaration, 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화할 수 있다.
  • data 클래스는 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동적으로 componentN 함수를 만들어준다.
  • 배열과 컬렉션도 5개 요소 까지는 componentN 함수가 있다. 따라서 크기가 정해진 컬렉션을 다루는 경우 구조 분해가 유용하다.
  • 본문 내 선언 문 뿐 아니라 변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해 선언을 사용할 수 있다.
val map: Map<String, String>

for ((key, value) in map) { // 루프 변수에 구조 분해 선언 사용
  ...
}

// 이런 확장 함수 제공하기 때문에 가능
operator fun Map.Entry.component1()
operator fun Map.Entry.component2()

구조 분해 선언

프로퍼티 접근자 로직 재활용: 위임 프로퍼티 (delegated property)

  • 관례에 의존하는 특성 중 독특하면서 강력한 기능인 위임 프로퍼티
  • 자신의 값을 필드가 아니라 데이터베이스 테이블, 브라우저 세션, 맵 등에 저장하고 싶은가?
  • 위임: 도우미 객체에게 그 작업을 처리하게 맡기는 디자인 패턴
  • 위임 객체(delegate): 작업을 처리하는 도우미 객체
class Foo {
  var p: Type by Delegate()
}
  • p 프로퍼티는 접근자 로직을 다른 객체에 위임한다.
  • by 키워드 뒤에 식을 계산한 결과인 Delegate 객체에게 위임에 쓰일 객체를 얻는다.
  • p는 내부 필드를 유지하지 않고, 그 프로퍼티의 게터나 세터는 delegate의 getValue, setValue를 호출하게 됨
class Delegate(
  val propValue: Int
) {
  operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue
  operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
    ...
  }
}
  • 코틀린은 프로퍼티를 표현하는 객체로 KProperty 타입을 활용
  • 컴파일러가 대충 다음과 같은 일을 한다
    1. by 키워드 뒤의 식으로 초기화 되는 객체를 감춰진 프로퍼티에 저장 <delegate>
    2. 프로퍼티를 표현하기 위해 KProperty 타입의 객체 제공
    // 이 프로퍼티 위임 코드는
    class C {
      var prop: Type by MyDelegate()
    }
    
    // 다음과 같이 컴파일 된다.
    class C {
      private val <delegate> = MyDelegate()
      var prop: Type
        get() = <delegate>.getValue(this, <property>)
        set(value: Type) = <delegate>.setValue(this, <property>, value)
    }
    

프로퍼티 위임

Comments