Kotlin v1.6.10 : Functions

HH·2022년 2월 15일
0

Kotlin Docs v1.6.10

목록 보기
1/3

Functions

번역문서 작성일 기준 Last modified: 10 November 2021

코틀린 함수는 fun 키워드로 선언된다.

fun double(x: Int): Int {
    return 2 * x
}

Function usage


함수는 표준 접근법(standard approach)을 사용해 호출된다.

val result = double(2)

member function 호출은 온점 표기법(dot notation)을 사용한다.

Stream().read() // class Stream의 인스턴스를 생성하고 read()를 호출한다.

Parameters


함수 파라미터는 파스칼 표기법을 사용해 정의된다 - 이름, 타입. 파라미터는 콤마를 사용해 구분되고 각 파라미터는 반드시 명시적으로 타입이 지정되어야 한다.

fun powerOf(number: Int, exponent: Int): Int {/*...*/}

함수 파라미터를 선언할 때 후행 쉼표(trailing comma)를 사용할 수 있다.

fun powerOf(
    number: Int,
    exponent: Int, //trailing comma
){/*...*/}

Default arguments


펑션 파라미터는 디폴트 값을 가질 수 있다. 이것은 해당하는 아규먼트를 생략할 때 사용된다. 이는 오버로드를 감소시킨다.

fun read(
    b: ByteArray,
    off: Int = 0,
    len: Int = b.size,
){/*...*/}

default value는 타입 다음에 =를 사용한다.

오버라이딩 메서드는 베이스 메서드와 언제나 같은 디폴트 값을 사용한다. 디폴트 파라미터 값을 가진 메서드를 오버라이딩 할 경우 디폴트 파라미터 값은 반드시 생략되어야 한다.

open class A {
    open fun foo(i: Int = 10) {/*...*/}
}

class B : A() {
    override fun foo(i: Int) {/*...*/} // 디폴트 값이 허용되지 않는다.
}

디폴트 값이 디폴트 값이 없는 파라미터에 선행할 경우, named arguments로 함수를 호출한 경우에만 디폴트 값을 사용할 수 있다.

fun foo(
    bar: Int = 0,
    baz: Int,
) {/*...*/}

foo(baz = 1) //디폴트 값 bar = 0이 사용될 수 있다.

디폴트 파라미터 이후에 오는 마지막 아규먼트가 람다일 경우, 그것을 named argument로써, 또는 괄호 밖에서 넘겨줄 수 있다.

fun foo(
    bar: Int = 0,
    baz: Int = 1,
    qux: () -> Unit,
) {/*...*/}

foo(1) {println("hello")} //기본 값 baz = 1을 사용한다.

foo(qux = {println("hello")}) // 기본값 bar = 0과 baz = 1을 모두 사용한다.

foo {println("hello")} // 기본값 bar = 0과 baz = 1을 모두 사용한다.

Named arguments


함수 호출 시 하나 이상의 아규먼트를 명명할 수 있다. 이것은 함수가 많은 아규먼트를 가지고 있고 값을 아규먼트와 연관시키기 어려울 때 유용하다. 특히 그 값이 불리언이거나 null일 경우 그렇다.

함수 호출에서 명명된 아규먼트를 사용할 경우 순서를 자유롭게 바꿀 수 있다. 그리고 기본 값을 사용하고 싶다면 그 아규먼트를 완전히 배제하면 된다.

아래 함수 reformat()이 기본값을 가진 아규먼트 4개를 가졌다고 가정하자.

fun reformat(
    str: String,
    normalizeCase: Boolean = true,
    upperCaseFirstLetter: Boolean = true,
    divideByCamelHymps: Boolean = false,
    wordSeparator: Char = ' ',
){/*...*/}

이 함수를 호출할 때 모든 아규먼트를 명명할 필요는 없다.

reformat(
    "String!"
    false,
    upperCaseFirstLetter = false,
    divideByCamelHumps = true,
    '_'
)

디폴트 값을 가진 모든 아규먼트를 스킵할 수도 있다.

reformat("This is a long String!")

전체를 생략하지 않고 디폴트 값을 가진 특정 아규먼트를 스킵할 수도 있다. 하지만 처음 스킵된 아규먼트 다음부터는 반드시 모든 아규먼트를 명명해야 한다.

reformat("This is a long String!", upperCaseFirstLetter = false, wordSeperator = '_')

