흰 스타렉스에서 내가 내리지

Kotlin 기초 본문

Kotlin

Kotlin 기초

주씨. 2023. 9. 5. 15:04
728x90

#변수

// 불변 변수
val a = 10

// 가변 변수
var a = 10
var a: Int = 10

 

#문자열

import java.util.Date

fun main(){
    val name = readLine()
    println("Hello, $name! Today is ${Date()}")
}

 

#배열

    val a = emptyArray<String>()
    val b = arrayOf("hello", "world")
    val c = arrayOf(1, 4, 9)

    val operations = charArrayOf('+', '-', '*', '/', '%')
    
    """
    중괄호 안에 들어있는 언어 요소를 람다라고 부른다. 
    인덱스를 표현하는 볍ㄴ수로 자동으로 선언되는 it를 사용한다. it 이외의 변수를 사용할 경우 컴파일 에러
    """.trimIndent()
    val squares = IntArray(10){ (it + 1) * (it + 1) }   
    println(squares.contentToString())  //[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
    
    
    """
        배열을 생성하고 나면 그 길이를 바꿀 수 없지만, + 연산을 사용해 원소를 추가한 새로운 배열을 만들 수는 있다.
    """.trimIndent()
    val d = intArrayOf(1, 2, 3) + 4   // 1, 2, 3, 4
    val e = intArrayOf(1, 2, 3) + intArrayOf(5, 6)   // 1, 2, 3, 5, 6
    
    """
        배열을 비교하고 싶으면 contentEquals() 함수를 사용한다.
    """.trimIndent()
    intArrayOf(1, 2, 3).contentEquals(intArrayOf(1, 2, 3)) // true

 

#함수

"""
코틀린 함수 파라미터는 무조건 불변이다.
"""
fun increment(n: Int): Int{
    return n++  // Error: can't change immutable variable
}

"""
어떤 함수가 단일 식으로만 구현될 수 있다면 return 키워드와 블록을 만드는 중괄호를 생략할 수 있다. 
"""
fun circleArea(radius: Double) = PI * radius * radius


"""
함수 정의에서 반환값 타입을 지정하지 않으면 코틀린은 Unit 함수를 정의한다고 가정한다.
Java의 void와 같은 개념
"""
fun prompt(name: String) = println("Hello, $name!")
fun prompt(name: String): Unit = println("Hello, $name!")
"""
위치 기반 인자와 이름 붙은 인자
"""
fun rectangleArea(width: Double, height: Double): Double{
    return width * height
}

rectangleArea(width = w, height = h)


fun swap(s: String, from: Int, to: Int): String{
    val chars = s.toCharArray() // 배열로 변환
    
    val tmp = chars[from]
    chars[from] = chars[to]
    chars[to] = tmp
    return chars.concatToString()  // 문자열로 다시 변환
}

fun main(){
    println(swap("Hello", 1, 2))  // Hlelo
    println(swap("Hello", to = 3, from = 0))  // lelHo
}
"""
디폴트 파라미터
"""
fun f(n: Int = 10) = n

fun main(){
    println(f())  // 10
}


"""
가변 인자
"""
fun printSorted(vararg items: Int){
    items.sort()
    println(items.contentToString())
}

fun main(){
    printSorted(6, 2, 10, 1)    // [1, 2, 6, 10]
    
    """
        스프레드 연산자인 *를 사용하면 배열을 가변 인자 대신 넘길 수 있다.
    """.trimIndent()
    val numbers = intArrayOf(6, 2, 10, 1)
    printSorted(*numbers)
    printSorted(numbers)  // Error: passing IntArray instead of Int
}

 

코틀린 함수는 정의된 위치에 따라 세 가지로 구분할 수 있다.

- 파일에 직접 선언된 최상위 함수

- 어떤 타입 내부에 선언된 멤버 함수

- 다른 함수 안에 선언된 지역 함수

 

private, internal (protected)

