코틀린 베이직 KB 08 문과 식 — if 와 try 가 값을 돌려준다
코틀린 베이직 · 8/26

KB 08 문과 식 — if 와 try 가 값을 돌려준다

gabury1고친 사람 github-actions[bot]

0. 삼항 연산자를 옮기면 컴파일이 안 된다

둘 중 큰 값을 고를 때 자바 개발자의 손은 삼항 연산자로 갑니다. int max = a > b ? a : b; 한 줄입니다. 이 줄을 담은 Ternary.java 는 문제없이 7 을 찍습니다.

같은 줄을 코틀린 파일 Ternary.kt 로 옮겼습니다.

Kotlin
fun main() {
    val a = 3
    val b = 7
    val max = a > b ? a : b
    println(max)
}
  • fun main() — 프로그램이 시작하는 함수입니다. 클래스로 감싸지 않고 파일에 바로 씁니다
  • val — 다시 대입할 수 없는 변수입니다. 타입을 안 적으면 오른쪽 값에서 알아냅니다
  • println(max) — 한 줄 출력입니다. 코틀린은 줄 끝에 세미콜론을 쓰지 않습니다

이 파일을 코틀린 컴파일러 kotlinc 에 넣었습니다. -d out 은 결과물을 out 폴더에 쓰라는 옵션입니다.

터미널
$ kotlinc Ternary.kt -d out
Ternary.kt:4:21: error: syntax error: Unexpected tokens (use ';' to separate expressions on the same line).
    val max = a > b ? a : b
                    ^^^^^^^

? 부터 줄 끝까지가 「예상하지 못한 토큰」으로 잡혔습니다. 토큰은 컴파일러가 소스를 읽을 때 쪼개는 낱말 단위입니다. ?·a·: 는 코틀린에 있는 토큰이지만, 코틀린 문법에는 조건 ? 값 : 값 이라는 모양이 없습니다. a > b 뒤에 ? 가 올 수 있는 문법이 없어 컴파일러는 거기서 멈췄습니다.

코틀린에서는 이 줄을 val max = if (a > b) a else b 로 씁니다. 이 줄로 바꾼 Max.kt 는 7 을 찍습니다.

코틀린 공식 문서는 삼항 연산자가 빠진 이유를 if 에서 찾습니다(「The if expression serves the same purpose as the ternary operator」 코틀린 문서). if 가 값을 돌려주니 따로 연산자를 둘 필요가 없다는 뜻입니다. 이 편은 그 차이를 문과 식이라는 두 낱말로 가릅니다. 문은 값을 남기지 않고, 식은 값을 남깁니다. 그 선을 그은 뒤 if·try·throw 를 차례로 자바와 맞대 봅니다. 출력은 전부 Kotlin 2.4.10, JDK(Java Development Kit) 21 에서 직접 돌린 결과입니다.

1. 문과 식 — 값이 남느냐 남지 않느냐

식(expression) 은 계산이 끝나면 값이 하나 남는 코드 조각입니다. a + b, a > b, max * 2 가 식입니다. 값이 남으니 = 오른쪽에도, 함수 인자로도 넣을 수 있습니다.

문(statement) 은 무언가를 하되 값을 남기지 않는 코드 조각입니다. 자바의 if, for, try 가 문입니다. 남는 값이 없으니 = 오른쪽에 넣을 수 없습니다.

둘을 가르는 시험은 하나입니다. val x = (그 코드) 로 쓸 수 있으면 식입니다.

대입은 자바에서만 식이다

두 언어는 이 선을 다르게 긋습니다. 가장 먼저 부딪히는 사례가 대입입니다. Assign.java 는 대입 x = 5 를 괄호로 싸서 덧셈에 넣었습니다. 자바에서 대입은 식이라, 대입한 값 5 가 괄호 밖으로 남습니다. 컴파일해 돌렸습니다. 자바 결과물은 코틀린 결과물과 섞이지 않게 jout 폴더에 모읍니다.

