📚 함수(2)


📔 람다 표현식

  • 예약어가 없고 함수 이름도 없는 게 특징
  • 코드 블록인 중괄호 안에 직접 매개변수와 표현식을 작성
  • 매개변수와 표현식을 구분하는 기호 “->”로 구분함. 매개변수는 괄호 처리를 하지 않음
  • 매개변수가 없을 시, 이를 생략하고 표현식만 작성
  • 람다 표현식을 작성하고 바로 실행하려면 람다 표현식 다음에 호출 연산자인 괄호를 사용해 매개변수에 해당 인자를 전달
  • 람다 표현식에는 기본적으로 return 키워드를 사용할 수 없지만, 인라인 함수로 사용할 때는 실제 람다 표현식 내부 로직이 호출된 곳에 코드로 삽입되기 때문에 return 키워드 사용 가능.
  • 람다 표현식 중간에 함수를 빠져나갈 경우, 레이블 된 return 키워드 사용 가능. 이때 반환값이 아닌 현재 처리하는 람다 표현식을 종료하는 조건으로만 사용
  • 람다 표현식에서 매개변수가 하나일 경우 별도의 매개변수를 지정하지 않고 하나의 매개변수라는 의미로 it을 사용 가능
{ println("아무 인자가 없음") }() // 인자가 없는 경우

println({x: Int -> x * x}(10)) // 인자가 하나 있는 경우
println({x: Int, y: Int -> x + y}) // 인자가 두 개 있는 경우

val a = { x: Int, y: Int -> x + y } // 재사용하려면 변수에 할당
println(a(200, 300))

fun func(x: Int, y: Int, f: (Int, Int) -> Int ): Int { // 함수 매개변수를 가지는 함수
    return f(x, y)
}

println(func(100, 200, {x, y -> x + y})) // 인자로 람다 표현식 전달
아무 인자가 없음
100
(kotlin.Int, kotlin.Int) -> kotlin.Int
500
300


📖 람다 표현식과 익명 함수 비교

람다 표현식은 좀 더 축약된 표현과 return 키워드를 사용하지 않는 것만 제외하면 익명 함수와 동일하다.

println({ p1: Int, p2: Int -> p1 + p2 } (100, 200)) // 람다 함수를 정의하고 즉시 실행

fun add(x: Int, y: Int) = x + y // 함수는 정의 후 호출
println(add(100, 200))

println({ x: Int, y: Int -> x + y } (100, 200))
println((fun (x: Int, y: Int) = x + y)(100, 200)) // 익명 함수를 정의하고 즉시 실행
300
300
300
300

📖 람다 표현식 활용

fun ret(): () -> Int {
    return { 100 } // 함수 반환값
}

val r = ret()

println(r())

val ll = listOf(1, 2, 3)
println(ll.map { it * it }) // 함수의 인자

class LL(val action: (Int) -> Int) // 클래스의 속성
val l = LL({it * 2})
println(l.action(10))
100
[1, 4, 9]
20


📔 클로저

📖 외부 함수와 내부 함수

  • 외부 함수(outer function): 지역 함수를 가진 함수
  • 내부 함수(inner function): 외부 함수 내 정의된 저역 함수

📖 클로저(Closer)

함수 밖에 정의된 변수들을 사용할 수 있는 함수

fun outer(x: Int) { // 외부 함수
    fun inner(y: Int) = x + y // 외부 함수 변수 사용
    println(inner(x)) // 외부 함수 지역 변수 인자 제공
}
outer(10)

fun outer1(x: Int): Int {
    fun inner(y: Int) = x + y
    return inner(x)
}
println(outer1(10))

fun outer2(x: Int): Int {
    return (fun(y: Int): Int {
        return x + y
    })(10) // 내부 함수를 익명 함수로 처리
}
println(outer2(10))

fun outer3(x: Int): Int {
    return { y: Int -> x + y }(10) // 내부 함수를 람다식으로 처리
}
println(outer3(10))
20
20
20
20

📖 함수 반환 처리

내부 함수를 실행하지 않고 반환 처리해 클로저 구성

  • 함수 참조: 일반 함수는 직접 함수의 레퍼런스르 가질 수 없어 함수 참조를 별도의 방식으로 가져와야 함
  • 함수 참조 표기법: 콜론을 두 개 붙이고 다음에 함수 이름을 정의
  • 함수 자료형 표기법: (매개변수 자료형, …) -> 반환 자료형 으로 지정. 이때 매개변수는 반드시 괄호 내에 표기
fun outer(x: Int): (Int) -> Int { // 함수를 참조로 반환
    fun inner(y: Int) = x + y
    return ::inner
}

val inner1 = outer(10)
println(inner1(10))

fun outer2(x: Int): (Int) -> Int { // 람다식 반환
    return { y: Int -> x + y }
}

val inner2 = outer2(10)
println(inner2(10))

fun outer3(x: Int): (Int) -> Int { // 익명 함수 반환
    return fun(y: Int) = x + y
}

val inner3 = outer3(10)
println(inner3(10))
20
20
20


📔 렉시컬 스코핑

