본문 바로가기

Kotlin/이펙티브코틀린

3장 재사용성 : 23 ~ 25

Item 23. 타입 파라미터의 섀도잉을 피하라

섀도잉이란

// 섀도잉 예시
class Person(val name: String) {
  fun addName(name: String) {
    // something
  }
}

 

코틀린에서는 동일한 이름을 여러번 사용하는 것이 가능하다

addName에서 name은 생성자로 받은 name이 아닌 파라미터로 받은 name을 가리킨다

addName() 내부에서 생성자에 선언된 name을 사용하기 위해서는 this.name으로 명시하여 어느 범위의 name인지 알려줘야 한다

이처럼 같은 이름을 가진 요소에 의해 값이 가려지는 것을 섀도잉이라고 한다

섀도잉은 읽는 사람을 혼란스럽게 하기 때문에 피하는게 좋을 것 같다

 

혼란스러워지는 예시😱

interface Tree
class Birch: Tree
class Spruce: Tree

class Forest<T: Tree> {
  fun <T: Tree> addTree(tree: T) {
    // ...
  }
}

// 사용
val forest = Forest<Birch>()
forest.addTree(Birch())
forest.addTree(Spruce())

 

Forest 생성시 Birch 타입으로 생성했지만, addTree에서 제네릭으로 또 타입을 받을 수 있도록 선언했기 때문에

섀도잉이 일어나 forest.addTree()에 Spruce() 타입을 넣어도 문제 없는 코드가 된다

하지만 보통 위와 같은 상황을 의도하는 경우는 거의 없으며, 얼핏 코드를 봐선 둘이 독립적으로 동작한다는 것을 알아내기 어렵다

 

독립적인 타입 파라미터를 의도했다면 이름을 다르게 해주는 것이 좋다

class Forest<T: Tree> {
  fun <ST: T> addTree(tree: ST) {
    // ..
  }
}

 

그리고 보통의 경우는 따로 받지 않고 클래스 타입 파라미터를 사용하도록 한다

class Forest<T: Tree> {
  fun addTree(tree: T) {
    // ...
  }
}

// 사용
val forest = Forest<Birch>()
forest.addTree(Birch())
forest.addTree(Spruce()) // Error! type mismatch

 

Item 24. 제네릭 타입과 variance 한정자를 활용하라

변성 (variance) 은 기저타입이 같고, 타입 인자가 다른 경우 이들의 관계를 설명하는 개념이다

제네릭에서 변성 개념은 왜 필요할까?

 

var any: Any = 1
any = "String"

코틀린에서는 위처럼 any 라고 하는 Any 타입으로 선언된 프로퍼티에 어떤것이든 넣을 수 있다

Any는 모든 타입(String, Int 등등)의 상위타입이기 때문이다 (Liskov Substitution Principle, LSP)

 

class Box<T>

 

 

 

 

 

 

 

그렇다면 Box 라는 클래스가 존재할 때,

 

T가 Any 로 지정된 박스를 만든다면 이 박스 안에는 String, Int 등등 여러 타입을 담는 것이 가능할까?

실제로 만들어 본다면 그럴 수 없다는 것을 알게된다

 

fun main() {
  val anyBox: Box<Any> = Box<Int>() // 오류 : type mismatch
  val nothingBox: Box<Nothing> = Box<Int>() // 오류 : type mismatch

 

 

 

변성의 개념 없이 모든 타입을 넣을 수 있게끔 자유도가 높아진다면 안정성은 떨어지게 된다

그렇다고 한 타입만 받을 수 있다면 재사용성이 떨어진다

그래서 코틀린에서는 variance 한정자(in 또는 out)라고 하는 제약 조건을 제공하고 있으며,

기본적으로 클래스 타입의 파라미터 타입 T에 이런 한정자를 사용하지 않는다면 불공변성으로 본다

 

 

 

 

 

불공변성 (invariant)

  • 타입들이 서로 관련성이 없다
  • 같은 타입만 넣을 수 있다
class Box<T>

fun main() {
  val anyBox: Box<Any> = Box<Int>() // 오류 : type mismatch
  val nothingBox: Box<Nothing> = Box<Int>() // 오류 : type mismatch
}

 

공변성 (covariant)

  • out
  • A가 B의 하위타입일 때, Box<A> 는 Box<B>의 하위타입이다
  • 하위타입과 자신과 같은 타입을 받을 수 있다 (-> 상위타입은 허용하지 않는다)
class Box<out T>

fun main() {
  val numberBox: Box<Number> = Box<Int>()
  val IntBox: Box<Int> = Box<Number>() // 오류
}

 

반변성(convariant) : 공변성의 반대 개념

  • in
  • A가 B의 하위타입일 때, Box<B>는 Box<A>의 상위타입이다
  • 상위타입과 자신과 같은 타입을 받을 수 있다 (-> 하위타입은 허용하지 않는다)
class Box<in T>

fun main() {
  val numberBox: Box<Number> = Box<Int>() // 오류
  val IntBox: Box<Int> = Box<Number>() 
}

 

함수 타입의 모든 파라미터 타입은 반공변성, 리턴 타입은 공변성을 가진다

fun printProcessedNumber(transition: (Int) -> Any) {
  print(transition(42))
}

 

(Int) -> Any 타입의 함수는 아래 모든 경우에 대해 동작한다

  • (Int) -> Number
  • (Number) -> Any
  • (Number)  -> Number
  • (Number) -> Int

 

파라미터 타입은 반공변성 리턴타입은 공변성을 가지기 때문이다

(T_in) -> T_out

 

kotlin variance 한정자의 안정성

java와 비교했을 때 kotlin에서는 variance 한정자의 안정성을 어떻게 향상했는지 알아보자

 

1. 배열은 불공변성이다

 

2. public in 한정자 위치에 out 한정자(공변성을 가지는 파라미터)가 오는 것을 금지한다

 

3. public out 한정자 위치에 in 한정자(반공변성을 가지는 파라미터)가 오는 것을 금지한다

 

'Kotlin > 이펙티브코틀린' 카테고리의 다른 글

2장 가독성 : Item 14 - 16  (0) 2023.08.02
item 6~10  (0) 2023.07.19
1장 : item 1  (0) 2023.07.12