Java
public class Assign {
    public static void main(String[] args) {
        int x;
        int y = (x = 5) + 1;
        // ↓ 5 6
        System.out.println(x + " " + y);
    }
}
터미널
$ javac -d jout Assign.java
$ java -cp jout Assign

(x = 5) 가 5 를 남겨 y 는 6 이 됐습니다. 같은 줄을 코틀린 Assign.kt 로 옮겼습니다. x 는 var x = 0 으로 선언했습니다. var 는 val 과 달리 다시 대입할 수 있는 변수입니다. 이번에는 컴파일 단계에서 멈춥니다.

터미널
$ kotlinc Assign.kt -d out
Assign.kt:3:14: error: only expressions are allowed in this context.
    val y = (x = 5) + 1
             ^^^^^

에러 문구는 「이 문맥에는 식만 올 수 있다」입니다. 코틀린에서 대입 x = 5 는 값을 남기지 않는 문입니다. 그래서 괄호 안에 넣어 값으로 쓸 수 없습니다.

BufferedReader 로 한 줄씩 읽을 때 자바에서 흔히 쓰는 while ((line = r.readLine()) != null) 도 같은 이유로 코틀린에서는 그대로 쓸 수 없습니다. 옮겨 컴파일하면 같은 only expressions are allowed in this context 에러가 납니다.

방향이 반대인 셋

대입과 반대로 움직인 코드도 있습니다. 자바에서 문이던 if·try·throw 가 코틀린에서는 식입니다.

코드 자바 코틀린 다루는 절
x = 5 (대입) 식 문 1절
if (...) ... else ... 문 식 2절
try { ... } catch ... 문 식 3절
throw ... 문 식 4절

식은 값이 남고 문은 남지 않습니다. 코틀린은 대입을 문으로 내리고 if·try·throw 를 식으로 올렸습니다.

2. if 가 값을 돌려준다

0절의 if (a > b) a else b 가 어떻게 값이 되는지 봅니다. 가지가 여러 줄일 때와 else 가 빠질 때를 차례로 봅니다.

자바는 = 오른쪽에서 if 를 거절한다

먼저 자바입니다. Grade.java 의 grade(int score) 메서드는 점수로 등급을 고르는 if 를 = 오른쪽에 두었습니다. javac 는 에러 세 개를 냈습니다. 첫 에러가 원인이라 뒤따르는 not a statement 두 개는 ... 로 줄였습니다.

터미널
$ javac -d jout Grade.java
Grade.java:3: error: illegal start of expression
        String g = if (score >= 90) "A"; else "B";
                   ^
...
3 errors

illegal start of expression 은 「식이 이렇게 시작할 수 없다」는 뜻입니다. = 오른쪽은 식이 올 곳인데, 자바의 if 는 문입니다. 자바에서는 삼항 연산자로 바꾸거나, 변수를 먼저 선언하고 가지마다 대입해야 합니다.

블록의 마지막 줄이 값이다

코틀린은 가지가 여러 줄이어도 if 가 값이 됩니다. Grade.kt 는 90점 이상이면 한 줄을 찍고 "A" 를 고릅니다.

Kotlin
fun grade(score: Int): String {
    val g = if (score >= 90) {
        println("top")     // top
        "A"
    } else {
        "B"
    }
    return g
}

fun main() {
    println(grade(95))     // A
    println(grade(70))     // B
}
  • score: Int — 코틀린은 이름 뒤에 콜론, 그 뒤에 타입을 씁니다. 괄호 뒤 : String 은 반환 타입입니다
  • if (...) { ... } else { ... } — 가지를 중괄호 블록으로 쓴 if 식입니다
  • 블록의 마지막 줄 "A" 가 그 가지의 값입니다. 앞줄 println("top") 은 실행만 되고 값이 되지 않습니다
  • return g — 자바의 return 과 같습니다

컴파일한 뒤 kotlin 명령으로 돌립니다. classpath 는 클래스를 찾을 경로 목록입니다. kotlin 은 코틀린 표준 라이브러리 jar 를 이 목록에 스스로 넣어 줍니다.