함수는 모두 지역 영역을 가지는데 이러한 지역 영역을 변수 스코프(variable scope)라고 부른다. 그리고 외부 함수와 내부 함수는 각각 스코프를 구성한다. 내부 함수는 외부 함수의 스코프를 참조할 수 있으므로 스코프 계층이 생긴다. 렉시컬 스코핑(lexical scoping)은 이런 계층에서 변수를 검색하는 방법을 의미한다.

  • 렉시컬 스코프의 처리기준: 항상 자기 함수 내부(local)를 조회하고 없으면 외부 함수(non-local)를, 이 마저도 없으면 전역인 패키지의 변수(global)를 찾음
  • 보통 LGB(Local -> Global -> Built in) 순으로 변수를 검색
val factor = 2
fun func(x: Int): Int = x * factor // 전역 변수 사용
println(func(20))
40


📖 내부 함수 반환 처리 시, 스코프 처리 방식

fun funcLen(length: Int): (String) -> Boolean { // 매개변수가 하나이며, 함수를 반환
    return { input: String -> input.length == length } // 내부 함수로 람다식 사용, length는 외부 함수의 매개변수
}

val len = funcLen(4)

println(len("가나다라마바사")) // 내부 함수 호출: 외부 함수 매개변수 사용
println(len("가나다라"))

val len1 = funcLen(6)

println(len1("가나다라마바사"))
println(len1("가나다라"))
false
true
false
false


📔 함수 자료형

📖 함수 자료형 표기법

  • 매개변수 자료형 표시: 괄호 안에 매개변수의 개수에 맞춰 자료형을 쉼표로 구분해서 정의
  • 반환 자료형 표시: -> 다음에 자료형 표시. 이 과정에서 괄호를 사용하지 않음
  • 매개변수 자료형에 함수가 전달될 때는 함수 자료형 지정
  • 보통 반환 자료형이 함수일 때도 함수 자료형 지정
  • 변수에 함수가 할당될 때는 익명 함수, 람다식, 함수 참조에서 정의된 것을 확인하고 타입추론이 가능
  • 함수 자료형에 여러 개 묶여 전달 시, “->” 을 기준으로 우측에 표기된 것을 괄호로 묶어 처리

📖 변수에 함수 자료형 처리

val a: () -> Unit = { println("함수 ") } // 매개변수가 없고 반환값은 Unit으로 처리
val b: (Int) -> Int = { x -> x * 3 } // 매개변수가 하나이고 반환값은 Int
val c: (Int, Int) -> Int = { x , y -> x + y } // 매개변수가 두 개이고 반환값은 Int

a()
println(b(10))
println(c(10, 20))
함수 
30
30

익명 함수 또한 람다식과 같은 방법으로 함수 자료형을 지정해 변수에 할당 가능

val a: () -> Unit = fun(){ println ("함수 ") }
val b: (Int) -> Int = fun(x: Int): Int { return x * 3 }
val c: (Int, Int) -> Int = fun(x: Int, y: Int): Int { return x + y }

a()
println(b(10))
println(c(10, 20))
함수 
30
30

함수 정의 또한 마찬가지로 위와 같이 사용할 수 있다.

fun unit(){ println("함수 ") }
fun triple(x: Int): Int { return x * 3 }
fun add(x: Int, y: Int): Int { return x + y }

val a: () -> Unit = ::unit
val b: (Int) -> Int = ::triple
val c: (Int, Int) -> Int = ::add

a()
println(b(10))
println(c(10, 20))
함수 
30
30


📖 내부 함수를 반환하는 함수를 변수에 할당

val producePrinter: () -> () -> Unit = 

val producePrinter1 = { { println("함수 전달 2") } }

fun outer(): () -> Unit {
    return { println("함수 전달 3") }
}

val producePrinter2: () -> () -> Unit = ::outer

producePrinter()()
producePrinter1()()
producePrinter2()()
함수 전달 1
함수 전달 2
함수 전달 3


📔 nullable한 함수 자료형 정의

📖 nullable 함수 자료형 표기법

  • nullable 함수 자료형: (함수 자료형)?, 함수 자료형 전체를 소괄호로 묶고 그다음 물음표를 붙임
  • nullable 함수 자료형 호출: 함수명?.invoke(인자), 널이 들어온 경우 안전호출을 처리하기 위해 invoke() 메서드로 처리

📖 매개변수에 nullable 함수 자료형

  • 인자의 개수가 하나이거나 마지막 인자이면, 람다식을 전달할 때 괄호를 생략해서 전달할 수 있다.
fun nullFunc(action: (() -> Unit)?): Long { // 함수 자료형 전체를 괄호로 묶은 후 물음표
    val start = System.nanoTime() // 시스템 시간을 조회
    action?.invoke() // 널값이 들어올 수 있으니 ? 이후 실행 처리
    
    return System.nanoTime() - start // 처리 시간 표시
}

println(nullFunc(null))

println(nullFunc { println("Hello Word") })

println(nullFunc(fun(): Unit { println("익명 함수") }))

fun unitFunc() = println("함수 처리")
println(nullFunc(::unitFunc))
400
Hello Word
107700
익명 함수
105100
함수 처리
106000


