IT/Kotlin

Kotlin - Smart cast, Backing field, etc

Saint95Jeon 2024. 4. 19. 16:09
728x90

값을 반환하면 식(expression), 반환하지 않으면 문(statement).

fun main() {
    var number: Int = if (condition1) { //if문이 값을 반환하므로 식.
        10
    } else {
        20
    }

    if (condition2) { //if문이 값을 반환하지 않으므로 문.
        number = 10
    } else {
        number = 20
    }

    println("결과: $number")
}

대입 연산자는 자바에서는 식이었으나 코틀린에서는 문이다. 즉, 자바에서의 대입 연산자는 변수에 값을 할당하면서 동시에 할당된 값을 반환하지만 코틀린에서는 변수에 값을 할당만 하고 값을 반환하지 않는다.

fun main() {
    var num1: Int = 1
    var num2: Int = 2
    
    num1 = num2 = 3 //코틀린의 대입 연산자는 식이 아니라 문이기 때문에 값을 반환하지 않아 해당 연산이 불가능하다.
}

등호와 식으로 이뤄진 함수를 식이 본문인 함수라 부르고, 중괄호로 둘러싸인 함수를 블록이 본문인 함수라 부른다. 식이 본문인 함수는 반환 타입을 생략해도 되지만, 블록이 본문인 함수는 반환 값이 있다면 return문과 함께 타입을 명시해야 한다.

fun add(a: Int, b: Int) = a + b //식이 본문인 함수. 반환 값을 생략할 수 있다.

fun multiply(a: Int, b: Int): Int { //블록이 본문인 함수. 반환 값이 있으므로 return문과 함께 타입을 명시한다.
    return a * b 
}

val 키워드를 사용한 변수를 단 한번만 실행되는 블록을 통해 초기화한다면 조건에 따라 다른 값을 가질 수 있다.

fun main() {
    val number: Int = if (condition) {
        10
    } else {
        20
    }

    println("결과: $number")
}

val 키워드를 사용한 변수의 참조 값은 변경할 수 없지만 참조하는 객체의 내부 값은 변경될 수 있다.

fun main() {
    val numbers = mutableListOf(1, 2, 3, 4, 5)
    println("초기 리스트: $numbers") //[1, 2, 3, 4, 5]
    
    numbers.add(6)
    println("변경된 리스트: $numbers") //[1, 2, 3, 4, 5, 6]

    numbers = mutableListOf(6, 7, 8, 9, 10) //에러 발생.
}

var 키워드를 사용한 변수의 값은 변경이 가능하지만 타입은 바뀌지 않는다.

fun main() {
    var number: Int = 10
    println("초기값: $number") //10

    number = 20 
    println("변경된 값: $number") //20
    
    number = "Hello world" //에러 발생.
}

코틀린 파일에서 자바 클래스 필드의 접근은 코틀린 문법과 자바 문법 둘 다 사용하여 접근이 가능하다. 반대로 자바 파일에서 코틀린 클래스 프로퍼티의 접근은 코틀린 문법을 사용할 수 없고 자바 문법으로만 접근이 가능하다.

 

1. 자바에서 정의한 클래스에 대해 코틀린 파일에서 객체를 생성하여 필드에 접근하는 방법.

//Person.java

public class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}
//Main.kt

fun main() {
    val person = Person("James")
    println(person.name)
    println(person.getName())
    //코틀린 문법과 자바 문법 둘 다 사용하여 접근이 가능하다.
}

 

2. 코틀린에서 정의한 클래스에 대해 자바 파일에서 객체를 생성하여 프로퍼티에 접근하는 방법.

//Person.kt

class Person(val name: String)
//Main.java

public class Main {
    public static void main(String[] args) {
        System.out.println(new Person("James").getName());
        //getName()이 아닌 name으로는 접근할 수 없다.
    }
}

*이름이 is로 시작하는 프로퍼티의 게터에는 get이 붙지 않고 원래 이름을 그대로 사용하며, 세터에는 is를 set으로 바꾼 이름을 사용한다.


코틀린의 프로퍼티는 자동으로 게터와 세터를 제공하는데, 이때 뒷받침 필드(Backing field)가 사용된다.

var name: String = "Kotlin"
    get() {
        return field //what if name itself?
    }
    set(value) {
        field = value 
    }

fun main() {
    println(name)
}

 

만약 게터와 세터에 뒷받침 필드 field가 아닌 name을 쓰면 어떻게 될까?

 

main함수에서 프로퍼티 name에 접근하면 값을 얻기 위한 게터가 호출된다.

 

게터 내부를 실행하면서 name을 반환하는데, 이는 다시 한 번 게터의 호출을 야기한다. 

 

즉, 값을 반환하지 못하고 계속 게터가 호출되어 무한루프에 빠지게 된다.

 

따라서 게터를 호출하지 않고 프로퍼티를 대신하는 변수가 필요한데, 바로 뒷받침 필드가 이런 역할을 한다.


클래스의 특성을 표현하기 위한 방법으로 메서드가 아닌 커스텀 접근자 프로퍼티를 고려해 볼 수 있다. 프로퍼티에 접근할 때마다 게터가 값을 매번 다시 계산하여 반환한다.