spread operator를 이용해 가변 개수 아규먼트(variable number of arguments (vararg))를 전달할 수 있다.

fun foo(vararg strings: String) {/*...*/}

foo(strings = *arrayOf("a", "b", "c"))

JVM에서: 자바 함수 호출시에는 명명된 아규먼트를 사용할 수 없다. 자바 바이트코드가 언제나 함수 파라미터 이름을 보존하지는 않기 때문이다.


Unit-returning functions


함수가 의미 있는 값을 반환하지 않을 경우 리턴 타입은 Unit이다. Unit은 단일 값 - Unit 만을 가진 타입이다. 이 값은 명시적으로 반환될 필요는 없다.

fun printHello(name: String?): Unit {
    if (name != null) 
        println("Hello $name")
    else
        println("Hi There!")

    // return 또는 return Unit 은 선택사항이다.
}

Unit 리턴 선언 또한 선택사항이다. 위 코드는 다음처럼 작성할 수 있다.

fun printHello(name: String?) { ... }

Single-expression functions


함수가 단일 표현식을 반환할 경우 중괄호가 생략되고 body가 = 심볼 다음에 지정될 수 있다.

fun double(x: Int): Int = x * 2

리턴 타입이 컴파일러에 의해 추론 가능할 경우 리턴 타입 선언 또한 선택적이다.

fun double(x: Int) = x * 2

Explicit return types


block body를 가진 함수는 Unit을 반환하지 않는 이상 반드시 리턴 타입을 명시적으로 지정해야 한다. Unit을 반환하는 경우에는 리턴 타입 지정이 선택 사항이다.

코틀린은 block body를 가진 함수의 리턴 타입을 추론하지 않는다. 그러한 함수는 body에서 복잡한 컨트롤 플로우를 가지고 있을 것이며, 리턴 타입은 코드를 읽는 사람에게(그리고 가끔씩은 컴파일러에게도) 불명확할 것이기 때문이다.


Variable number of arguments (varargs)


vararg 수정자(modifier)로 함수 파라미터(일반적으로 마지막 파라미터 하나)를 표시할 수 있다.

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) //ts는 array이다
        result.add(t)
    return result    
}

이 경우 가변 개수 아규먼트를 함수에 전달할 수 있다.

val list = asList(1, 2, 3)

위와 같이, 함수 내부에서 타입 Tvararg 파리미터는 T 배열로 보인다. 위 경우에서 ts 변수는 Array<out T> 타입을 가진다.

오직 하나의 변수만 vararg으로 표시될 수 있다. vararg 파라미터가 목록의 마지막 하나가 아닐 경우 후속 파라미터의 값들은 named argument 문법으로 전달될 수 있다. 또는, 만약 파라미터가 함수 타입을 가졌다면 괄호 밖에서 람다를 전달할 수 있다.

vararg 함수를 호출할 때 아규먼트를 개별적으로 asList(1, 2, 3)처럼 전달할 수 있다. 이미 배열이 있고 그 내용을 함수에 전달하고 싶을 경우 spread operator (배열의 접두사 *)fmf tkdydgksek.

val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

primitive type 배열을 vararg으로 전달하고 싶을 경우 toTypedArray() 함수를 이용해 레귤러(typed) 배열로 전환해야 한다.

val a = intArrayOf(1, 2, 3) // IntArray는 primitive type array 이다
val list = asList(-1 0, *a.toTypedArray(), 4)

Infix notation


infix 키워드로 표시된 함수는 infix notation(고정 표기법) (호출을 위한 온점과 괄호를 생략한다)으로 호출할 수 있다. infix 함수는 다음 조건을 반드시 만족시켜야 한다:

  • 반드시 member functions이거나 extension functions여야 한다.

  • 반드시 단일 파라미터를 가져야 한다.

  • 파라미터는 반드시 가변 개수 아규먼트를 허용하지 않아야 하고, 디폴트 값을 가지지 않아야 한다.

infix fun Int.shl(x: Int): Int { ... }

// infix 표기법으로 함수 호출
1 shl 2

// 이것은 아래와 같다
1.shl(2)