실행할 클래스 GradeKt 는 컴파일러가 파일 이름 + Kt 로 지은 이름입니다. 파일에 바로 쓴 함수는 이 클래스의 static 메서드가 됩니다(KB 01 첫 파일 — fun main 한 줄이 JVM 에서 도는 법).

터미널
$ kotlinc Grade.kt -d out
$ kotlin -cp out GradeKt

95점일 때는 top 을 찍은 뒤 A 를 돌려주었습니다. 70점일 때는 else 블록의 마지막 줄 "B" 가 값입니다.

else 가 빠지면

if 가 값을 남기려면 조건이 거짓일 때 남길 값도 있어야 합니다. NoElse.kt 는 val score = 95 를 두고 else 없는 if 를 val g 에 넣었습니다. 조건은 참인데도 컴파일러는 거절합니다.

터미널
$ kotlinc NoElse.kt -d out
NoElse.kt:3:13: error: 'if' must have both main and 'else' branches when used as an expression.
    val g = if (score >= 90) "A"
            ^^

「식으로 쓸 때는 본 가지와 else 가지가 둘 다 있어야 한다」는 에러입니다. 이 검사에서 컴파일러는 score 의 실제 값을 따지지 않고 가지가 둘 다 있는지만 봅니다. 조건이 거짓인 경우 g 에 넣을 값이 없으니 거절합니다.

단서는 「식으로 쓸 때」입니다. 4절에 나올 findName 의 if (id == 1) return "kim" 은 else 가 없는데도 경고 없이 컴파일됩니다. 그 if 는 값을 쓰지 않는 문이기 때문입니다.

코틀린의 if 는 가지의 마지막 줄을 값으로 남기는 식입니다. 값으로 쓸 때만 else 가 필수입니다.

3. try 도 식이다

자바에서 try 안에서 정한 값을 밖에서 쓰려면 변수를 try 앞에 선언해야 했습니다. 그 변수를 final 로 두려 하면 문제가 생깁니다.

자바 — final 변수는 catch 에서 못 채운다

Parse.java 는 문자열을 숫자로 바꾸고, 실패하면 -1 을 넣습니다. 한 번만 대입할 값이라 final 을 붙였습니다.

Java
public class Parse {
    static int parse(String s) {
        final int n;
        try {
            n = Integer.parseInt(s);
        } catch (NumberFormatException e) {
            n = -1;
        }
        return n;
    }
}

try 가지와 catch 가지 중 한쪽만 실행되니 n 은 한 번만 대입됩니다. 그런데 javac 의 판단은 다릅니다.

터미널
$ javac -d jout Parse.java
Parse.java:7: error: variable n might already have been assigned
            n = -1;
            ^
1 error

「n 이 이미 대입됐을 수도 있다」는 에러입니다. javac 는 try 블록의 어느 줄에서 예외가 났는지 모른다고 봅니다. 그래서 catch 에 들어온 시점에 n 이 이미 채워졌을 가능성을 지우지 못합니다.

final 을 뗀 Parse2.java 는 통과하고 parse("abc") 에 -1 을 찍었습니다. 대신 n 은 언제든 다시 대입할 수 있는 변수로 남습니다.

코틀린 — try 가 값을 남긴다

코틀린은 try 전체를 val 의 오른쪽에 둡니다. Parse.kt 입니다.

Kotlin
fun parse(s: String): Int {
    val n = try {
        s.toInt()
    } catch (e: NumberFormatException) {
        -1
    }
    return n
}

fun main() {
    println(parse("42"))    // 42
    println(parse("abc"))   // -1
}
  • s.toInt() — 문자열을 Int 로 바꿉니다. 숫자가 아니면 NumberFormatException 을 던집니다
  • catch (e: NumberFormatException) — 자바의 catch (NumberFormatException e) 입니다. 여기서도 이름이 먼저, 타입이 뒤입니다
  • val n = try { ... } catch ... { ... } — 예외가 없으면 try 블록의 마지막 줄이, 잡으면 catch 블록의 마지막 줄이 값입니다