fun main(){
    """
    함수 내 함수
    """.trimIndent()
    fun printSorted(vararg items: Int){
        items.sort()
        println(items.contentToString())
    }
    
    printSorted(6, 2, 10, 1)    // [1, 2, 6, 10]
    
}

 

 

#조건문

fun main(){
    """
    Java의 if문과 Kotlin의 if문 차이
    Kotlin은 if를 식으로 사용할 수 있다. 
    """.trimIndent()
    fun max(a: Int, b: Int) = if(a > b) a else b

    """
    if의 두 가지 중 어느 쪽이 블록인 경우에도 마찬가지다.
    이 겨웅에는 블록 맨 끝에 있는 식의 값이 블록 전체의 값이 된다.
    """.trimIndent()
    val s = readLine()!!
    val i = s.indexOf("/")

    // 10/3 같은 문자열 /를 기준으로 10과 3으로 나눠서 나눗셈을 수행한다.
    val result = if (i >= 0){
        val a = s.substring(0, i).toInt()
        val b = s.substring(i + 1).toInt()
        (a/b).toString()
    }else ""
}

 

코틀린은 자바와 달리 3항 연산자가 없다.

하지만 if를 식으로 쓸 수 있다는 점이 이 단점을 대부분 상쇄해준다.

 

    """
        when 문과 여럿 중에 하나 선택하기
    """.trimIndent()
    fun hexDigit(n: Int): Char{
        if (n in 0..9) return '0' + n
        else if (n in 10..15) return 'A' + n - 10
        else return '?'
    }
    
    // 코틀린 when은 조건을 만족하는 가지만 실행하고 절대 폴스루를 하지 않는다.
    fun hexDigit2(n: Int): Char{
        when{
            n in 0..9 -> return '0' + n
            n in 10..15 -> return 'A' + n - 10
            else -> return '?'
        }
    }

    fun hexDigit3(n: Int): Char{
        when (n) {
            in 0..9 -> return '0' + n
            in 10..15 -> return 'A' + n - 10
            else -> return '?'
        }
    }

 

 

#예외처리

    """
        예외 던지기
    """.trimIndent()
    fun sayHello(name: String){
        try{
            val message =
                if (name.isNotEmpty()) "Hello, $name"
                else throw IllegalArgumentException("Empty name")
        }catch (e: java.lang.IllegalArgumentException){
            val message = "ㅋㅋ"
        }
    }


    """
    자바와 코틀린의 try문에서 가장 크게 다른 점은 코틀린 try가 식이라는 것이다.
    이 식의 값은 (예외가 발생하지 않은 경우) try 블록의 값이거나 예외를 처리한 catch 블록의 값이 된다.
    """.trimIndent()
    fun readInt(default: Int) = try{
        readln().toInt()
    }catch (e: NumberFormatException){
        default
    }

 

 

#클래스

private class Person{
    var name: String = "Joos"
    var age: Int = 0

    fun showMe(){
        println("${this.name} : ${this.age}")
    }
}

fun main(){
    val person = Person()
    println(person.showMe())
}
class Person(firstName: String, familyName: String) {
    val fullName = "$firstName $familyName"

    init{
        println("Created new Person instance: $fullName")
    }
}

fun main(){
    val person = Person("seungwoo", "joo")  // Created new Person instance: seungwoo joo
}
"""
코틀린은 init 블록 안에서 프로퍼티를 초기화하는 것도 허용한다.
"""
class Person(fullName: String){
    val firstName: String
    val familyName: String
    
    init{
        val names = fullName.split(" ")
        if(names.size != 2){
            throw IllegalArgumentException("Invalid name: $fullName")
        }
        firstName = names[0]
        familyName = names[0]
    }
}


