코틀린 고급

짱구·2023년 3월 23일
0

확장 함수

코틀린은 클래스를 상속하거나 데코레이터 패턴과 같은 디자인 패턴을 사용하지 않고도 클래스를 확장할 수 있는 기능을 제공합니다.
예를 들어 일반적으로 수정할 수 없는 코틀린의 표준 라이브러리에 기능을 추가하기 위해 확장을 사용할 수 있습니다.

MyStringExtensions.kt
fun String.first(): Char {
	return this[0]
}

fun String.addFirst(char: Char): String {
	return char + this.substring(0)
}

fun main() {
    println("ABCD".first()) // 출력 : A
    println("ABCD".addFirst('Z')) // 출력 : ZABCD
}

확장 함수 내부의 this는 확장의 대상이 되는 객채의 참조입니다. (이런한 것을 receiver 혹은 수신자 객체라고 부릅니다.)

확장 함수를 자바로 변환

자바로 변환을 했을 때 확장 함수는 실제론 대상 객체를 수정하지 않습니다.
자바 내부적으로 static 메서드를 만듭니다.
첫번째 인자로 확장 대상 객체를 사용합니다.

import org.jetbrains.annotations.NotNull;
public final class Java_MyStringExtensionsKt {
  public static final char first(@NotNull String $this) {
  	return $this.charAt(0);
  }
}

메서드가 중복될 경우

확장하려는 클래스에 동일한 명칭의 함수가 존재할 경우 클래스 내부의 함수가 우선이 됩니다.

class MyExample {
    fun printMessage() = println("메인 클래스 출력")
}

fun MyExample.printMessage() = println("메서드 출력")

fun String.lowercase(b : String): String {
    return b.uppercase()
}

fun main() {
    MyExample().printMessage() // 메인 클래스 출력
    
    println(a.lowercase()); // abcd 출력
    
}

널 가능성이 있는 클래스에 대한 확장

null인 경우 내부에서 this == null 과 같은 형태로 null 검사를 수행할 수 있습니다.
이렇게 처리하면 안전연산자 '?'로 체크 할 필요없이 호출할 수 있습니다.


// 메서드에 안전연산자를 붙힘
fun MyExample?.printNullOrNotNull() {
    if (this == null) println("널인 경우에만 출력")
    else println("널이 아닌 경우에만 출력")
}

fun main() {
    var myExample: MyExample? = null
    
     // null이 확정적으로 들어있어도 실행
    myExample?.printNullOrNotNull()
    
     // 원래는 안전 연산자가 없고 null이 확정적으로 들어있으면 오류
    myExample.printNullOrNotNull() // 하지만 메서드 내에서 null check를 했기에 정상 실행
	// 출력 : 널인 경우에만 출력
    
    myExample = MyExample()
    myExample.printNullOrNotNull()
    // 출력 : 널이 아닌 경우에만 출력
}

제네릭

코틀린의 클래스는 자바와 마찬가지로 타입 파라미터 를 가질 수 있습니다.

class MyGenerics<T>(val t: T)

제네릭을 사용한 클래스의 인스턴스를 만들려면 타입 아규먼트를 제공하면 됩니다.

fun main() {
	val generics = MyGenerics<String>("테스트")
}

또한 인자를 통해 타입 추론이 가능한 경우 타입 아규먼트를 생략할 수 있습니다.

fun main() {
	val generics = MyGenerics("테스트")
}

타입 추론이 가능하기 때문에 변수의 타입에 타입 아규먼트를 추가해도되고 그렇지 않은 경우 타입 아규먼트를 생성자에서 추가해도 됩니다.

val list1: MutableList<String> = mutableListOf()
val list2 = mutableListOf<String>()

어떤 타입이 들어올지 알수 없지만 안전하게 사용하고 싶은 경우 스타 프로젝션 구문을 제공합니다

val list: List<*> = listOf<String>("테스트")

변성

제네렉에서 파라미터화된 타입이 서로 어떤 관계에 있는 지 설명하는 개념입니다.
변성은 크게 공변성, 반공변성 그리고 무공변성으로 나뉩니다.
이펙티브 자바에선 공변성과 반공변성을 설명할때 PECS 규칙을 언급합니다.
PECS는 Producer-Extends, Consumer-Super의 약자입니다.

  • 공변성은 자바 제네릭의 extends 코틀린에선 out
  • 반공변성은 자바 제네릭의 super 코틀린에선 in

공변성

공변성을 쓰지 않아 문제가 되는 케이스
※ CharSequence는 String의 부모 클래스입니다.

class MyGenerics<T>(val t: T)

fun main() {
  val generics = MyGenerics<String>("테스트")
  val charGenerics : MyGenerics<CharSequence> = generics // 컴파일 오류
}

String이 아닌 타입을 넣지 못하게 막으므로 의미있는 컴파일 오류지만 특정 상황에선 공변성이 필요할 수 있습니다.
공변성이 필요한 상황에서는 공변성 키워드인 out을 사용해서 해결할 수 있습니다.

class MyGenerics<out T>(val t: T)

fun main() {
  val generics = MyGenerics<String>("테스트")
  val anygenerics : MyGenerics<CharSequence> = generics
}

out을 사용하면 MyGenerics<CharSequence>가 MyGenerics<String>의 상위 타입이므로 공변성이 적용됩니다.

반공변성

반공변성을 쓰지 않아 문제가 되는 케이스

class Bag<T> {
    fun saveAll(
            to: MutableList<T>,
            from: MutableList<T>,
    ) {
        to.addAll(from)
    }
}
  
fun main() {
    val bag = Bag<String>()
  
	// in이 없다면 컴파일 오류
    bag.saveAll(mutableListOf<CharSequence>(""), mutableListOf<String>(""))
}

이때 반공변성 키워드인 in을 사용해 상위 타입도 전달 받을 수 있도록 합니다.

class Bag<T> {
    fun saveAll(
            to: MutableList<in T>,
            from: MutableList<T>,
    ) {
        to.addAll(from)
    }
}
fun main() {
    val bag = Bag<String>()
    bag.saveAll(mutableListOf<CharSequence>(""), mutableListOf<String>(""))
}

공변성과는 반대로 Bag<String>가 Bag<CharSequence>의 상위 타입이 되므로 반공변성입니다.

무공변성

in, out 어떤 것도 지정하지 않은 경우에는 무공변성이며 String은 CharSequence의 하위 타입이고 MyGenerics<String>와 MyGenerics<CharSequence>는 아무 관계도 아니고 아무런 영향을 주지 않는 상황을 말합니다.

출처 : fastcampus

profile
코드를 거의 아트의 경지로 끌어올려서 내가 코드고 코드가 나인 물아일체의 경지

0개의 댓글