ABOUT ME

작은 디테일에 집착하는 개발자

Today
-
Yesterday
-
Total
-
  • [Kotlin] 코틀린하며 가끔 잊어버리는 내용 (Feat. 개념적인 내용)
    IT Study/Android 2024. 4. 14. 17:07
    728x90

    출처 : https://kotlinlang.org/

     

    1. 다중 패러다임, 코틀린

    코틀린은 함수형 프로그래밍과 객체 지향 프로그래밍을 지원하는 다중 패러다임 언어.

    함수형 언어
    1. 일급 함수 (변수에 함수를 할당할 수 있다.)
    2. 불변성 (val과 같은 키워드가 존재한다.)
    3. 고차 함수 (filter, map 등)

    객체 지향 언어
    1. 클래스와 객체 (데이터 + 메서드가 결합된 클래스를 정의한다. 이를 인스턴스화하여 객체 생성할 수 있다.)
    2. 상속 (재사용 가능)
    3. 캡슐화 (접근 제어자 등을 통해 숨기고 공유하기가 가능)
    4. 다형성 (오버로딩, 오버라이딩)

     

     

    2. 변수, 자료형

    변수는 되도록 val, not mutable과 같이 불변성을 가져가는 것이 좋다. 그러나 변수에 값이 할당되지 않음을 하나의 정보로 사용하는 경우가 있을텐데, 이때에는 자료형 뒤에 ?를 붙여 nullable 하게 사용할 수 있다.

    ex. var name: String? = null

     

     

    3. 진수의 사용

    코틀린에서는 10진수와 함께 16진수(0x), 2진수(0b)를 사용할 수 있다. (8진수는 불가하다.)

     

     

    4. 형 변환

    코틀린은 명시적 형변환을 지원한다. (암시적인 형변환 지원하지 않는다.)

    ex. to자료형 (toString, toInt 등)

     

     

    5. 타입 추론 = 자료형 추론

    " " → String, ' ' → Char

    ' '(작은 따옴표)는 한 번도 사용해보지 않은 듯
    작은 따옴표 시, 체크해보자.

     

     

    6. 단일 표현식 함수

    단일 표현식 함수에서는 반환 타입을 생략할 수 있다.

    ex. fun add(a: Int, b: Int) = a + b

     

     

    7. 연산자

    $$(AND), ||(OR)와 같이 다른 언어에서도 볼 수 있는 연산자말고 코틀린에서만 볼 수 있는 연산자는 무엇이 있을까. 바로 is, !is
    is
    는 자료형이 맞는지 확인하는 연산자, !is는 자료형이 아닌지 확인하는 연산자이다.

    ex. 변수 is 자료형 (a is Int, 변수 a가 해당 자료형 Int가 맞는지 확인. 동시에 명확하게 해당 변수 a를 해당 자료형 Int로 형 변환)

     

     

    8. for 문

    for 문 내의 변수에 val과 같은 키워드를 붙이지 않아도 된다. step or downTo를 통해 ±1 외 ±n도 사용할 수 있다.
    for 문 내에 문자도 사용 가능하다.

    ex. for (i in 'a'..'e') print(i) (결과는 abcde 출력)

     

     

    9. break, continue

    중첩 for문 사용 시, 다른 언어에서는 내부 for문에서도 break or continue를 사용하고, 외부 for문에서도 break or continue를 사용해야 한다. 그러나 코틀린은 레이블 이름과 @를 사용하여 하나의 break 문으로 중첩 for문을 종료할 수 있다.

    ex. 레이블명@for ... break@레이블명

    loop@ for (i in 1..10) {
        for (j in 1..10) {
            println("i = $i, j = $j")
            if (i == 1 && j == 2) break@loop
        }
    }

     

     

    10. 클래스

    클래스란, 인스턴스(서로 다른 속성의 객체)를 만들어내는 툴이다.

     

     

    11. 생성자

    class Person (val name: string, val birthYear: Int)

    위 코드에서 (...) 괄호 내에 들어있는 속성의 선언이 바로 생성자 선언이다. 속성 선언과 동시에 생성자를 선언하는 방법이다.

    이때, 인스턴스 속성 초기화와 동시에 특정 구문을 수행하게 할 수도 있는데 (optional) 이는 init 함수를 사용하여 수행할 수 있다.

     

     

    12. init 함수

    인스턴스 생성과 동시에 (바로, 혹은 직후) 호출되는 함수

    class Person (val name: string, val birthYear: Int) {
        init {
            println("${this.birthYear}년생 ${this.name}님이 생성되었습니다.")
        }
    }

     

     

    13. 보조 생성자

    기본 생성자와 다른 형태의 생성자를 제공하여, 인스턴스 생성 시 다양한 형태의 인스턴스를 생성할 수 있게 돕는다. 편의 관점에서도.
    그러나 주의할 점은 기본 생성자를 통해 속성을 초기화해줘야 한다.

    class Person (val name: string, val birthYear: Int) {
        init {
            println("${this.birthYear}년생 ${this.name}님이 생성되었습니다.")
        }
        
        constructor(name: String): this(name, 1999) {
            println("보조 생성자로 생성되었습니다.")
        }
    }
    그렇다면 순서가 중요하다. 보조 생성자로 인스턴스를 생성할 경우 순서는?
    1. 인스턴스 생성
    2. init 실행
    3. constructor(보조 생성자 함수) 내부 구문 실행 

     

     

    14. 상속이 필요한 이유

    1. 이미 존재하는 클래스를 확장해, 새로운 속성 혹은 함수를 추가한 클래스를 만들어야 할 경우
    2. 여러 개 클래스를 만들었는데, 클래스들의 공통점을 뽑아 코드 관리를 편하게 하고 싶을 경우

     

     

    15. open

    1. open class 클래스명 : 상속할 수 있는 수퍼 클래스를 선언할 경우
    2. open fun 함수명 : 서브 클래스에서 오버라이드(수정 및 재사용)할 수 있는 함수를 선언할 경우

     

     

    16. 수퍼 클래스와 서브 클래스

    1. 서브 클래스는 수퍼 클래스 내 속성과 같은 이름의 속성을 가질 수 없다. (+ 함수도)
    2. 서브 클래스 생성 시, 수퍼 클래스의 생성자까지 호출된다.
    fun main() {
        val a = Animal("별", 5, "개")
        val b = Dog("별", 5)
        
        b.bark()
    }
    
    open class Animal(var name: String, var age: Int, var type: String) {
        fun introduce() {
            println("이름은 ${name}, 나이는 ${age} 그리고 ${type} 입니다.")
        }
    }
    
    class Dog(name: String, age: Int): Animal(name, age, "개") { // 2. 서브 클래스 생성 시, 수퍼 클래스의 생성자까지 호출된다.
        fun bark() {
            println("명멍")
        }
    }​

     

     

    17. 추상화 (추상 함수과 추상 클래스)

    수퍼 클래스에서
    1. 함수의 구체적인 구현은 없지만 (= 비어있지만)
    2. 모든 서브 클래스에서 이 함수가 반드시 있어야 한다는 것을 명시하는 (= 비어있는 함수의 내용을 구현하도록 해야 할 경우)
    특징
    1. 추상 클래스는 직접 인스턴스를 만들 수 없다.
    2. 추상 클래스 내 추상 함수는 서브 클래스에서 반드시 오버라이드 되어야 한다.
    fun main() {
        val r = Rabbit("토깽이")
        r.eat()
        r.sniff()
    }
    
    abstract class Animal(val name: String) {
        // 추상 클래스 내 추상 함수
        abstract fun eat()
        
        // 추상 클래스 내 일반 함수
        fun sniff() {
            println("킁킁")
        }
    }
    
    class Rabbit(name: String) : Animal(name) {
        override fun eat() {
            println("${name}이(가) 당근을 먹는다.")
        }
    }

     

    추상화는 인터페이스를 통해서도 할 수 있다.

     

     

    18. 인터페이스

    추상화 클래스와 인터페이스의 차이는?
    1. 추상 클래스는 생성자를 가질 수 있지만, 인터페이스는 생성자를 가질 수 없다.
    2. 인터페이스에서 구현 내용이 있으면 open, 구현 내용이 없으면 abstract 함수로 간주하기 때문에 키워드 사용이 필요없다.
    3. 인터페이스는 여러 개를 상속 받을 수 있다.
    fun main() {
        val r = Rabbit("토깽이")
    
        r.sniff()
        r.eat()
    
        r.hasLegs()
        r.walk()
    }
    
    abstract class Animal(val name: String) {
        // 추상 클래스 내 추상 함수
        abstract fun eat()
        
        // 추상 클래스 내 일반 함수
        fun sniff() {
            println("킁킁")
        }
    }
    
    interface Walker {
        // 인터페이스 내 추상 함수
        fun hasLegs()
        
        // 인터페이스 내 일반 함수
        fun walk() {
            println("다리로 걷는다.")
        }
    }
    
    
    class Rabbit(name: String) : Animal(name), Walker {
        // 필수 오버라이드
        override fun eat() {
            println("${name}이(가) 당근을 먹는다.")
        }
    
        // 필수 오버라이드
        override fun hasLegs() {
            println("${name}은(는) 4개의 다리를 가진다.")
        }
    }

     

     

    19. 고차함수

    고차함수는 함수를 마치 변수 혹은 인스턴스처럼 취급하는 방법으로, 함수를 파라미터로도 결과값으로도 반환받을 수 있다.
    함수를 파라미터로 넘겨줄 경우, 함수 이름 앞에 ::(콜론 2개)를 붙여준다.

    ex. b(::a)

    fun main() {
        b(::a) // "fun b()가 호출한 fun a() 출력"
    }
    
    private fun a(string: String) {
        println("$string fun a() 출력")
    }
    
    private fun b(function: (String) -> Unit) {
        function("fun b()가 호출한")
    }

     

     

    20. 람다함수

    그 자체가 고차함수, 연산자 없이 변수에 담을 수 있는 함수이다.

    fun main() {
        // 고차함수
        b(::a) // "fun b()가 호출한 고차함수 fun a() 출력"
    
        // 람다함수
        val c: (String) -> Unit = {
            println("$it 람다함수 fun c() 출력")
        }
        b(c) // "fun b()가 호출한 람다함수 fun c() 출력"
    }
    
    private fun a(string: String) {
        println("$string 고차함수 fun a() 출력")
    }
    
    private fun b(function: (String) -> Unit) {
        function("fun b()가 호출한")
    }
    val c: (String) -> Unit = { println("$it 람다함수 fun c() 출력") } 는 아래와 같이 사용할 수도 있다.

    val c = { string: String -> println("$string 람다함수 fun c() 출력") }

     

     

    20-1. 여러 줄이 작성된 람다함수

    람다함수는 여러 줄 작성도 가능하며, 마지막 줄이 반환된다.

    val calculate: (Int, Int) -> Int = { a, b ->
        println(a)
        println(b)
        a + b
    }

     

     

    20-2. 파라미터가 없는 람다함수

    실행할 구문만 나열한다. 추가로, 파라미터가 하나인 람다함수는 파라미터를 생략하고 it 키워드로 대체하여 사용할 수 있다.

    val a: () -> Unit = { println("no parameter") }

     

     

    21. 스코프 함수

    함수형 언어의 특징을 사용하기 위한 기본 제공 함수. apply, run, with, also, let.

     

     

    21-1. apply

    인스턴스를 생성하고, 변수에 담기 전 초기화할 때 주로 사용.

    fun main() {
        var a = Book("스리의 코틀린", 3000).apply {
            name = "[특가] $name"
            discount(1_000)
        } // 인스턴스 생성과 동시에 변수에 내용을 할당할 때 사용
        
        println("상품명: ${a.name}, 가격: ${a.price}원")
    }
    
    class Book(var name: String, var price: Int) {
        fun discount(discountMoney: Int) {
            price -= discountMoney
        }
    }
    apply 스코프 함수 사용 시, 이점?
    메인 함수와의 별도의 스코프에서 인스턴스의 변수와 함수를 조작할 수 있어, 코드가 깔끔해진다. (가독성 향상)

     

     

    21-2. run

    이미 인스턴스가 만들어진 후에 인스턴스의 함수나 속성을 스코프 내에서 사용해야 할 때 사용된다.

    package com.nf4.kotlin
    
    fun main() {
        var a = Book("스리의 코틀린", 3000).apply {
            name = "[특가] $name"
            discount(1_000)
        }
    
        a.run {
            println("상품명: $name, 가격: ${price}원") // 인스턴스 속성 이름만을 직접 사용
        }
    
        println("상품명: ${a.name}, 가격: ${a.price}원")
    }
    
    class Book(var name: String, var price: Int) {
        fun discount(discountMoney: Int) {
            price -= discountMoney
        }
    }

     

     

    21-3. with

    run과 동일하게, 이미 인스턴스가 만들어진 후에 인스턴스의 함수나 속성을 스코프 내에서 사용해야 할 때 사용된다.
    그러나 with는 참조연산자 a.run { ... } 대신 파라미터 with(a) {...} 로 받는다는 차이만 존재한다.

    package com.nf4.kotlin
    
    fun main() {
        var a = Book("스리의 코틀린", 3000).apply {
            name = "[특가] $name"
            discount(1_000)
        }
    
        a.run { // 참조 연산자로 받는 run: a.
            println("상품명: $name, 가격: ${price}원")
        }
    
        with(a) { // 파라미터로 받는 with: (a)
            println("상품명: $name, 가격: ${price}원")
        }
    
        println("상품명: ${a.name}, 가격: ${a.price}원")
    }
    
    class Book(var name: String, var price: Int) {
        fun discount(discountMoney: Int) {
            price -= discountMoney
        }
    }
    진짜 차이가 이뿐이라고?
    네.

     

     

    21-4. also와 let

    apply 사용 시 also로 대체하여 사용할 수 있다. (처리가 끝나면 인스턴스(변수)를 반환)
    run 사용 시 let으로 대체하여 사용할 수 있다. (처리가 끝나면 최종 결과를  반환)

    also, let은 왜 필요할까?
    아래의 예시를 보자.

    메인 함수의 최상단에 price라는 변수를 선언해둘 경우,
    run 스코프 함수를 사용해도 메인 함수에 선언되어 있는 price의 가격 5000원이 출력된다.

     

    오류: 스코프에 맞추어 price가 2000원이 아닌 5000원으로 출력

    fun main() {
        // price 변수 선언
        val price = 5_000
    
        val a = Book("스리의 코틀린", 3000).apply {
            name = "[특가] $name"
            discount(1_000)
        }
    
        a.run {
            println("run 상품명: $name, 가격: ${price}원")
            // run 상품명: [특가] 스리의 코틀린, 가격: 5000원
        }
    
        println("println 상품명: ${a.name}, 가격: ${a.price}원")
        // println 상품명: [특가] 스리의 코틀린, 가격: 2000원
    }
    
    class Book(var name: String, var price: Int) {
        fun discount(discountMoney: Int) {
            price -= discountMoney
        }
    }

     

    해결: run → let으로 수정 (및 it 키워드 사용)

    fun main() {
        val price = 5_000
    
        val a = Book("스리의 코틀린", 3000).apply {
            name = "[특가] $name"
            discount(1_000)
        }
    
        a.let {
            println("let 상품명: ${it.name}, 가격: ${it.price}원")
            // let 상품명: [특가] 스리의 코틀린, 가격: 2000원
        }
    
        println("println 상품명: ${a.name}, 가격: ${a.price}원")
        // println 상품명: [특가] 스리의 코틀린, 가격: 2000원
    }
    
    class Book(var name: String, var price: Int) {
        fun discount(discountMoney: Int) {
            price -= discountMoney
        }
    }

     

     

    22. 옵저버

    감시자의 역할을 하는 녀석.
    함수로 직접 요청하지 않았지만 키 입력, 터치 발생, 데이터 수신 등과 같이 발생하는 동작을 이벤트라고 한다.
    이벤트가 발생할 때마다 즉각적으로 처리할 수 있도록 만드는 프로그래밍 패턴을 옵저버 패턴이라고 한다.

    옵저버 패턴을 구현할 때에는 2개의 클래스가 필요하다.

    1. 이벤트를 수신하는(전달받는) 클래스 A
    2. 이벤트를 발생시키고 전달하는 클래스 B

    일반적으로 "B에서 이벤트가 발생하면, A에 있는 이벤트를 처리하는 함수를 호출하여 알려주면 되겠군."이라고 생각한다.
    그런데 실제로 사용될 때에는 "A에서 B 인스턴스를 부르고

     

    fun main() {
        EventPrinter().start() // 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 
    }
    
    interface EventListener {
        fun onEvent(count: Int)
    }
    
    // 이벤트 발생 클래스 (파라미터로 인터페이스를 받는다)
    class Counter(val listener: EventListener) {
        fun count() {
            for (i in 1..100) {
                if (i % 5 == 0) listener.onEvent(i)
            }
        }
    }
    
    // 이벤트 수신 클래스 (인터페이스를 상속받는다)
    class EventPrinter : EventListener {
        override fun onEvent(count: Int) {
            print("$count ")
        }
    
        fun start() {
            val counter = Counter(this) // 이벤트 발생 클래스의 인스턴스를 만들되, this 키워드로 구현부(onEvent) 전달
            counter.count()
        }
    }

     

    익명객체로 옵저버를 구현할 수도 있다.

    fun main() {
        EventPrinter().start()
    }
    
    interface EventListener {
        fun onEvent(count: Int)
    }
    
    // 이벤트 발생 클래스 (파라미터로 인터페이스를 받는다)
    class Counter(val listener: EventListener) {
        fun count() {
            for (i in 1..100) {
                if (i % 5 == 0) listener.onEvent(i)
            }
        }
    }
    
    // 이벤트 수신 클래스 (인터페이스를 상속받는다)
    class EventPrinter {
        fun start() {
            val counter = Counter(object : EventListener { // 이름이 없는 익명객체로 옵저버 구현
                override fun onEvent(count: Int) {
                    print("$count ")
                }
            })
            counter.count()
        }
    }
Designed by Tistory.