📖 함수 반환을 nullable 함수 자료형 처리

val innerFunc: (Int) -> ((Int) -> Int)? = { n -> null } // 반환값을 전체로 nullable 함수 자료형 처리, 함수 전달 대신 null 반환
println(innerFunc(10)?.invoke(20)) // 안전 호출

val innerFunc1: (Int) -> ((Int) -> Int)? = { n -> { m -> n + m } } // 반환값을 전체로 nullable 함수 처리, 함수 표현식으로 전달, 반환 자료형도 함수 표현
println(innerFunc1(10)?.invoke(20))

val innerFunc2: (Int) -> ((Int) -> Int)? = fun(n: Int): ((Int) -> Int)? {
    return fun(m: Int): Int {
        return n + m
    }
}
println(innerFunc2(10)?.invoke(20))

fun funcDT(n: Int): ((Int) -> Int)? {
    fun inner(m: Int): Int { return n + m }
        return ::inner
}

println(funcDT(10)?.invoke(20))
null
30
30
30


📔 호출 메서드 - invoke()

📖 함수에서 호출 연산자 사용

val toUpperCase = { str: String -> str.uppercase() }
println(toUpperCase.invoke("summer")) // 람다식은 invoke() 메서드 사용 가능

val upper = fun(str: String): String {
    return str.uppercase()
}
println(upper.invoke("fall in love")) // 익명 함수도 invoke() 메서드 사용 가능

fun toLowerCase(str: String): String {
    return str.lowercase()
}
println(toLowerCase("SUMMER")) // 함수를 직접 호출할 경우, 호출 연산자만 사용 가능

val lower = ::toLowerCase // 함수 참조로 사용하면, invoke() 메서드 사용 가능
println(lower.invoke("Summer"))
SUMMER
FALL IN LOVE
summer
summer


📖 클래스에 연산자 오버로딩

  • 함수 자료형 구분
    • 함수 자료형 표기법: 코틀린 문법에서 사용하는 함수 자료형은 매개변수 자료형과 반환 자료형으로 표기
    • 함수 인터페이스 표기법: 코틀린 패키지 내부에 함수 자료형과 일치하는 인터페이스를 제공. 예) Function1<Int, Int>: 매개변수 1개와 반환값 1개를 꺾쇠 내부에 표시
class MyFunction : () -> Unit { // 함수 자료형 상속
    override operator fun invoke() { // 실행 연산자 오버라이딩
        println("실행 연산 실행 1")
    }
}

val function = MyFunction() // 함수 객체 생성 
function() // 함수 실행

class A : Function<Unit> { 함수 자료형 인터페이스 상속
    operator fun invoke() { // 실행 연산자 오버라이딩
        println("실행 연산 실행 2")
    }
}

val functionA = A() // 함수 객체 생성
functionA()
val a = object : (Int, Int) -> Int { // object 표현식, 함수를 상속
    override fun invoke(x: Int, y: Int): Int { //연산자 오버라이딩
        println("객체 invoke 호출 1")
        return x + y
    }
}

val x = a(10, 20)
println(x)

object Func: (Int, Int) -> Int { // object 정의, 함수를 상속
    override fun invoke(x: Int, y: Int): Int { // 연산자 오버라이딩
        println("객체 invoke 호출 2")
        return x * y
    }
}

val y = Func(10, 20)
println(y)
객체 invoke 호출 1
30
객체 invoke 호출 2
200


📔 함수 오버로딩

함수의 매개변수의 개수와 자료형만 달리하면 같은 이름의 함수를 여러 개 정의할 수 있다. 이것을 ‘함수 오버로딩’이라 한다.

📖 함수 식별자

  • 함수 이름 + 시그니처를 식별자로 봄. 예) 두 개의 매개변수를 갖진 함수일 경우, 함수명 + (Int, Int)가 함수 식별자
  • 함수 식별자가 다르면 다른 함수로 봄

📖 매개변수가 다른 함수 오버로딩

fun func(a: String, b: String): String { // 동일한 타입의 매개변수 두 개
    return "func" // 반환값은 다름
}

fun func(a:String): Int { // 동일한 타입의 매개변수 하나
    return 100 // 반환값은 다름
}

println(func("가", "나"))
println(func("가"))
func
100

📖 매개변수의 개수가 동일한 함수 오버로딩

fun test1(a: String, b: String? = null) { // 두 개의 매개변수 
    println("test1")
}

fun test1(a: Int, b: String) { // 두 개의 매개변수
    println("test2")
}

test1(100, "a")
test1("ccc", "a")
test2
test1

📖 초깃값과 가변 인자로 함수 오버로딩 줄이기

fun test3(a: Any): String = "매개변수 하나"
fun test3(a: String, b: String = "Hello"): String = "매개변수 둘" // 초깃값 지정
fun test3(vararg a: String): String = "가변 매개변수" // 가변 인자 지정

println(test3("a", "b", "c", "d"))
println(test3("a"))
println(test3(100))
가변 매개변수
매개변수 둘
매개변수 하나