ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코틀린 기초 문법 ③
    KOTLIN 2022. 9. 28. 22:37

     

     

     

    참고한 강의와 책은 이전 블로그 글에 올려두었다.

     

    https://dodop-blog.tistory.com/391

     

    코틀린 기초 문법 ①

    요즘 코드가 Java -> Kotlin으로 넘어가고 있고, 사용이 많고 동기 분들을 보니 모두 코틀린 공부를 하고 계셔서 Kotlin 공부를 시작했다. 노션에 따로 적으면서 공부하긴 했지만, 블로그에도 함께 적

    dodop-blog.tistory.com

     

    책이 후반부로 가면서 점점 어려워지고 있어서 고민이다 🤦‍♀️

    inline함수 같은 경우 어떻게 컴파일이 되는지 이해가 되지 않아서 고민이었는데 동기 분이 알려주셔서 실제 컴파일 내용을 확인해 볼 수 있었다! 🙌

    제너릭스 같은 경우는 정리가 제대로 되지 않아 다시한번 읽고 내용을 추가할 예정이다! (완료)

     

     

     

     

     

     

     

     

     

     

    고차함수
    • 파라미터와 반환 값으로 람다를 사용
    • 함수를 마치 클래스에서 만들어낸 인스턴스 처럼 취급하여 파라미터로 넘기거나 반환값으로 받기도 함
    • 코틀린에서는 모든 함수를 고차 함수로 사용 가능
    • ::함수 의 형태로 고차함수 형태로 함수 넘기기가 가능
    • null
      • canReturnNull : 널이 될 수 있는 함수 타입
      • funReturnNull : 널이 될 수 있는 반환 타입을 갖는 함수 타입
    // null이 될 수 있는 함수 타입 변수를 정의하기 위해서는 함수 타입 전체가 널이 될 수 있는 타입임을 선언하기 위해 함수 타입을 괄호로 감싸고 그 뒤에 물음표를 붙임 
    var funOrNull: ((Int, Int) -> Int)? = null 
    

     

    fun rollDice(range: IntRange, time: Int, callback: (result: Int) -> Unit) {
        for (i in 0 until time) {
            val result = range.random()
            callback(result)
        }
    }
    
    fun main() {
        rollDice(1..6, 4, { result -> println(result) })
    // 6
    // 1
    // 5
    // 4
    }

     

     

     

    • 쓰레드를 3초간 sleep 하면 다음과 같은 결과도 확인할 수 있음
    fun rollDice(callback: (result: Int) -> Unit): String {
        thread {
            Thread.sleep(3000)
            callback(4)
        }
    
        return "Dice Rolled"
    }
    
    fun main() {
        val result = rollDice {
            println(it)
        }
        println(result)
    
    // Dice Rolled
    // 3초후 
    // 4
    }

     

     

     

     

     

     

     

    inline 함수

    • inline 키워드를 붙이면 컴파일러는 그 함수를 호출한 식을 모두 함수 본문으로 바꿈
    • 함수가 람다를 인자로 사용하는 경우 그 함수를 인라인함수로 만들면 람다 코드도 함께 인라이닝되어 무명 클래스와 객체가 생성되지 않아 성능이 개선될 수 있음
    • 자바에서는 코틀린의 인라인 함수를 다른 보통 함수처럼 호출 → 실제로 인라이닝 되지 않음
    • 함수의 파라미터 중에 함수 타입인 파라미터가 있고, 그 파라미터에 해당하는 인자(람다)를 함께 인라이닝함으로써 얻는 이익이 더 큰 경우에만 함수를 인라인 함수로 만들어야 함
    • 참고 예시 → https://sabarada.tistory.com/176
     

    [kotlin] 코틀린 차곡차곡 - 10. 인라인(inline) 함수와 reified 키워드

    안녕하세요. 오늘은 코틀린에서 인라인 함수와 reified 키워드에 대해서 알아보는 시간을 가져보도록 하겠습니다. inline function 인라인(inline) 키워든는 자바에서는 제공하지 않는 코틀린만의 키워

    sabarada.tistory.com

    // 1. inline 함수를 사용하지 않는 겨우 
    fun doSomethingElse(lambda: () -> Unit) {
        println("Doing something else")
        lambda()
    }
    
    fun doSomething() {
        println("Before lambda")
        doSomethingElse {
            println("Inside lambda")
        }
        println("After lambda")
    }
    
    // 자바 코드는 다음과 같이 실행이 된다 
    public static final void doSomething() {
        System.out.println("Before lambda");
        doSomethingElse(new Function() {
                public final void invoke() {
                System.out.println("Inside lambda");
            }
        });
        System.out.println("After lambda");
    }
    
    // 2. inline 함수를 사용하는 경우 
    inline fun doSomethingElse(lambda: () -> Unit) {
        println("Doing something else")
        lambda()
    }
    
    // 아래와 같이 실행됨을 확인할 수 있고 무의미한 Function 객체 생성을 막을 수 있다
    // 람다식의 로컬 변수가 사용되는 경우 객체에 변수가 추가되는 것을 절약할 수 있음 
    public static final void doSomething() {
        System.out.println("Before lambda");
        System.out.println("Doing something else");
        System.out.println("Inside lambda");
        System.out.println("After lambda");
    }
    

     

     

    • 그렇다면 inline 함수를 항상 쓰는 것이 좋은가?
      • 기본적으로 JVM의 JIT 컴파일러에 의해서 일반 함수들은 inline 함수를 사용했을 때 더 좋다고 생각되어지면 JVM이 자동으로 만들어주고 있으나 inline 함수를 사용하면 좋지 않거나 사용이 불가능한 경우(ex)public inline 함수는 private 함수를 호출할 수 없음)도 존재

     

     

     

     

     

    타입

    Any, Any?

    • 최상위 타입
    • Any
      • null이 될 수 없는 타입의 조상 타입
      • toString, equals, hashCode를 가진 상위 타입
      • object에 있는 wait, notify등은 사용할 수 없음

     

     

    Unit 타입

    • 코틀린의 void
    • void와 달리 모든 기능을 갖는 일반적인 타입으로 Unit 타입을 인자로 사용이 가능
    • 그냥 함수를 정의한다면 함수의 파라미터 목록 뒤에 오는 Unit 반환 타입 지정을 생략해도 되지만, 함수 타입을 선언할 때는 반환 타입을 반드시 명시

     

     

    Nothing 타입

    • 함수가 성공적으로 값을 돌려주지 않는 경우를 표현하기 위한 타입
    • 아무 값도 포함하지 않음
    • 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 사용 (그 외의 사용에서 nothing은 변수에 아무값도 저장할 수 없기 때문에 의미가 없음)
    • fail은 특별한 메세지가 들어있는 예외를 발생시켜 현재 테스트를 실패시키는 역할을 수행
    val address = company.address ?: fail("No address")
    println(address.city)
    

     

     

     

     

     

     

    제너릭스 ✨
    • 함수 클래스 선언시 고정적인 자료형 대신 실제 자료형으로 대체하는 타입 파라미터로 타입 파라미터를 받는 타입을 정의 할 때 사용됨
    • 제네릭 타입의 인스턴스를 만들기 위해서는 타입 파라미터를 구체적인 타입인자로 치환해야 함

     

     

    타입 파라미터 제약

    • 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능
    • 상한 : 제네릭 타입을 인스턴스화 할 때 사용하는 타입 인자는 반드시 그 상한 타입이거나 상한 타입의 하위 이어야 함
    • T 는 기본적으로 nullable
    // 타입 파라미터 뒤에 상한을 지정 
    fun <T : Number> List<T>.sum(): T
    
    // null 이 아닌 값만 할당 가능하도록 지정 
    fun <T : Any> List<T>.sum(): T
    

     

     

    소거된 타입 파라미터

    • 타입이 소거된 파라미터의 경우 (ex)listOf()) 안에 어떤 타입의 원소를 저장하는지 실행 시점에는 알 수 없음
    • 다만, 저장해야 하는 타입 정보의 크기가 줄어들어서 전반적인 메모리 사용량이 줄어든다는 장점
    • 스타 프로텍션 (*) : 인자를 알 수 없는 제네릭 타입을 표현할 때 사용

     

     

    실체화된 타입 파라미터

    • 코틀린 제네릭 타입의 타입 인자 정보는 실행 시점에 지워지지만 인라인 함수의 타입 파라미터는 실체화되므로 실행 시점에 인라인 함수의 타입 인자를 알 수 있음
    • 사용되는 경우
      • 타입 검사와 캐스팅(is, !is, as, as?)
      • 코틀린 리플렉션 API
      • 코틀린 타입에 대응하는 java.lang.Class를 얻기 (::class.java)
      • 다른 함수를 호출할 때 타입 인자로 사용
    • 할 수 없는 일
      • 타입 파라미터 클래스의 인스턴스 생성
      • 타입 파라미터 클래스의 동반 객체 메서드 호출
      • 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화 하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
      • 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정
    • 해당 함수를 인라인 함수로 만들고 타입 파라미터를 reified로 지정하면 타입이 T의 인스턴스인지 실행 시점에 알 수 있음 → 이 경우에는 함수를 inline으로 만드는 이유가 성능 향상이 아닌 실체화 한 타입 파라미터를 사용하기 위함으로 성능을 개선하려면 인라인 함수의 크기를 계속 관찰해야 함
    // isA 인라인 함수가 reified됨에 따라 value is T 파트가 컴파일 됨
    inline fun <reified T> isA(value: Any) = value is T
    
    • 인라인 함수에서만 실체화한 타입 인자를 쓸 수 있는 이유?
      • 컴파일러는 인라인 함수의 본문을 구현한 바이트 코드를 그 함수가 호출되는 모든 지점에 삽입하고 이로인해 컴파일러는 실체화한 타입 인자를 이용해 인라인 함수를 호출하는 각 부분의 정확한 타입 인자를 알 수 있음
      • 즉, 컴파일러는 타입 인자로 쓰인 구제적인 클래스를 참조하는 바이트 코드를 생성해 삽입 가능

     

     

    변성

    • 무공변(invariant) : 제네릭 타입을 인스턴스화 할 때 타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않는 제네릭 타입
    • 공변성 : 하위 타입 관계를 유지
    • 자바에서는 모든 클래스가 무공변
    • null이 될 수 없는 타입은 null이 될 수 있는 타입의 하위 타입
    // 클래스가 T에 대해 공변적임을 선언 
    // Producer<A>가 Product<B>의 하위타입이면 Producer는 공변적 
    interface Producer<out T> {
    		fun produce(): T
    }
    
    • 타입 파라미터 T에 붙은 out키워드
      • 공변성 : 하위 타입 관계가 유지
      • 사용 제한 : T를 아웃 위치에서만 사용이 가능
    • MutableList<T> 에서 T는 인자로 받기 때문에 타입 파라미터 T에 대한 공변적인 클래스 선언이 불가 (아웃 위치에서만 가능하다)
    • 생성자 파라미터는 인이나 아웃 어느쪽도 아니기 때문에 해당 타입을 선언에 사용이 가능
    open class Animal {
    		fun feed() { /* ... */ }
    }
    
    class Herd<T: Animal> {
    		val size: Int get() = ...
    		operator fun get(i: Int): T { ... }
    }
    
    fun feedAll(animals: Herd<Animal>) {
    		for (i in 0 until animals.size) {
    				animals[i].feed()
    		}
    }
    
    class Cat: Animal() {
    		fun cleanLitter() { ... }
    }
    
    fun takeCareOfCats(cats: Herd<Cat>) {
    		for ( i in 0 until cats.size) {
    				cats[i].cleanLitter()
    				// feedAll(cats) 타입 불일치 오류 발생 
    				// Herd 클래스의 T타입 파라미터에 대해 아무 변성도 저장하지 않았기 때문 
    				// 사용하기 위해선 타입 캐스팅을 명시적으로 해주어야 한다 
    		}
    }
    
    // 다음과 같이 Herd를 out으로 타입 변성을 저장해주면 캐스잍 없이 사용이 가능
    class Herd<out T: Animal> {
    		val size: Int get() = ...
    		operator fun get(i: Int): T { ... }
    }

     

     

     

     

     

     

    • 반공병성
      • 뒤집힌 하위 타입 관계 <in T>
      • Consumer 를 예로 들어 타입 B가 타입 A의 하위 타입인 경우 Consumer<A>가 Consumer<B>의 하위 타입인 관계가 성립하면 제네릭 클래스 Consumer<T>는 타입 인자 T에 대해 반공변

     

     

    공변성 반공변성 무공변성
    Producer<out T> Consumer<in T> MutableList<T>
    타입 인자의 하위 타입 관계가 제네릭 타입에서도 유지 타입 인자의 하위 타입 관계가 제네릭 타입에서 뒤집힘 하위 타입 관계가 성립하지 않음
    T를 아웃 위치에서만 사용 가능 T를 인 위치에서만 사용 가능 T를 아무 위치에서나 사용 불가

     

     

     

     

     

     

    지연 초기화 
    • lateinit 변경자 : null 이 될 수 없는 프로퍼티에 지정하면 프로퍼티를 생성자가 호출된 다음에 초기화
    • 요청이 들어오면 비로소 초기화
    • 위임 프로퍼티(delegated property)의 일종
    • 변수에 객체할당을 선언과 동시에 할 수 없는 경우 선언만하고 초기값 선언을 나중에 하기 위해 사용
    • 코드에서는 선언시 객체 생성 및 할당하지만 동작시 사용시점 까지 초기화를 미룸으로서 코드 실행 최적화가 가능(cf)람다함수)
    • 단, 초기값 할당 전까지 변수 사용은 금지
    • String 클래스를 제외한 기본 자료형에는 사용하지 않음
    class Foo {
    // 지연 초기화 
        lateinit var a: String
        fun start() {
    				// 초기화 여부 확인 
            a = if (::a.isInitialized) "is initialized" else "isn't initialized"
        }
    }

     

     

     

    위임 프로퍼티(by lazy())를 사용한 초기화 지연

    • lazy 함수는 기본적으로 스레드 안전
    • 필요에 따라 동기화에 사용할 락을 lazy 함수에 전달하거나 다중 스레드 환경에서 사용하지 않을 프로퍼티를 위해 lazy 함수가 동기화를 하지 못하게 막을 수도 있음
    • lazy 함수는 코틀린 관례에 맞는 시그니처의 getValue 메서드가 들어있는 객체를 반환
    class Person(val name: String) {
    	val email by lazy { loadEmails(this)
    }

     

     

     

     

    // 지연 초기화 전 
    class StudentHeavy {
        init {
            println("Student Heavy Initialized")
        }
    }
    
    class JStudent {
        val heavy = StudentHeavy()
    }
    
    
    fun main() {
        val student = JStudent()
    // init 구문이 실행됨 (로딩됨)
    // Student Heavy Initialized
    }
    
    // 지연 초기화 후 
    class JStudent {
    // 필요할 때만 로딩하겠다 
    // 현재 객체가 사용되지 않았으므로 로딩되지 않음 
        val heavy by lazy { StudentHeavy() }
    }
    
    
    // 다음과 같이 객체가 사용될 때가 되어서야 초기화 된다 
    fun main() {
        val student = JStudent()
        student.heavy
    // Student Heavy Initialized
    }

     

     

    • Delegates.observable을 이용해 프로퍼티 변경 통지를 구현할 수도 있음
    class StudentHeavy {
        init {
            println("Student Heavy Initialized")
        }
    }
    
    class JStudent {
        var marks: Int by Delegates.observable(50) { property, oldValue, newValue->
            println("Old Value $oldValue")
            println("New Value $newValue")
        }
    }
    
    
    fun main() {
        val student = JStudent()
        student.marks = 70
    // Old Value 50
    // New Value 70
        student.marks = 80
    // Old Value 70
    // New Value 80
    }

     

     

     

     

    • Delegates.vetoable 을 사용하면 조건을 만족할 때만 프로퍼티를 변경하도록 할 수 있음
    class JStudent {
        var age: Int by Delegates.vetoable(14) { property, oldValue, newValue ->
            println("New Age $newValue")
            println("Old Age $oldValue")
            newValue >= 14
        }
    }
    
    // 조건(새로운 나이가 14이상일 때)을 만족할 때가 되어서야 값이 변경되는 것을 확인할 수 있다 
    fun main() {
        val student = JStudent()
        student.age = 13
    // New Age 13
    // Old Age 14
        student.age = 12
    // New Age 12
    // Old Age 14
        student.age = 15
    // New Age 15
    // Old Age 14
    }

     

     

     

     

     

     

     

     

     

     

    비트 연산
    • 실제로 정수형의 값을 비트단위로 나누어 데이터를 좀 더 작은 단위로 담아 변수하나에 여러개의 값을 담아 경제성을 높이기 위해 사용됨
    • 비트 연산 값도 들어가므로 주로 플래그값(여러개의 상태값을 0과 1로 담음)처리 혹은 네트워크 등에서 프로토콜의 데이터양을 줄이기 위해 자주 사용됨
    • 코틀린에서는 모든 정수형이 부호를 포함하여 최상위 비트에는 부호 표시로 데이터를 담지 않음

     

    bitwise shift 연산자

    • shl : 부호비트를 제외하고 모든 비트를 좌측으로
    • shr : 부호 비트를 제외하고 모든 비트를 우측으로
    • ushr : 부호 비트를 포함하여 모든 비트를 우측으로

     

     

    bitwise 연산자

    • and : 비트 단위로 비교하여 둘다 1인 자리만 반환
      • 원하는 자리에 1을 넣어 비트를 확인하는 용도
      • 만들고 싶은 자리에 0을 넣어서 비트를 clear하는 용도
    • or : 둘중 하나라도 1인 자리는 1로 반환
      • 비트 값을 1로 설정하여 비트의 set연산
    • xor : 두 값이 같으면 0, 다르면 1로 반환
      • 비트들이 동일한지 알아내는 용도

     

     

    inv()

    • 비트를 반전 시킴

     

     

     

     

     

     

    애노테이션
    • 적용하려는 대상 앞에 애노테이션을 붙임
    • 프로퍼티를 애노테이션 인자로 사용하려면 그 앞에 const 변경자를 붙여야 함 → 일반 프로퍼티를 애노테이션 인자로 사용하려고 시도하면 “Only const val can be used in constant expressions” 오류가 발생
      • cf) const 가 붙은 프로퍼티는 파일의 맨 위나 object 안에 선언되어야 하며, 원시 타입이나 String으로 초기화 해야만 함
    • 사용 지점 대상 선언( @대상 : 어노테이션이름)으로 애노테이션을 붙일 요소를 정할 수 있음
    • 규칙을 지정하려면 공개 필드나 메서드에 @Rule을 붙여야 하는데, 코틀린의 필드는 기본적으로 비공개이기 때문에 @get : Rule과 같이 getter에 룰을 적용시키는 방식을 사용함
      • 프로퍼티에 적용하기 위해 property, field, get, set, receiver, param, setparam, delegate, file 과 같이 적용 대상을 선정할 수 있음
    • @Suppress : 컴파일러의 경고를 무시하기 위한 애노테이션

     

     

     

     

    애노테이션을 이용한 직렬화, 역직렬화

    • 직렬화 : 객체를 저장장치에 저장하거나 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환하는 것
      • 자주 쓰이는 형식 JSON : JSON에서는 객체의 타입이 저장되지 않기 때문에 JSON 데이터로부터 인스턴스를 만들려면 타입 인자로 클래스를 명시해야함
    • 역직렬화 : 텍스트나 이진 형식으로 저장된 데이터로부터 원래의 객체를 만들어내는 것
    data class Person(val name: String, val age: Int)
    
    
    fun main() {
    // 직렬화 가상 예시 
        println(serialize(person))
    // {"age": 29, "name": "Alice"}
    
    // 역직렬화 가상 예시 
    		println(deserialize<Person>(json))
    // Person(name=Alice, age=29)
    }

     

     

     

    • @JsonExclude : 직렬화나 역직렬화시 그 프로퍼티를 무시
    • @JsonName : 프로퍼티를 표현하는 키/값 쌍의 키로 프로퍼티 이름 대신 애노테이션이 저장한 이름을 쓰게할 수 있음
    data class Person(@JsonName("alias") val name: String, @JsonExclude val age: Int? = null)
    
    
    fun main() {
    // 직렬화 가상 예시 
        println(serialize(person))
    // {"alias": "Alice"}
    
    
    // 역직렬화 가상 예시 
    		println(deserialize<Person>(json))
    // Person("Alice")
    }

     

     

     

     

    메타애노테이션

    • 애노테이션 클래스에 붙일 수 있는 애노테이션
    • ex) @Target : 애노테이션을 적용할 수 있는 요소의 유형을 저장 (사용하지 않으면 모든 선언에 적용 가능)
    • cf) 자바에서의 @Retention 애노테이션 (애노테이션 클래스를 소스, class, 실행시점(리플렉션이용)에 접근할 수 있게할지 정함)은 기본적으로 class 파일에 저장하지만, 코틀린에서는 기본적으로 애노테이션의 @Retention을 RUNTIME으로 지정

     

     

     

    애노테이션 파라미터로 클래스 사용

    • ex) @DeserializeInterface : 인터페이스 타입인 프로퍼티에 대한 역직렬화를 제어 (인자로 대상:class를 넘긴다)
    • 만약 넘기는 대상이 KClass<Any>라면 오직 Any::class만 넘길 수 있지만, out 키워드를 이용하면 공변성에 의해 모든 코틀린 타입 T에 대해 KClass<T>가 KClass<out Any>의 하위 타입이 됨

     

     

    애노테이션 파라미터로 제네릭 클래스 받기

    • 제네릭 클래스를 인자로 받아야 하면 KClass<out 허용할 클래스 이름<*>> 처럼 허용할 클래스의 이름 뒤에 스타 프로젝션을 덧붙임

     

     

     

Designed by Tistory.