"42" 와 "abc" 를 넣어 돌렸습니다.

터미널
$ kotlinc Parse.kt -d out
$ kotlin -cp out ParseKt

다시 대입할 수 없는 val 인데도 에러가 없습니다. 대입은 val n = 한 번뿐입니다. 어느 가지의 값이 들어갈지는 try 식이 정합니다. 앞의 Parse.java 를 막아 세운 might already have been assigned 는 나오지 않습니다. 가지마다 따로 대입하는 줄이 아예 없기 때문입니다.

finally 의 마지막 줄은 값이 되지 않는다

try 식에 finally 를 붙여도 값은 try 나 catch 쪽에서 나옵니다. val r = try { "try" } finally { "finally" } 를 담은 Finally.kt 는 try 를 찍었습니다. "finally" 줄에는 expression is unused(쓰이지 않는 식) 경고가 났습니다.

코틀린 공식 문서의 예외 항목에도 같은 규칙이 있습니다(「The finally block is always executed, but it doesn't change the result of the try-catch block.」 코틀린 문서 「Exceptions」).

코틀린의 try 는 try 나 catch 블록의 마지막 줄을 값으로 남깁니다. 그래서 자바에서 final 을 포기해야 했던 코드를 val 한 줄로 씁니다.

4. throw 도 식이다

값이 null 이면 기본값 대신 예외를 던지고 싶을 때가 있습니다. 이 절에서는 그 한 줄이 자바와 코틀린에서 어떻게 갈리는지 봅니다. 먼저 그 한 줄에 쓰이는 연산자 ?: 부터 봅니다.

?: 는 null 인가를 본다

코틀린에는 ?: 라는 연산자가 있습니다. 모양이 비슷해 삼항 연산자의 줄임꼴처럼 보이지만 뜻이 다릅니다. Elvis.kt 입니다.

Kotlin
fun findName(id: Int): String? {
    if (id == 1) return "kim"
    return null
}

fun main() {
    val name = findName(2) ?: "none"
    println(name)          // none
}
  • String? — 타입 뒤의 ? 는 null 일 수도 있다는 표시입니다. 물음표 없는 String 에는 null 을 담지 못합니다
  • ?: — 왼쪽이 null 이 아니면 왼쪽 값을, null 이면 오른쪽 값을 씁니다. 엘비스 연산자(Elvis operator)라고 부릅니다

findName(2) 가 null 을 돌려주었으므로 name 은 오른쪽의 "none" 이 됐습니다. findName(2) 결과를 변수 found 에 받았다면 자바로는 found != null ? found : "none" 입니다. 세 모양을 나란히 놓으면 이렇습니다.

모양 무엇을 보고 고르나 코틀린에서
c ? x : y c 가 참인가 문법 에러
if (c) x else y c 가 참인가 삼항 연산자 대신 쓴다
x ?: y x 가 null 인가 null 일 때 쓸 값을 정한다

?: 가 가르는 기준은 참과 거짓이 아니라 null 인가 아닌가입니다. null 을 다루는 문법은 뒤에 나올 널 편에서 따로 봅니다.

?: throw 한 줄

이번에는 ?: 의 오른쪽에 기본값 대신 throw 를 둡니다. Require.kt 입니다. findName 은 Elvis.kt 와 같아 생략했습니다.

Kotlin
fun nameOf(id: Int): String {
    val name = findName(id) ?: throw IllegalArgumentException("no user")
    return name
}

fun main() {
    println(nameOf(1))  // kim
    println(nameOf(2))  // 예외로 멈춘다
}
  • IllegalArgumentException("no user") — 예외 객체 생성입니다. 코틀린에는 new 가 없습니다
  • ?: throw ... — findName(id) 가 null 이면 곧바로 예외를 던집니다. 아니면 그 값이 name 에 들어갑니다

nameOf(2) 는 findName 이 null 을 돌려주어 Exception in thread "main" java.lang.IllegalArgumentException: no user 를 찍고 멈춥니다.

자바로 옮기면 삼항 연산자의 한쪽 가지에 throw 를 두는 모양입니다. Require.java 의 nameOf 입니다.

Java
    static String nameOf(int id) {
        String found = findName(id);
        String name = found != null
            ? found
            : throw new IllegalArgumentException("no user");
        return name;
    }

findName 은 kim 아니면 null 을 돌려주는 같은 메서드라 생략했습니다. 컴파일 결과입니다.

터미널
$ javac -d jout Require.java
Require.java:10: error: illegal start of expression
            : throw new IllegalArgumentException("no user");
              ^
1 error

2절과 같은 illegal start of expression 입니다. 자바의 throw 는 문이라 삼항 연산자의 가지에 들어가지 못합니다. JDK 14 부터 쓰는 switch 식의 가지만은 throw 문을 따로 허용합니다(JEP 361, Switch.java 로 확인).

코틀린의 throw 는 값 자리에 들어가는 식입니다. 그래서 ?: 의 오른쪽에 두어 null 검사와 예외를 한 줄로 씁니다.

5. throw 의 타입은 Nothing

4절의 nameOf 를 다시 봅니다. ?: 의 오른쪽에 throw 를 뒀는데도 name 은 String 으로 남았고, return name 이 그대로 통과했습니다. 왜 그럴까요.

?: 식 전체가 무엇이 되는지는 왼쪽 값과 오른쪽 식을 둘 다 담을 수 있느냐로 정해집니다.

값이 하나도 없는 타입

답은 throw 식의 타입인 Nothing 에 있습니다. Nothing 은 코틀린에 내장된 타입으로, 정상적으로 끝나지 않는 식의 타입입니다. 늘 예외를 던지거나 끝없이 도는 코드가 여기에 해당합니다. 끝나지 않으니 실제로 돌려주는 값은 하나도 없습니다.

값이 없으니 어떤 타입이 필요한 곳에 두어도 모순이 생기지 않습니다. 그래서 Nothing 은 모든 타입의 하위 타입입니다. 코틀린 타입 계층의 맨 위에는 자바 Object 에 해당하는 Any 가 있습니다.

아래 그림에서 화살표는 위에서 아래로 「이것의 하위 타입」을 가리킵니다. 화살표가 모두 Nothing 으로 모이는 것이 「모든 타입의 하위 타입」이라는 말의 그림입니다.

flowchart TD
    A["Any · 자바 Object 에 해당"] --> S["String"]
    A --> I["Int"]
    A --> U["Unit · 6절"]
    S --> N["Nothing · 값이 하나도 없다"]
    I --> N
    U --> N

그림은 null 을 못 담는 타입만 그렸습니다. String? 처럼 null 을 담는 타입까지 넣으면 맨 위는 Any? 입니다.

String 이 필요한 곳에도, Int 가 필요한 곳에도 Nothing 이 들어갑니다. 4절의 ?: 도 그렇습니다. String 자리에 Nothing 을 넣어도 되니, 둘을 함께 담을 자리를 String 보다 넓게 잡을 까닭이 없습니다. 그래서 name 은 String 으로 남습니다.

Nothing 을 돌려주는 함수

Nothing 은 함수 반환 타입으로도 씁니다. Fail.kt 의 fail 은 늘 예외를 던지는 함수입니다.

Kotlin
fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

fun check(n: Int): Int {
    val size = if (n >= 0) n else fail("negative")
    return size
}
  • : Nothing — 이 함수는 절대 정상적으로 돌아오지 않는다는 선언입니다
  • if (n >= 0) n else fail("negative") — 한쪽 가지는 Int, 다른 쪽은 Nothing 입니다. 식 전체의 타입은 Int 가 되어 size 에 들어갑니다

자바에는 이런 타입이 없습니다. 늘 예외를 던지는 일을 메서드 하나로 빼 두고 싶어도, 그 메서드를 void 로 만들면 삼항 연산자 가지에 넣을 수 없습니다. bad type in conditional expression 에러가 납니다.

그래서 자바에서는 그 메서드가 예외를 던지지 않고 예외 객체를 돌려주게 만듭니다. 던지는 것은 부르는 쪽에서 throw fail(...) 로 합니다.

JVM(Java Virtual Machine), 곧 자바 가상 머신에도 Nothing 은 없습니다. 클래스 파일에서 무엇이 됐는지 javap 로 봤습니다. javap -p 는 JDK 에 딸린 도구로, 클래스 파일에 든 선언을 자바 선언문 모양으로 보여 줍니다(-p 는 private 까지).

터미널
$ kotlinc Fail.kt -d out
$ javap -p out/FailKt.class
Compiled from "Fail.kt"
public final class FailKt {
  public static final java.lang.Void fail(java.lang.String);
  public static final int check(int);
}

fail 의 반환 타입이 java.lang.Void 입니다. Void 는 인스턴스를 만들 수 없는 자바 클래스입니다. 값이 하나도 없는 Nothing 을 JVM 의 타입 가운데 뜻이 가장 가까운 것으로 옮긴 결과입니다.

클래스 파일을 자바 코드 모양으로 되돌리는 일을 디컴파일(decompile)이라 부릅니다. 같은 파일을 디컴파일러 CFR 0.152 로 되돌리면, check 안의 fail("negative") 호출 바로 뒤에 throw new KotlinNothingValueException(); 줄이 붙어 있습니다. 소스에 없는 줄입니다. 반환 타입이 Nothing 인 함수가 혹시 돌아오더라도 다음 줄로 넘어가지 못하게 컴파일러가 넣은 줄입니다.

코틀린의 throw 는 타입이 Nothing 인 식입니다. Nothing 은 모든 타입의 하위 타입이라 ?: 의 오른쪽이나 if 의 한 가지처럼 값이 필요한 곳에 들어갑니다.

6. Unit 이 void 를 대신한다

throw 가 Nothing 이라면, 값을 돌려주지 않는 평범한 함수의 타입은 무엇일까요. 자바라면 void 입니다. 이 절에서는 코틀린의 Unit 을 자바 void 와 맞대 봅니다.

Unit 은 값이 딱 하나뿐인 타입입니다. 그 하나뿐인 값의 이름도 Unit 입니다. 공식 문서의 표현으로는 「Unit is a type that has only one value, also called Unit.」 입니다(코틀린 문서 「Functions」).

중괄호 { } 로 본문을 쓰고 반환 타입을 안 적은 함수가 돌려주는 것이 이 Unit 입니다.

Nothing 과 헷갈리기 쉬우니 갈라 둡니다. Nothing 은 값이 하나도 없는 타입입니다. Unit 은 의미 없는 값이 하나 있는 타입입니다.

코틀린 — Unit 은 변수에 담긴다

Log.kt 는 중괄호 본문에 반환 타입을 안 적은 log 와 : Unit 을 적은 logUnit 을 나란히 두었습니다. main 에서는 log 의 결과를 변수에 받아 찍습니다.

Kotlin
fun log(message: String) {
    println(message)
}

fun logUnit(message: String): Unit {
    println(message)
}

fun main() {
    val u = log("hi")      // hi
    println(u)             // kotlin.Unit
}

log 가 돌려준 것이 무엇인지 u 를 찍어 확인합니다.

터미널
$ kotlinc Log.kt -d out
$ kotlin -cp out LogKt

u 에는 kotlin.Unit 이 담겼습니다. 함수 호출은 두 언어 모두 식입니다. 다만 자바의 void 호출은 남는 값이 없고, 코틀린 함수는 Unit 이라는 값 하나를 남깁니다. 그래서 변수에 받을 수 있습니다.

이번에는 클래스 파일의 모양입니다. javap -p 로 선언을 봤습니다.

터미널
$ javap -p out/LogKt.class
Compiled from "Log.kt"
public final class LogKt {
  public static final void log(java.lang.String);
  public static final void logUnit(java.lang.String);
  public static final void main();
  public static void main(java.lang.String[]);
}

log 와 logUnit 둘 다 void 입니다. 반환 타입을 안 적든 : Unit 을 적든 클래스 파일에서는 같습니다. 인자 없는 main() 옆의 main(String[]) 은 JDK 21 까지의 실행기가 찾는 main(String[]) 모양에 맞추려고 컴파일러가 덧붙인 메서드입니다(KB 01 첫 파일 — fun main 한 줄이 JVM 에서 도는 법).

자바 — void 는 값이 아니다

같은 일을 자바로 하면 void 메서드 log 의 결과를 Object u = log("hi"); 로 받는 모양이 됩니다. Object 는 무엇이든 담는 타입인데도 이 줄을 담은 Log.java 는 컴파일되지 않습니다.

터미널
$ javac -d jout Log.java
Log.java:7: error: incompatible types: void cannot be converted to Object
        Object u = log("hi");
                      ^
1 error

log("hi") 호출은 식이지만, void 라 끝난 뒤 남는 값이 없어 Object 로도 바꿀 수 없습니다. 코틀린의 Unit 은 값이 하나 있는 진짜 타입이라 val 에 담깁니다.

코틀린의 Unit 은 클래스 파일에서 void 가 됩니다. 그런데도 소스에서는 val 에 담겨 kotlin.Unit 으로 찍힙니다.

7. 한 장 요약

코틀린에는 삼항 연산자가 없다. if·try·throw 가 값을 남기는 식이기 때문이다. throw 의 타입은 모든 타입의 하위 타입인 Nothing, 값이 없는 함수의 타입은 클래스 파일에서 void 가 되는 Unit, 그리고 자바에서 식이던 대입은 코틀린에서 문이다.

질문 자바 코틀린 확인한 방법
조건으로 값 고르기 c ? x : y if (c) x else y. ? : 는 문법 에러 Ternary.kt · Max.kt
?: 없음 왼쪽이 null 이면 오른쪽 값 Elvis.kt
대입 x = 5 식. (x = 5) + 1 이 6 문. only expressions are allowed in this context Assign
if 를 = 오른쪽에 illegal start of expression 된다. 블록의 마지막 줄이 값 Grade
else 없는 if 문이라 늘 된다 값으로 쓰면 'if' must have both main and 'else' branches NoElse.kt
try 에서 값 정하기 final 이면 might already have been assigned val n = try { ... } catch ... { ... } Parse
finally 의 마지막 줄 해당 없음 값이 되지 않고 expression is unused 경고 Finally.kt
삼항 연산자 가지에 throw illegal start of expression. switch 식 가지만 허용 ?: throw 한 줄로 쓴다 Require · Switch.java
늘 던지는 함수 그런 타입이 없어 예외 객체를 돌려받아 throw fail(...) 반환 타입 Nothing. 클래스 파일에서 java.lang.Void Fail.kt · javap -p
값 없는 함수의 타입 void. 변수에 못 담는다 Unit. 클래스 파일은 void, 담으면 kotlin.Unit Log · javap -p

관련 항목

문과 식을 가르는 코틀린 문법 요소

문장과 식 · if 식 · try 식 · throw 식 · 대입문 · 블록 · 엘비스 연산자 · 삼항 연산자

코틀린 타입 계층에서 Nothing 과 Unit 이 서는 층

Nothing · Unit · Any · 하위 타입 · 바텀 타입 · 널 가능 타입

if 식과 try 식이 번역되는 자바의 문법과 규칙

void · java.lang.Void · final 지역 변수 · 확정 대입 · switch 식 · finally 블록

식으로 쓸 때 컴파일러가 내는 에러와 경고

컴파일 에러 · 쓰이지 않는 식 · NumberFormatException · IllegalArgumentException · KotlinNothingValueException

컴파일 결과를 열어 보는 도구

javap · 디컴파일 · 디컴파일러 · CFR · 클래스 파일

이 편에 쓰인 언어와 개발 도구

코틀린 · 자바 · kotlinc · javac · 컴파일러 · JDK · 코틀린 표준 라이브러리