fun main(){
    val person = Person("John Doe")
    println(person.firstName)	// John
}
"""
주생성자 파라미터를 프로퍼티 초기화나 init 블록 밖에서 사용할 수는 없다.
예를 들어 멤버 함수 내부에서는 firstName을 사용할 수 없기 때문에 다음 코드는 잘못된 코드다.
"""
class Person(firstName: String, familyName: String){
    val fullName = "$firstName $familyName"

    fun printFirstName(){
        println(firstName) // Error: firstName is not available here
    }
}

"""
코틀린은 간단하게 생성자 파라미터의 값을 멤버 프로퍼티로 만들 수 있는 방법을 제공한다.
기본적으로 생성자 파라미터 앞에 val이나 var 키워드를 덧붙이면, 자동으로 해당 생성자 파라미터로 초기화되는 프로퍼티를 정의한다.
이 때 파라미터 이름을 프로퍼티 초기화나 init 블록 안에서 참조하면 생성자 파라미터를 가리키고, 다른 위치에서 참조하면 프로퍼티를 가리키게 된다. 
"""
class Person(val firstName: String, familyName: String){
    val fullName = "$firstName $familyName"

    fun printFirstName(){
        println(firstName)
    }
}
// 코틀린 클래스에서는 본문을 아예 생략할 수 있다. 실제 인텔리J 코틀린 플러그인은 이런 코딩스타일을 권장한다.
class Person(val firstName: String, val familyName: String = "")

// 함수와 마찬가지로 디폴트 값과 vararg를 생성자 파라미터에 사용할 수 있다.
class Room(vararg val persons: Person){
    fun showNames(){
        for(person in persons) println(person.firstName)
    }
}


// 부생성자 - constructor 키워드
class Person{
    val firstName: String
    val familyName: String
    
    constructor(firstName: String, familyName: String){
        this.firstName = firstName
        this.familyName = familyName
    }
    
    constructor(fullName: String){
        val names = fullName.split(" ")
        if(names.size != 2) throw IllegalArgumentException("Invalid name: $fullName")
        
        firstName = names[0]
        familyName = names[1]
    }
}

 

 

#null 가능성

Java에서는 함수 전달인자가 null이면 런타임에 NPE가 나지만, Kotlin에서는 컴파일 시점에 NPE가 난다.

 

코틀린 타입 시스템에는 널 값이 될 수 있는 참조 타입과 넣 값이 될 수 없는 참조 타입을 확실히 구분해주는 큰 장점이 있다. 

이 기느은 널 발생 여부를 컴파일 시점으로 옮겨주기 때문에 악명 높은 NullPointerException 예외를 상당 부분 막을 수 있다. 

 

코틀린에서 널이 될 수도 있는 값을 받는 함수를 작성하려면, 

파라미터 타입 뒤에 물음표(?)를 붙여서 타입을 널이 될 수 있는 타입으로 지정해야 한다.

fun isBooleanString(s: String?) = s=="false" || s=="true"

코틀린에서 String? 같은 타입은 널이 될 수 있는 타입(nullable type)이라고 불린다. 