class Rectangle(var width: Double, var height: Double) {
    val isSquare: Boolean
        get() {
            return width == height
        }
}

fun main() {
    val rectangle = Rectangle(1.0, 2.0)
    println(rectangle.isSquare) //false
    rectangle.height = 1.0
    println(rectangle.isSquare) //true
}

enum 클래스 선언 방법.

enum class Colour(
    val r: Int, val g: Int, val b: Int
) {
    Red(255, 0, 0), Green(255, 165, 0),
    Blue(0, 0, 255); //상수들의 정의가 끝나면 세미콜론(;)을 붙여야 한다.

    fun rgb() = (r * 256 + g) * 256 + b
}

fun main() {
    println(Colour.Red.rgb())
}

인자가 없는 when 식은 각 분기의 조건이 불리언 결과를 계산하는 식이어야 한다.

class Rectangle(var width: Double, var height: Double) {
    val isSquare: Boolean
        get() {
            return width == height
        }
}

fun main() {
    val rectangle = Rectangle(1.0, 1.0)
    val result = when {
        rectangle.isSquare -> "Square!"
        else -> "Not square.."
    }
}

 

다양한 타입의 검사는 when 식을 고려해 볼 수 있다.

fun processFruit(fruit: Any) {
    when (fruit) {
        is String -> println("이 과일은 문자열로 표현됩니다.")
        is Int -> println("이 과일은 정수로 표현됩니다.")
        is Double -> println("이 과일은 소수로 표현됩니다.")
        else -> println("이 과일은 알 수 없는 형식으로 표현됩니다.")
    }
}

코틀린 컴파일러는 is로 타입 검사 후, 해당 타입으로 자동 형변환을 시켜주기 때문에 따로 형변환 없이 해당 타입의 프로퍼티를 사용할 수 있는데 이를 스마트 캐스트라고 한다.

fun process(value: Any) {
    if (value is String) {
        println(value.length) //컴파일러가 value를 String 타입으로 캐스트 해준다.
    }
}

fun main() {
    process("Hello")
}

 

단, 스마트 캐스트는 타입을 검사한 이후에 값이 바뀌지 않는 경우에만 작동한다.

 

var를 사용해도 초기값에 해당하는 타입으로 고정되고 커스텀 접근자를 사용해도 반환 타입이 고정인데 왜 타입에 안전하지 못하다고 하는 걸까?

 

아래의 코드를 보자.

open class Animal
class Dog: Animal() {
    fun bark() = "bark!"
}
class Cat: Animal() {
    fun meow() = "meow!"
}

fun main() {
    var animal: Animal = Dog()

    if(animal is Dog) {
        animal = Cat()
        animal.bark() //run time error could have occured if a compiler allowed the smart cast.
    }
}

 

var(mutable) animal 변수의 타입이 Animal이고, Dog 인스턴스를 참조하고 있다.

 

만약 if문에서 Dog와의 타입검사 후, 스마트 캐스트가 적용되었다고 가정해 보자.

 

이제 if문 블록 내에서는 animal이 Dog로 취급된다.

 

따라서 어떤 형변환 없이 animal.bark() 함수의 호출이 가능해진다.

 

만약 animal이 Cat 인스턴스를 참조한다면 animal은 더 이상 if문 내에서 Dog로 취급되지 않아야 한다.

 

하지만 컴파일러는 여전히 if문 블록 내에서 animal을 Dog로 취급하고, animal.bark() 함수의 호출을 허용한다.

 

결국 실행 시에 런타임 에러가 발생하게 된다.

 

따라서 var로 선언한 프로퍼티와 커스텀 접근자는 타입에 안전할 수 없다.

 

참고로 원하는 타입으로의 명시적 캐스팅은 as 키워드를 사용한다.


범위를 이용한 for의 사용

// 1부터 5까지 반복
for (i in 1..5) {
    println(i)
}

// 거꾸로 반복
for (i in 5 downTo 1) {
    println(i)
}

// 2씩 증가하면서 반복
for (i in 1..5 step 2) {
    println(i)
}

// 0부터 9까지 반복 (10은 포함되지 않음)
for (i in 0 until 10) {
    println(i)
}

 

컬렉션을 이용한 for의 사용

//리스트의 인덱스와 값을 함께 사용
val list = listOf("apple", "banana", "orange")

for ((index, value) in list.withIndex()) {
    println("인덱스: $index, 값: $value")
}

//맵의 키와 값 모두 사용
val map = mapOf("apple" to 1, "banana" to 2, "orange" to 3)

for ((key, value) in map) {
    println("키: $key, 값: $value")
}

in을 사용하여 어떤 값이 컬렉션 혹은 범위에 포함되는지 확인할 수 있다.

//컬렉션에 값이 포함되어 있는지 확인.
if (3 in listOf(1, 2, 3, 4, 5)) {
    println("3은 리스트에 포함되어 있습니다.")
} else {
    println("3은 리스트에 포함되어 있지 않습니다.")
}

//범위에 값이 포함되어 있는지 확인.
if (7 in 1..10) {
    println("7 범위에 포함되어 있습니다.")
} else {
    println("7 범위에 포함되어 있지 않습니다.")
}

함수를 제외한 모든 블록 식(if, when, try-catch, etc)의 마지막 값이 결과다.

728x90