Infix function 호출은 산술연산자, 타입 캐스트, rangeTo 연산자보다 낮은 우선순위를 가진다.
아래 표현식을 참고하자.

  • 1 shl 2 + 31 shl (2 + 3) 과 동일하다.
  • 0 until n * 2는 `0 until (n * 2) 와 동일하다.
  • xs union ys as Set<*>xs union (ys as Set<*>)과 동일하다.

반면 infix function call의 우선순위는 불리언 연산자 &&||, is - 그리고 in - 체크, 그리고 몇몇 다른 연산자보다 높다. 이들 표현식은 다음과 같다.

  • a && b xor ca && (b xor c)와 같다.
  • a xor b in c(a xor b) in c와 같다.

infix function은 언제나 리시버와 파라미터를 모두 지정해야 한다는 점을 유의하자. infix notation을 사용하면서 current receiver로 메서드를 호출할 경우 this를 명시적으로 사용해야 한다. 이는 모호하지 않은 파싱을 보장하기 위해 필요하다.

class MyStringCollection {
    infix fun add(s: String) { /*...*/}

    fun build() {
        this add "abs" // Correct
        add("abs") // Correct
        add "abs" //Incorrect : 리시버가 반드시 지정되어야 한다. 
    }
}

Function scope


코틀린 함수는 파일의 top level에서 선언될 수 있다. 이는 함수를 가지고 있기 위한 클래스를 생성할 필요가 없다는 뜻이다. 자바, C#, Scala와 같은 언어에서는 클래스를 생성할 필요가 있었다. top level function에 더해서, 코틀린 함수는 member function 및 extension function으로 로컬로 선언될 수 있다.


Local functions


코틀린은 다른 함수 내부에 있는 함수인 로컬 함수를 지원한다.

fun dfs(graph: Graph) {
    fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v, visited)
    }

    dfs(graph.vertices[0], HashSet())
}

로컬 함수는 외부 함수(클로저)의 로컬 변수에 접근할 수 있다. 위 케이스에서는 visited가 로컬 변수가 될 수 있다.

fun dfs(graph: Graph) {
    val visitied = HashSet<Vertex>()
    fun dfs(current: Vertex) {
        if (!visitied.add(current)) return
        for (v in current.neighbors)
            dfs(v)
    }

    dfs(graph.vertices[0])
}

Member functions


멤버 함수는 클래스 또는 객체 내부에 정의된 함수이다.

class Sample {
    fun foo() { print("Foo") }
}

멤버 함수는 dot notation으로 호출할 수 있다.

Sample().foo() // 클래스 Sample의 인스턴스를 생성하고 foo를 호출한다

클래스와 오버라이딩 멤버에 대한 더 많은 정보는 ClassesInheritance를 참고하자.


Generic functions


함수는 함수 이름 앞에 꺽쇠 괄호를 사용해 지정된 제네릭 파라미터를 가질 수 있다.

fun <T> singletonList(item: T): List<T> {/*...*/}

제너릭 함수에 대한 더 많은 정보는 Generics을 참조하자.


Tail recursive functions


코틀린은 tail recursion이라고 알려진 함수형 프로그래밍 스타일을 지원한다. 일반적으로 루프를 사용하는 어떤 알고리즘들을 위해, 스택 오버플로우 리스크 없이 (루프 대신) 재귀 함수를 사용할 수 있다. 함수가 tailrec 수정자로 표시되고 형식 컨디션을 만족시킬 경우, 컴파일러는 재귀를 최적화하여 빠르고 효과적인 루프 기반 버전을 남긴다.

val eps = 1E - 10 // "good enough". 10^-15가 된다.

tailrec fun findFixPoint(x: Double = 1.0): Double = 
    if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))

이 코드는 수학적 상수인 코사인의 fixpoint를 계산한다. 이것은 1.0부터 시작해 변화가 없을 때까지 Math.cos를 호출한다. eps precision에 따라 0.7390851332151611라는 결과를 얻는다. 이 결과는 더 전통적인 아래 코드와 같다.

val eps = 1E - 10 // "good enough". 10^-15가 된다.
private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (Math.abs(x - y) < eps>) return x
        x = Math.cos(x)
    }
}

tailrec 수정자를 사용하기 위해서는 함수는 반드시 자신이 수행하는 마지막 연산자로서 자기 자신을 호출해야 한다. 재귀 호출 이후, try/catch/finally 블록 내부, 또는 open function에 더 많은 코드가 있다면 꼬리 재귀를 사용할 수 없다. 현재 재귀 함수는 Kotlin for the JVM과 Kotlin/Native에 의해 지원되고 있다.

더보기:

0개의 댓글