"""
    nullable type은 원래 타입에 들어있는 어떤 프로퍼티나 메서드도 제공하지 않는다.
    아래 예시를 보면, String? 타입에 isEmpty()나 iterator() 메서드가 없기 때문에 에러가 발생한다. 
    """.trimIndent()
    fun isLetterString(s: String?): Boolean{
        if (s.isEmpty()) return false       // error
        for(ch in s) if(!ch.isLetter()) return false    //error
    }
    
    """
    널이 될 수 있는 값을 처리하는 가장 직접적인 방법은 해당 값을 조건문을 사용해 null과 비교하는 것이다.
    null에 대한 검사를 추가하면 코드가 어떤 이유에서인지 컴파일된다. 
    스마트캐스트(Smart Cast)라고 불리는 코틀린 기능이 이런 일을 가능하게 해준다. 
    """.trimIndent()
    fun isLetterString(s: String?): Boolean{
        if (s == null) return false
        
        // s는 여기서 널이 될 수 없다
        if (s.isEmpty()) return false
        return true
    }
    
    """
    스마트 캐스트는 when이나 루프 같은 조건 검사가 들어가는 다른 문이나 식 안에서도 작동한다.
    """.trimIndent()
    fun describeNumber(n: Int?) = when(n){
        null -> "null"
        // 아래에 있는 가지에서 n은 널이 될 수 없다.
        in 0..10 -> "small"
        in 11..100 -> "large"
        else -> "out of range"
    }
    
    """
    스마트 캐스트를 실행하려면 대상 변수의 값이 검사 지점과 사영 지점 사이에서 변하지 않는다고 
    컴파일러과 확실할 수 있어야 한다. 특히 지금까지 살펴본 불변 지역 변수는 초기화 후 변경되지 
    않으므로 항상 제한 없이 스마트 캐스트를 쓸 수 있다.
    (객체의) 가변 프로퍼티에 대해서는 절대 스마트 캐스트를 적용할 수 없다. 일반저긍로 언제든
    코드의 다른 부분에서 프로퍼티 값을 바꿀 수 있기 때문이다. 
    """.trimIndent()

 

 

#안전한 호출 연산자

널이 될 수 있는 타입의 값에 대해서는 그에 상응하는 널이 될 수 없는 타입의 값에 있는 메서드를 사용할 수 없다고 이미 설명해싿., 

하지만 특별한 안전한 호출 연산 (safe call)을 사용하면 이런 제약을 피할 수 있다. 

fun readInt() = readLine()!!.toInt()

콘솔 I/O 를 사용한다면 이 함수는 잘 작동한다. 하지만 프로그램이 파일을 표준 입력에 파이프로 연결하면, 파일이 비어있는 경우 이 함수가 KotlinNullPointerException 예외를 발생시키면서 실패할 수 있다. 

안전한 호출 연산자를 사용하면 다음 형태로 코드를 다시 작성할 수 있다. 

fun readInt() = readLine()?.toInt()

위 코드는 기본적으로 다음 함수와 같다.

fun readInt(): Int?{
    val tmp = readLine()

    return if (tmp != null) tmp.toInt() else null
}

 

'수신 객체가 널이 아닌 경우에는 의미 있는 일을 하고, 수신 객체가 널인 경우에는 널을 반환하라' 라는 패턴은 실전에서 꽤 많이 발생한다. 

따라서 안전한 호출을 사용하면 불필요한 if 식과 임시 변수 사용을 줄여서 코드를 단순화 할 수 있다.

 

한 가지 유용한 숙어는 안전한 호출 연산자를 연쇄시켜 다음과 같이 쓰는것이다.

println(readLine()?.toInt()?.toString(16))

 

 

#엘비스 연산자

널이 될 수 있는 값을 다룰 때 유용한 연산자로 널 복합 연산자(null coalescing operator)인 ?:을 들 수 있다. 

이 연산자를 사용하면 널을 대신할 디폴트 값을 지정할 수 있다. 

    fun sayHello(name: String?){
        println("Hello, " + (name ?: "Unknown"))
    }

    sayHello("John")    // Hello, John
    sayHello(null)      // Hello, Unknown

왼쪽 피연산자가 널이 아닐 경우에는 왼쪽 피연산자의 값이고, 왼쪽 피연산자가 널일 경우에는 오른쪽 피연산자의 값이다.

 

 

#lateinit

lateinit 표시가 붙은 프로퍼티는 값을 읽으려고 시도할 때 프로그램이 프로퍼티가 초기화됐는지 검사해서 초기화되지 않은 경우 UninitializedPropertyAccessException을 던진다.

class Content{
    lateinit var text: String

    fun loadFile(file: File){
        text = file.readText()
    }
}

lateinit 프로퍼티의 경우 항상 자동으로 접근자가 생성되기 때문에 프로그래머가 직접 커스텀 접근자를 정의할 수 없다. 

 

 

 

