코틀린 완벽 가이드 3장

맥모닝·2023년 12월 15일
0

Kotlin-In-Depth

목록 보기
1/1
post-thumbnail

1. 식이 본문인 함수란 무엇인가?

return 키워드와 블록을 만드는 중괄호({})를 생략하는 단일 식 형태의 함수로, 컴파일러가 표현식의 타입을 추론할 수 있기 때문에 명시적인 반환 타입을 생략해도 된다.

fun circleArea(radius: Double) = { PI * radius * radius }

블록이 본문인 함수 대신 식이 본문인 함수를 쓰면 어떤 경우에 더 좋을까?

로직이 단순하고 결과를 바로 반환하는 경우 식이 본문인 함수를 쓰는게 더 좋다.

블록이 본문인 함수는 복잡한 연산이나 여러 단계의 처리가 필요한 경우에 더 적절하다.

2. 디폴트 파라미터와 함수 오버로딩 중 어느 쪽을 써야 할지 어떻게 결정할 수 있을까?

미리 정해진 디폴트 값을 사용할 수 있게 하고자 메서드를 오버로딩해야 하는 경우 디폴트 파라미터를 사용하고, 파라미터의 개수나 타입에 따라 다른 로직을 수행해야 하는 경우 오버로딩을 사용하는 것이 좋다.

🙄 외전 : 반은 맞고, 반은 틀리다.

  • 함수가 호출되는 런타임에서 좌에서 우로 해석하기 때문에 c는 디폴트 값으로 들어간(스택에 다시 쌓인) a와 b를 알고 있다. b는 a를 알고 있다.
fun plus(a: Int, b: Int) = a + b

fun c(a: Int, b: Int = plus(a, 4), c: Int = plus(a + b)) = a + b + c

3. 이름 붙은 인자를 사용할 경우의 장단점은 무엇인가?

장점 : 인자의 순서를 바꿀 수 있다.

  • WHY) : 인자의 위치가 아니라 파라미터의 이름을 명시하여 인자를 전달하는 방식이기 때문이다.
  • 외전 : 두 인자의 타입이 같다면 컴파일 에러가 발생하지 않고 런타임 에러가 발생할 수 있다.
    • BUT) 두 인자의 타입이 다르다면 컴파일 에러가 발생한다.
fun plus(a: Int, b: Int) = a + b

fun main() {
    val a = 3
    val b = 7
    print(plus(b, a)) // 10 
}

단점 : 위치 기반 인자와 이름 붙은 인자를 함께 사용한 경우 원래 인자가 들어가야 할 위치에 이름 붙은 인자를 지정해야 정상 처리되며, 그렇지 않은 경우 위치 기반 인자의 타입이 어긋나거나 이미 할당된 인자를 재할당하기 때문에 컴파일 오류가 발생한다.

4. 인자 개수가 가변적인 함수를 정의하는 방법은 무엇인가?

파라미터 정의 앞에 vararg 변경자(modifier)를 붙여 배열 타입으로 넘기거나 스프레드(spread) 연산자인 *를 사용하여 배열을 가변 인자 대신 넘길 수 있다.

코틀린과 자바에서 vararg 함수는 어떻게 다른가?

자바에서는 vararg 파라미터가 항상 마지막 파라미터이지만, 코틀린에서는 vararg 파라미터가 함수 파라미터 중 어디에나 올 수 있다. 다만, vararg 파라미터 이후의 파라미터는 이름 붙은 인자로만 전달할 수 있다.

fun printSorted(vararg items: Int, prefix: String = "") { }

fun main() {
    printSorted(6, 2, 10, 1, "!") // Error
    
    printSorted(6, 2, 10, 1, prefix = "!") // Success
    
    val numbers = intArrayOf(6, 2, 10, 1)
    printSorted(*numbers) // Success
}
  • 외전 : 인자의 형태가 블록인 경우(람다) 명시적인 인자가 필요없다.
fun printSorted(vararg items: Int, block: () -> Unit = {}) { }

fun main() {
    printSorted(1, 2, 3 , 5) { /*....*/ } // Success
} 

5. Unit과 Nothing 타입을 어디에 사용하는가? (이들을 자바의 void와 비교해 설명하라.)