#커스텀 접근자

"""
아래는 프로퍼티 값을 읽을 때 사용하는 커스텀 게터(getter)를 정의한다.
프로퍼티에 명시적으로 field를 사용하는 디폴트 접근자나 커스텀 접근자가 하나라도 있으면 backing field가 생성된다.
""".trimIndent()
class Person(val firstName: String, val familyName: String){
    val fullName: String
        get(): String{
            return "$firstName $familyName"
        }
}
"""
위 코드에서, fullName 프로퍼티는 읽을 때 마다 다시 계산된다. 
funllName은 뒷받침하는 필드(backing field)가 없기 때문에 클래스 인스턴스에서 전혀 메모리를 차지하지 않는다.
즉, 기본적으로 fullName은 프로퍼티 형태인 함수와 같다.

직접 뒷받침하는 필드에 접근하는 것.
프로퍼티가 어떤 저장된 값을 사용하지만 프로퍼티에 대한 접근을 커스텀화해야 할 경우, 뒷받침하는 필드에 접근할 수 있으면 유용하다.
""".trimIndent()
class Person2(val firstName: String, val familyName: String, age: Int){
    val age: Int = age
        get(): Int{
            println("Accessing age")
            return field
        }
}

println(Person("Sinner", "Jannik").fullName)        // Sinner Jannik
println(Person2("Sinner", "Jannick", 30).age)   // Accessing age, 30

계산에 의해 값을 돌려주는 프로퍼티의 경우 backing field가 필요하지 않다.

 

Custom Getter가 있는 프로퍼티는 약간의 문법적인 차이에도 불구하고 파라미터가 없는 함수처럼 동작한다. 

 

var로 정의하는 가변 프로퍼티에는 값을 읽기 위한 Getter와 값을 설정하기 위한 Setter라는 두 가지 접근자가 있다. 

class Person(var firstName: String, var familyName: String){
    var age: Int? = null
        set(value){
            if(value != null && value <= 0){
                throw IllegalArgumentException("Invalid age: $value")
            }
            field = value
        }

    var fullName: String
        get(): String = "$firstName $familyName"
        set(value){
            val names = value.split(" ")    // 공백으로 구분해 단어를 분리한다.
            if(names.size != 2){
                throw IllegalArgumentException("Invalid full name: '$value'")
            }
            firstName = names[0]
            familyName = names[1]
        }
}

val person = Person("John", "Doe")
person.age = 20     // 커스텀 세터 호출
println(person.age) // 커스텀 게터 호출



"""
프로퍼티 접근자에 별도로 가시성 변경자를 붙일 수도 있다.
""".trimIndent()
class Person2(name: String){
    var lastChanged: Date? = null
        private set     // Person2 클래스 밖에서는 변경할 수 없다.

    var name: String = name
        set(value){
            lastChanged = Date()
            field = value
        }
}

 

 

#객체

코틀린은 어떤 클래스에 인스턴스가 오직 하나만 존재하게 보장하는 싱글턴 패턴을 내장하고 있다. 

코틀린에서는 클래스와 비슷한 방법으로 싱글턴을 선언한다. 

다만 class 대신 object라는 키워드를 사용한다.

object Application{
    val name = "My Application"
    override fun toString() = name
    fun exit(){}
}

fun describe(app: Application) = app.name

fun main(){
    println(Application.name)       // My Application
    println(describe(Application))  // My Application
}

 

 

#객체 식 

자바의 익명 클래스와 아주 비슷하다. 

익명 객체 타입은 지역 선언이나 비공개 선언에만 전달될 수 있다. 

예를 들어 midPoint 함수를 최상위 함수로 정의하면 객체 멤버에 접근할 때 컴파일 오류가 발생한다.

fun main(){
    fun midPoint(xRange: IntRange, yRange: IntRange) = object {
        val x = (xRange.first + xRange.last)/2
        val y = (yRange.first + yRange.last)/2
    }

    val midPoint = midPoint(1..5, 2..6)
    println("${midPoint.x} ${midPoint.y}")  // 3 4
}

 

 

#함수

fun main(){
    """
    고차 함수
    """.trimIndent()
    fun aggregate(numbers: IntArray, op: (Int, Int) -> Int): Int {
        var result = numbers.firstOrNull() ?: throw IllegalArgumentException("Empty Array")

        for(i in 1..numbers.lastIndex) result = op(result, numbers[i])
        // op.invoke(result, numbers[i])
        return result
    }

    val result = aggregate(intArrayOf(1, 2, 3)) { result, op -> result + op }
    println(result)     // 6


    """
    호출 가능 참조
    """.trimIndent()
    fun check(s: String, condition: (Char) -> Boolean): Boolean{
        for(c in s){
            if(!condition(c)) return false
        }

        return true
    }

    fun isCapitalLetter(c: Char) = c.isUpperCase() && c.isLetter()

    println(check("Hello") {c -> isCapitalLetter(c)})   // false
    println(check("Hello") {isCapitalLetter(it)})       // false

    // 코틀린에는 이미 존재하는 함수 정의를 함수 타입의 식으로 사용할 수 있는 더 단순한 방법이 있다.
    // 호출 가능 참조 (callback reference)를 사용하면 된다.
    // 함수 이름 앞에 ::을 붙이면 된다
    println(check("Hello", ::isCapitalLetter))          // false

    """
    ::을 클래스 이름 앞에 적용하면 클래스의 생성자에 대한 호출 가능 참조를 얻는다
    """.trimIndent()
    class Person(var firstName: String, var familyName: String)

    val createPerson = ::Person
    createPerson("John", "Doe")

    """
    주어진 클래스 인스턴스의 문맥 안에서 멤버함수를 호출하고 싶을 때는 바인딩된 호출 가능 참조를 사용한다.
    """.trimIndent()
    class Person2(val firstName: String, val familyName: String){
        fun hasNameOf(name: String) = name.equals(firstName, ignoreCase = true)
    }

    val isJohn = Person2("John", "Doe")::hasNameOf
    println(isJohn("JOHN"))     // true
    println(isJohn("Jake"))     // false


    """
    호출 가능 참조를 직접 호출하고 싶다면 참조 전체를 괄호로 둘러싼 다음에 인자를 지정해야 한다.
    """.trimIndent()
    fun max(a: Int, b: Int) = if (a > b) a else b
    println((::max)(1, 2))      // 2
//    println(::max(1, 2))              // error: this syntax is reserved for future use


    """
    코틀린 프로퍼티에 대한 호출 가능 참조를 만들 수도 있다.
    """.trimIndent()
    val person = Person("John", "Doe")
    val readName = person::firstName.getter
    val writeFamily = person::familyName.setter

    println(readName())     // John
    writeFamily("Smith")
    println(person.familyName)  // Smith

}

 

 

 

#인라인 함수와 프로퍼티

고차 함수와 함숫값을 사용하면 함수가 객체로 표현되기 때문에 성능 차원에서 부가 비용이 발생한다.

코틀린은 함숫값을 사용할 때 발생하는 런타임 비용을 줄일 수 있는 해법을 제공한다. 

기본적인 아이디어는 함숫값을 사용하는 고차 함수를 호출하는 부분을 해당 함수의 본문으로 대체하는 인라인(inline) 기법을 쓰는 것이다. 

인라인 될 수 있는 함수를 구별하기 위해 프로그래머는 inline 변경자를 함수 앞에 붙여야 한다.

inline fun indexOf(numbers: IntArray, condition: (Int) -> Boolean): Int{
    for (i in numbers.indices){
        if(condition(numbers[i])) return i
    }

    return -1
}

fun main(){
    println(indexOf(intArrayOf(4, 3, 2, 1)){it < 3})    // 2
}