Unit은 함수가 의미 없는 반환값을 돌려줄 때 사용한다.

Nothing은 프로그램의 순차적 제어 흐름이 그 부분에서 끝나되 어떤 잘 정의된 값에 도달하지 못할 때 사용한다.

Unit은 자바 void에 해당하는 코틀린 타입으로, Nothing과 달리 한 가지 인스턴스가 존재하는데, 이 인스턴스는 보통 유용한 값이 없다는 사실을 표현한다. 반면 Nothing은 아예 값이 없다는 사실을 표현한다.

Nothing이나 Unit이 타입인 함수를 정의해 사용할 수 있는가?

Nothing 타입은 return의 경우 이 문장을 둘러싼 함수가 끝난다. 모든 코틀린 타입의 하위 타입으로 간주되기 때문에 식이 필요한 위치에 return을 사용해도 타입 오류가 발생하지 않는다.

  • return e의 e 값은 return 식의 값이 아니라 함수의 반환값이라는 점에 유의하자. return 식 자체는 아무 값이 없고 Nothing 타입에 속한다.

Unit 타입은 함수에서 결과가 항상 Unit으로 동일하기 때문에 결과를 지정하는 return 문을 쓸 필요가 없다. 하지만 함수 본문의 끝에 도달하기 전에 함수 실행을 마치려면 return 문을 사용해 함수를 끝내야 한다.

6. return 0과 같은 코드의 의미를 설명해보라.

해당 함수가 정수 0을 반환하는 것으로, 해당 함수의 반환 타입이 Int일 때 사용된다. 보통 함수의 반환 유형을 명시적으로 선언하고, 반환값이 필요 없는 경우에는 Unit을 사용한다. 따라서 return 0은 주로 함수가 특정 결과를 반환하는 상황에서 사용된다.

  • 함수가 어떤 계산을 수행하고 그 결과를 반환하는데, 그 결과가 0이면 성공적으로 완료되었음을 나타낼 때 사용할 수 있다.
fun exampleFunction(): Int {
    // 어떤 계산을 수행한 후, 결과가 0이면 성공적으로 완료되었음을 나타냄
    return 0
}

fun main() {
    val result = exampleFunction()

    if (result == 0) {
        println("함수가 성공적으로 실행되었습니다.")
    } else {
        println("함수 실행 중 오류가 발생했습니다.")
    }
}

이런 코드가 올바르지만 불필요한 중복이 있는 것으로 여겨지는 이유는 무엇인가?

함수의 성공 또는 실패를 나타내는 값이 0 하나로 특정되어 있기 때문이다. 즉, 함수의 성공 여부를 판단하는 데 사용되는 값이 항상 0이라면, 매번 이 값을 확인하는 코드가 중복된 느낌을 줄 수 있다.

코틀린에서는 일반적으로 함수가 성공적으로 수행되면 Unit을 반환하거나, 실패한 경우 예외를 던지는 등의 방식을 사용한다. 이로써 성공 또는 실패를 판단하는 코드에서 중복을 최소화하고 가독성을 높일 수 있다.

fun exampleFunction(): Unit {
    // 어떤 계산을 수행
    // 성공적으로 완료되면 아무것도 반환하지 않음 (Unit)
}

fun main() {
    try {
        exampleFunction()
        println("함수가 성공적으로 실행되었습니다.")
    } catch (e: Exception) {
        println("함수 실행 중 오류가 발생했습니다: $e")
    }
}

7. return 문을 사용하지 않는 함수를 정의할 수 있는가?

반환값이 필요 없는 함수(반환 타입이 Unit인 경우)를 정의할 때 return 문을 생략할 수 있다.

8. 지역 함수란 무엇인가?

특정 함수 내에 정의된 함수로, 자신을 둘러싼 함수의 변수(args)나 함수에 접근할 수 있다. 지역 함수와 변수는 가시성 변경자를 붙일 수 없다.

fun main(args: Array<String>) {
    fun swap(i: Int, j: Int): String {
        val chars = args[0].toCharArray()
        val tmp = chars[i]
        chars[i] = chars[j]
        chars[j] = tmp
        return chars.concatToString()
    }
    
    println(swap(0, args[0].lastIndex))
}