indexOf() 함수가 인라인됐기 때문에 컴파일러는 함수 호출을 함수의 본문으로 대체한다. 

즉, main() 함수의 본문으로 대체된다. 

 

인라인 함수를 쓰면 컴파일된 코드의 크기가 커지지만, 지혜롭게 사용하면 성능을 크게 높일 수 있다. 

특히, 대상 함수가 상대적으로 작은 경우 성능이 크게 향상된다.

 

코틀린 버전 1.1부터는 프로퍼티 접근자를 인라인하도록 허용한다. 

이 기능을 사용하면 함수 호출을 없애기 때문에 프로퍼티를 읽고 쓰는 성능을 향상시킬 수 있다

class Person(var firstName: String, var familyName: String){
    val fullName: String
        inline get() = "$firstName $familyName"
}

class Person2(var firstName: String, var familyName: String){
    inline val fullName
        get() = "$firstName $familyName"
}

 

 

 

# 비지역적 제어 흐름

고차 함수를 사용하면 return 문 등과 같이 일반적인 제어 흐름을 깨는 명령을 사용할 때 문제가 생긴다. 

fun forEach(a: IntArray, action: (Int) -> Unit){
    for (n in a) action(n)
}

fun main(){
    forEach(intArrayOf(1, 2, 3, 4)) {
        if (it < 2 || it > 3) return    // error: 'return' is not allowed here
        println(it)
    }
}

이 코드는 컴파일되지 않는다. 

return 문은 디폴트로 자신을 둘러싸고 있는 fun, get, set으로 정의된 가장 안쪽 함수로부터 제어 흐름을 반환시킨다. 

따라서 위의 코드는 main 함수로부터 반환을 시도하는 코드가 된다. 

 

해결방법1. 람다 대신 익명함수 사용

해결방법2. return 문에 문맥 이름 추가

fun forEach(a: IntArray, action: (Int) -> Unit){
    for (n in a) action(n)
}

fun main(){
    forEach(intArrayOf(1, 2, 3, 4)) {
        if (it < 2 || it > 3) return@forEach    
        println(it)     
    }       // 2 3
}

 

람다가 인라인될 경우에는 return 문을 사용할 수 있다.

하지만 우리가 의도한 것과는 약간 다르다.

inline fun forEach(a: IntArray, action: (Int) -> Unit){
    for (n in a) action(n)
}

fun main(){
    forEach(intArrayOf(1, 2, 3, 4)) {
        if (it < 2 || it > 3) return    // 컴파일 O, main에서 반환됨
        println(it)
    }
}

 

 

 

#확장 함수

확장 함수는 어떤 클래스의 멤버인 것처럼 호출할 수 있는 (그러나 실제로는 멤버가 아닌) 함수를 뜻한다.

fun String.truncate(maxLength: Int): String{
    return if (length <= maxLength) this else substring(0, maxLength)
}

fun main(){
    println("hello".truncate(10))   // hello
    println("hello".truncate(3))    // hel
}

일단 정의하고 나면, 다른 String 클래스의 멤버와 마찬가지로 이 함수를 사용할 수 있다.

하지만, 확장 함수 자체는 수신 객체가 속한 타입의 캡슐화를 꺨 수 없다.

예를 들어, 확장 함수는 클래스 밖에 정의된 함수이므로 수신 객체가 속한 클래스의 비공개 멤버에 접근할 수 없다.

 

 

#영역 함수

어떤 식을 계산한 값을 문맥 내부에서 임시로 사용할 수 있도록 해주는 몇 가지 함수가 있다. 

이러한 함수를 영역 함수라고 부른다. 

run, let, with, apply, also

 

1. run()

run() 함수는 확장 람다를 받는 확장 함수이며 람다의 결과를 돌려준다. 

기본적인 사용패턴은 객체 상태를 설정한 다음, 이 객체를 대상으로 어떤 결과를 만들어내는 람다를 호출하는 것이다. 