이런 함수를 자바에서는 어떻게 흉내 낼 수 있을까?

지역 함수를 둘러싼 영역의 변수나 파라미터 목록 등의 문맥을 포획해주는 특별한 클래스(익명 클래스, 람다 표현식)를 선언한다. 그래서 지역 함수를 호출할 때마다 이런 특별한 객체를 생성하는 부가 비용이 든다.

9. 공개(public)와 비공개(private) 최상위 함수는 어떤 차이가 있는가?

최상위 함수는 디폴트로 공개 가시성을 가지며, 함수가 정의된 파일 내부뿐 아니라 프로젝트 어디에서나 쓰일 수 있다. 반면에 최상위 함수를 비공개로 정의하면 함수가 정의된 파일 안에서만 해당 함수를 볼 수 있다.

10. 패키지를 사용해 코드를 어떻게 여러 그룹으로 나눌 수 있는가?

맨 앞에 패키지 이름을 지정하면 파일에 있는 모든 최상위 선언(타입, 함수, 프로퍼티)을 지정한 패키지 내부에 넣을 수 있다. 패키지를 사용하지 않으면 컴파일러는 파일이 디폴트 최상위 패키지에 속한다고 가정한다. 디폴트 최상위 패키지는 이름이 없다.

package foo.bar.util

fun readInt(radix: Int = 10) = readLine()!!.toInt(radix)

// 패키지 계층에서 루트 패키지 바로 아래에 있는 경우
package numberUtil

fun readDouble() = readLine()!!.toDouble()

자바와 코틀린 패키지의 가장 핵심적인 차이는 무엇인지 설명하라.

자바에서는 패키지 구조와 컴파일 대상 루트에 있는 소스 트리 디렉터리 구조가 같아야 한다. 둘의 경로가 다르면 컴파일 오류가 발생한다.

코틀린의 패키지 계층 구조는 소스 파일에 있는 패키지 디렉티브로부터 구성된 별도의 구조다. 즉 소스 파일 트리와 패키지 계층 구조가 다를 수도 있다.

  • 예를 들어 소스 파일은 모두 한 디렉터리 아래에 있지만 각각이 서로 다른 패키지에 포함될 수도 있고, 한 패키지에 포함된 소스 파일들이 모두 서로 다른 디렉터리에 들어갈 수도 있다.

11. 임포트 별명이란 무엇인가?

임포트한 선언에 새 이름을 부여하는 것으로, 새 이름은 임포트 이렉티브가 있는 파일 전체 영역에서 유효하다. 또 다른 형태의 임포트로 어떤 영역에 속한 모든 선언을 한꺼번에 임포트할 수 있다. 전체 이름 뒤에 *를 붙이면 된다.

자바의 정적 임포트와 비슷한 임포트를 코틀린에서는 어떻게 처리하는가?

자바에서는 임포트 별명을 줄 수 없기 때문에 이름이 같으면 풀패키지 밖에 명시할 수 없다.

import foo.readInt as fooReadInt
import bar.readInt as barReadInt

fun main() {
    val n = fooReadInt()
    val m = barReadInt()
}

코틀린에서는 정적 임포트 구문 없이 일반적인 임포트 디렉티브 구문을 사용해 클래스의 메소드나 속성을 임포트할 수 있다.

12. if 문/식은 어떤 일을 하는가? 각각을 자바의 if 문 및 3항 조건 연산자(?:)와 비교해보라.

if 문/식은 불(boolean) 식의 결과에 따라 두 가지 대안 중 하나를 선택할 수 있다. 코틀린 if는 자바 if 문과 비슷한 문법을 제공하지만, 3항 연산자(조건 ? 참일 때식 : 거짓일 때식)가 없다. 대신에 if 문을 식으로 쓸 수 있다는 점이 이 단점을 대부분 상쇄해준다. if 문을 식으로 사용할 때는 양 가지가 모두 있어야 한다.

13. when 문을 처리하는 알고리즘을 설명하라.

when 문은 코드에 쓰여져 있는 순서대로 조건을 검사해서 맨 처음으로 참으로 평가되는 조건을 찾고, 해당 조건에 대응하는 문을 실행한다. 만약 모든 조건이 거짓이라면 else 부분을 실행한다.

자바 switch와 코틀린 when은 어떤 차이가 있는가?

  1. when에서는 임의의 조건을 검사할 수 있지만 switch에서는 주어진 식의 여러 가지 값 중 하나만 선택할 수 있다. 게다가 자바의 switch 문은 폴스루(fall-through)라는 의미를 제공한다. 코틀린 when은 조건을 만족하는 가지만 실행하고 절대 폴스루를 하지 않는다.
  • 폴스루 : 어떤 조건을 만족할 때 프로그램이 해당 조건에 대응하는 문을 실행하고 명시적으로 break를 만날 때까지 그 이후의 모든 가지를 실행한다.
  1. switch 식에는 범위 검사(코틀린의 in/!in)를 지원하지 않고 오직 정수, 이넘, 문자열 같은 몇 가지 타입에 대해서만 사용할 수 있다. when에서는 상수가 아닌 임의의 식을 사용해도 된다.

14. 자바 for (int i = 0; i < 100; i++)와 같이 수를 세는 루프를 코틀린에서는 어떻게 구현하는가?

for (i in 0..99) or for (i in 0 until 100)

15. 코틀린이 제공하는 루프 문에는 어떤 것이 있는가?

  • for, while, do-while

while과 do...while의 차이는 무엇인가?

while 문은 어떤 조건이 참인 동안 루프를 실행하지만 루프 몸통을 실행하기 전에 조건을 먼저 검사한다. 이 경우 처음부터 조건이 거짓이면 루프 몸통이 한 번도 실행되지 않는다.

do-while 문은 루프 몸통을 실행한 다음에 조건을 검사하므로 루프 몸통이 최소 한 번은 실행된다.

16. break와 continue를 사용해 루프의 제어 흐름을 어떻게 변경할 수 있는가?

break는 즉시 루프를 종료시키고, 실행 흐름이 루프 바로 다음 문으로 이동하게 만든다.

continue는 현재 루프 이터레이션(iteration)을 마치고 조건 검사로 바로 진행하게 만든다.

17. 예외 처리 과정을 전체적으로 설명하라.

프로그램은 예외를 잡아내는 핸들러를 찾는다. 예외와 일치하는 핸들러가 있다면 예외 핸들러가 처리한다.
현재 함수 내부에서 핸들러를 찾을 수 없으면 함수 실행이 종료되고 함수가 스택에서 제거 된다. 그리고 호출한 쪽에서 핸들러 검색을 수행한다. 이런 경우를 호출자에게 전파했다고 말한다.
프로그램 진입접에 이를 때까지 예외를 잡아내지 못하면 현재 스레드가 종료된다.

자바와의 차이점은 무엇인가?

자바와 달리 코틀린에서는 클래스 인스턴스를 생성(여기서는 예외)할 때 new 같은 특별한 구문을 사용하지 않는다. 자바에서는 도착할 수 없는 코드를 금지하지만, 코틀린은 허용한다. 자바와 달리 코틀린에서는 검사 예외(checked exception)와 비검사 예외(unchecked exception)을 구분하지 않는다.

자바와 코틀린에서 try 문이 어떻게 다른지 설명하라.

코틀린에서는 try가 식으로 쓰일 수 있어 try 블록의 값이거나 예외를 처리한 catch 블록의 값이 된다.


외전

1. 식은 로직이 단순하고 결과를 바로 반환하는 경우에만 사용될까? No

명시적인 반환타입 vs 암시적인 반환 타입

명시적인 반환 타입은 먼저 반환 타입을 확정짓고, 그 타입에 맞춰 함수 블록을 개발할 수 있다.

암시적인 반환 타입은 함수 블록이 내가 원하는 타입으로 잘 반환하는지를 컴파일러의 입장을 들어볼 수 있다. 하지만 복잡성이 높아질수록 컴파일 속도가 엄청 느려지는 단점이 있다.

할당 식도 타입 추론이 똑같이 작동한다. 변수마다 타입을 명시해주는 것이 컴파일 속도가 빨라진다.

fun a(): Number = if(true) {
    // 가정: 타입 추론을 적용했을 때
    val a: {Comparable<*> & Number} = if (true) 3 else 7.0
    4
} else {
    6
}

2. 디폴트 파라미터 vs 함수 오버로딩