class Address{
    var zipCode: Int = 0
    var city: String = ""
    var street: String = ""
    var house: String = ""

    fun post(message: String): Boolean{
        println("Message for {$zipCode, $city, $street, $house} : $message")
        return readLine() == "OK"
    }
}

fun main(){
    val isReceived = Address().run{
        zipCode = 123456
        city = "London"
        street = "Baker Street"
        house = "221b"
        post("hello!")
    }

    if(!isReceived){
        println("Message is not delivered")
    }
}

run 함수가 없으면 Address 인스턴스를 담을 변수를 추가해야 한다. 

이로 인해 함수 본문의 나머지 부분에서도 이 변수에 접근할 수 있게 된다. 

 

 

++ 문맥이 없는 run

주로 이 함수를 사용하는 경우는 어떤 식이 필요한 부분에서 블록을 사용하는 것이다.

class Address(var city: String = "", var street: String = "", var house: String = "")

fun Address.asText(): String{
    return "$city, $street, $house"
}

fun main(){
    val address = run {
        val city = readLine() ?: return
        val street = readLine() ?: return
        val house = readLine() ?: return
        Address(city, street, house)
    }

    println(address.asText())
}

 

 

2. with()

with()는 확장 함수 타입이 아니므로 문맥 식을 with의 첫 번째 인자로 전달해야 한다.

일반적으로 사용하는 경우는 문맥 식의 멤버 함수와 프로퍼티에 대한 호출을 묶어 동일한 영역 내에서 실행하는 경우다.

class Address(var city: String = "", var street: String = "", var house: String = "")

fun main(){
    val message = with (Address("London", "Baker Street", "221b")){
        "Address: $city, $street, $house"
    }
    println(message)
}

 

3. let()

let 함수는 run과 비슷하지만 확장 함수 타입의 람다를 받지 않고 인자가 하나뿐인 함수 타입의 람다를 받는다는 점이 다르다. 따라서 문맥 식의 값은 람다의 인자로 전달된다. let의 반환값은 람다가 반환하는 값과 같다.

외부 영역에 새로운 변수를 도입하는 일을 피하고 싶을 때 주로 이 함수를 사용한다. 

class Address(var city: String = "", var street: String = "", var house: String = "")

fun Address.asText(): String{
    return "$city, $street, $house"
}

fun main(){
    Address("London", "Baker Street", "221b").let{
        // 이 안에서는 it 파라미터를 통해 Address 인스턴스에 접근할 수 있음
        println(it.asText())        //London, Baker Street, 221b
    }

    Address("London", "Baker Street", "221b").let{addr ->
        // 이 안에서는 addr 파라미터를 통해 Address 인스턴스에 접근할 수 있음
        println(addr.asText())      // London, Baker Street, 221b
    }
}

 

 

4. apply(), also()

apply() 함수는 확장 람다를 받는 확장 함수이며 자신의 수신 객체를 반환한다. 

이 함수는 일반적으로 run()과 달리 반환값을 만들어내지 않고 객체의 상태를 설정하는 경우에 사용한다.

also() 함수는 apply() 와 달리 인자가 하나 있는 람다를 파라미터로 받는다.

class Address{
    var city: String = ""
    var street: String = ""
    var house: String = ""

    fun post(message: String){}
}

fun main(){
    val message = readLine() ?: return

    Address().apply{
        city = "London"
        street = "Baker Street"
        house = "221b"
    }.post(message)

    Address().also{
        it.city = "London"
        it.street = "Baker Street"
        it.house = "221b"
    }.post(message)
}

 

'Kotlin' 카테고리의 다른 글

마이크로서비스 구축  (0) 2023.09.09
Kotlin 컬렉션  (0) 2023.09.08
Enum Class, Data Class, Inline Class  (1) 2023.09.08
backing field  (1) 2023.09.08