디폴트 파라미터로 함수 오버로딩을 절대로 흉내낼 수 없는 부분은 인자가 다른 형을 갖는 것을 허용하지 않는다. 어떤 함수가 다양한 형의 인자를 받고 싶다면 함수 오버로딩 외에는 방법이 없다.

fun plus(a: Int, b: Long) = a + b // Error

fun c(a: Int, b: Int = plus(a, 4), c: Int = plus(a + b)) = a + b + c

3. 이름 붙은 인자 장단점

장점 : 인자의 순서를 바꿀 수 있다.

"인자의 순서를 바꿀 수 있다"의 조건

(1) 인자가 너무 많을 때 (2) 기본값 인자가 많을 때 (3) vararg 뒤에 인자가 더 있을 때

  • 선택적 인자이기 때문에 이름을 지정한 인자만 쓸 수 있어서 유리하다. 또는 디폴트가 있는 부분들은 순서에 상관없이 이름을 지정할 수 있어서 유리하다.
fun plus(c: Int, a: Int = 3, b: Int = 4) = a + b

fun main() {
    print(plus(5, b = 2, a = 1))
}
  • 모든 값을 다 넣을 수 없으니까 다 기본값으로 주어지고 선택적 인자로 쓸 수 밖에 없다.
// Compose-style : 인자가 수백가지다.
style( 
    border = 10.px,
    padding = 10.px,
    ...
)

8. 지역 함수 - 언제 쓸까?

fun main(args: List<String>) {
    fun localFun(a: Int) {
        a * 2
    }
    println("${localFun(11)}")
} 
  • 자바로 디컴파일된 코드 : 지역 함수를 람다로 바꿔서 처리한다.
public final class MainKt {
    public static void main(@NotNull String[] args) {
        ...
        println(String.valueOf($fun$localFun$1.invoke(11)))
    }
}

하지만 람다를 쓰나 함수를 쓰나 똑같다(?) 그러면 람다는 언제 쓰고, 함수는 언제 쓰나?

함수는 람다와 달리 리턴문을 사용할 수 있기 때문에, 예외 처리를 다룰 때(쉴드 패턴) 함수 내부를 구현하기 쉽다.

13. when문

  • 반드시 순차적으로 수식을 평가한다. 따라서 절대로 컴파일 최적화가 이루어지지 않는다.
// 임의의 정수를 그에 대응하는 16진 숫자로 바꾸는 함수
fun hexDigit(n: Int): Char {
    when {
        n in 0..9 -> return '0' + n
        n in 10..15 -> return 'A' + n - 10
        else -> return '?'
    }
}
  • when 문에 값(n)이 있는 경우에는 병렬 최적화가 일어날 가능성이 존재한다.
fun readHexDigit() = when(val n = readLine()!!.toInt()) {
    in 0..9 -> '0' + n
    in 10...15 -> 'A' + n - 10
    else -> '?'
}
  • 자바에서 if-else 문 대신 switch를 쓰는 이유 : 어셈블러가 if-else 문은 개별로 평가하는 단계형으로 컴파일하지만, switch문은 병렬 맵으로 한번에 점프하는 명령으로 컴파일된다.

14. for 문

  • 코틀린은 2단계 컴파일을 한다 : 1단계에서 바이트코드를 만들고 2단계에서 플랫폼용으로 번역한다.
  • 2단계에서 컴파일 최적화가 많이 이루어지기 때문에 1단계에서 코틀린에서는 객체였지만 2단계에서 그냥 값으로 번역될 수 있다.
fun main() {
    val a = readLine()!!.toInt()
    val b = readLine()!!.toInt()
    println(5 in a..b)
}
  • 객체 생성 비용이나 함수 호출 비용이 발생하는가? : No - 런타임에 IntRange 인스턴스를 생성하지 않고, 5를 입력한 값과 비교한다.

15. for vs while

반복문 안에서 for 문은 while 문보다 얼마나 반복할지를 명확히 알 수 있다.

바디가 루프 계획에 영향을 미치는 경우(동적 계획)는 for 문보다 while 문을 사용하는 것이 좋다.

참고한 사이트

profile
필요한 내용을 공부하고 저장합니다.

0개의 댓글