본문 바로가기

Study/Kotlin

Kotlin / 함수 알아보기

반응형

함수는 재사용하는 가장 작은 단위의 구성요소이다.

객체지향 프로그래밍 기법이 도입되면서 모든 것을 객체로 관리 -> 함수도 1급 객체로 처리됨

=> 함수도 정수처럼 변수, 매개변수, 반환값 등 사용할 수 있다

 

1. 함수

메모리에 있는 함수를 참조해서 가져와야 함수를 실행할 수 있다. -> 함수 이름 등으로 함수를 식별해야 한다.

- 함수 머리부 (Function Header) : 함수 이름과 매개변수 개수에 맞게 자료형과 반환 자료형으로 구성

- 함수 몸체부 (Function Body) : 실제 실행되는 코드 영역

fun 함수명(매개변수명1: String, 매개변수명2: String): Pair<String, String> { // 반환타입 : 튜플처리

    val 지역변수1 = 100
    var 지역변수2 = 300

    fun 지역함수명(매개변수명: String): String {
        return "매개변수명"
    }

    class 지역클래스명 {}

    //object 지역 오브젝트 {} // 함수 내부에서 지정 금지

    println(" $지역변수1 $지역변수2")// 지역변수를 사용하지 않으면 컴파일 에러

    return Pair(매개변수명1, 매개변수명2) // 튜플로 반환값 전환

}

val 결과값 = 함수명("함수", "호출")

println(결과값)


// 100 300
// (함수, 호출)

반환값이 없는 함수 정의와 실행

함수를 호출하면 실행된 결과를 반드시 반환한다. 반환값이 없을 경우에도 보통 반환자료형을 지정해야 한다. 이때 아무것도 처리하지 않는 Unit 자료형을 지정한다.

fun func(): Unit{ // 반환값이 없는 함수에도 반환 자료형 정의
    println(" 특정 처리 기능이 없음")
}

func()

fun funcNoReturn(){ // 반환값이 없을 경우는 생략 가능
    println(" 반환값을 생략 가능")
}

funcNoReturn()


// 특정 처리 기능이 없음
// 반환값을 생략 가능

 

위 예제의 함수에는 return 문이 없어서 실제 반환값이 없다. 그래서 Unit을 반환 자료형으로 표시한다.

아무것도 표시하지 않으면 반환 자료형을 Unit으로 추론한다.

 

함수 몸체부(블록 처리)

fun add(x: Int, y: Int) : Int{
    return x+y
}

fun add1(x: Int, y: Int) : Int = x+y

println(add(10,20)) // 30
println(add1(10,20)) // 30

함수 코드 블록의 하나의 라인으로 처리될 경우 간략하게 작성 할 수 있다.

위의 예제처럼 코드블록 대신 = 과 표현식으로 표시가 가능하다.

 

fun comSingle(x: Int, y: Int) = if (x > y) x else y // if표현식 사용

val r = comSingle(20, 10)
println(r) // 20

조건문, object 표현식 등은 하나의 표현식으로 작성할 수 있다.

if 표현식일 경우 if else 구문을 모두 하나의 표현식 처리로 작성했다.

 

함수의 매개변수와 인자

함수는 동일한 이름으로 여러 개의 함수를 정의할 수 있다.

함수 호출의 인자를 보고 정확한 함수를 식별하는 것이 중요하다.

fun addVar(x: Int, y: Int, z: Int) = x + y + z

println("위치인자 = "+ addVar(10, 20, 30)) // 매개변수 위치에 따라 인자 매핑
println("이름인자 = "+ addVar(z=100, x=20, y=30)) // 매개변수 이름과 인자를 같이 넣어서 처리
println("인자혼합 = "+ addVar(30, z=20, y=30)) // 위치인자와 이름인자 혼합

// 위치인자 = 60
// 이름인자 = 150
// 인자혼합 = 80

함수에 3개의 인자를 전달해서 실행할 수 있고, 각 매개변수의 이름을 지정하면 순서와 상관없이 값을 할당해서 처리할 수 있다.

 

fun defaultArg(x: Int = 100, y: Int = 200) = x + y

println("인자 전달 없음 = " + defaultArg()) // 인자 전달 없음 = 300

함수의 매개변수와 인자는 항상 일치해야 하기 때문에, 이를 대비하기 위해 함수를 정의할 때 매개변수에 초기값을 지정할 수 있다.

 

fun addVarArg(vararg x: Int): Int {
    var result = 0
    for (i in x) {
        result += i
    }
    return result
}

println("가변인자 0 = "+ addVarArg()) // 가변인자 0 = 0
println("가변인자 4 = "+ addVarArg(1,2,3,4)) // 가변인자 4 = 10

val ll = intArrayOf(1, 2, 3, 4) // 스프레드 처리할 때는 array 등 기본 배열 사용
println("스프레드 연산 사용 = "+ addVarArg(*ll)) // 스프레드 연산 사용 = 10

하나의 매개변수를 지정했지만, 예약어 vararg가 붙어서 여러 개의 인자를 전달받을 수 있다.

배열로 정의된 것을 가변인자로 처리하려면 *를 붙여 모든 원소가 전달되어야 한다. > 이 연산자를 스프레드 연산자라고 한다.

 

코틀린은 가변 객체와 불변 객체가 있는데, 가변 객체를 함수의 인자로 전달하면 가변 객체 내부의 값을 변경할 수 있다.

함수 내부에서 외부 값을 변경하지 않으려면 가변 리스트를 복사해서 함수의 인자로 전달해야 한다.

 

지역변수, 지역함수와 변수 스코프

- 지역변수 : 함수 내부에서만 사용할 수 있는 변수

- 지역함수 : 함수 내부에서만 사용할 때 지정한다. 보통 클로저 환경을 구성하거나 세부 기능을 분리해서 처리할 때 사용

- 지역 클래스 : 함수 내부에서 클래스를 활용해서 사용할 때 정의. (함수 기능이 커지면 클래스로 변환하기 때문에 실제로 거의 사용X)

패키지, 함수 등은 내부에서 변수를 정의할 때 변수를 관리하는 영역인 변수 스코프가 생긴다.

 

var outVar = 300
val outVarR = 999

fun outerFunc1(x: Int): Int {
    val y = 100
    fun localFunc() = x + y + outVarR
    return localFunc()
}

fun outerFunc2(x: Int): Int {
    val y = 100
    fun localFunc(): Int {
        outVar += x
        return x + y + outVar
    }
    return localFunc()
}

println("전역변수 참조 = " + outerFunc1(100)) // 전역변수 참조 = 1199
println("전역변수 갱신 = " + outerFunc2(100)) // 전역변수 갱신 = 600
println("전역변수 = " + outVar) // 전역변수 = 400

outFunc2에서 전역변수를 갱신해서 조회하면 변경된 것을 확인할 수 있다.

이렇게 변경할 수 있는 전역변수는 함수 내에서 변경할 수 있다는 것에 주의해야 한다.

 

패키지 기반에서 정의된 변수와 함수는 전역영역

함수 내부에 정의된 변수와 함수는 지역영역

지역함수 내에 다시 변수와 함수가 정의도면 지역의 지역영역

 

이렇게 함수의 계층 구조에 따라 스코프도 계층화할 수 있다.

 

var outVar = 300 // 전역변수 정의

fun outerFunc(x: Int): Int {
    val y = 100
    fun innerFunc(): Int { // 지역함수 정의
        var z = 777
        z += outVar
        fun localFunc() = x + y + z // 지역함수 내의 지역함수 정의
        return localFunc()
    }
    return innerFunc()
}

println(outerFunc(100)) // 1277

 

 

최상위 변수 지정하고, 함수 내부에 지역변수와 지역함수를 정의했다.

지역함수 내부에 지역변수와 지역함수를 정의할 수 있다.

함수를 실행하면 지역함수의 지역함수가 먼저 실행되고 그 결과를 받은 내부함수가 실행된 후에 최종 결과가 반환된다.

 

2. 익명함수와 람다식

2-1 익명함수

익명함수는 함수의 이름을 가지지 않는다. 일회성으로 처리하는 용도로 사용한다.

println((fun(매개변수1: Int, 매개변수2: Int): Int { // 익명함수 즉시 실행
    return 매개변수1 + 매개변수2
})(100,200))

val 덧셈 = fun(매개변수1: Int, 매개변수2: Int): Int { // 익명함수를 변수에 할당
    return 매개변수1 + 매개변수2
}

val res1 = 덧셈(300, 200) // 익명함수를 실행
println(res1)

val res2 = (fun(매개변수1: Int, 매개변수2: Int): Int { // 즉시 실행
    return 매개변수1 + 매개변수2
})(500, 200)

//300
//500
//70

익명함수를 이름으로 호출하려면 변수에 할당하고 변수 이름으로 조회해서 실행한다

 

익명함수도 함수 코드 블록 내부에 지역변수, 매개변수, 반환 자료형을 지정해서 처리할 수 있다.

익명함수 내부에 익명함수를 지역함수로 정의할 수 있다.

val res3 = (fun(x: Int): (Int) -> Int { // 외부 익명함수
    val inner = fun(y: Int): Int { // 내부 익명함수
        return x + y
    }
    return inner // 변수로 반환
})(10)(20)

val res4 = (fun(x: Int): (Int) -> Int { // 외부 익명함수
    return fun(y: Int): Int { // 바로 반환 내부 익명함수
        return x + y
    }
})(10)(20)

val res5 = fun(x: Int, y: Int, f: (Int, Int) -> Int): Int { // 함수를 매개변수로 받음
    return f(x, y) // 함수 실행결과를 전달
}

println(res3) //30
println(res4) //30
// 익명함수를 인자로 전달
println(res5(100,200, fun(x: Int, y: Int): Int { return x + y })) //300

위 예제는 익명함수 내부에 익명함수를 작성하고 변수에 할당한 후에 이를 반환값으로 반환한 것이다.

이를 변수에 할당하기 전에 내부 익명함수를 바로 실행해서 처리한 결과를 변수에 할당했다.

실제 변수에는 최종 처리결과가 저장된 것을 알 수 있다.

 

2-2 람다표현식 (lamda Expression)

람다표현식은 상수처럼 사용하는 함수를 의미한다.

익명함수보다 더 간편하게 함수를 정의하고 상수처럼 인자나 반환값 등으로 전달하기 편리하다.

보통 함수의 인자나 반환할 때 많이 사용한다.

 

- 예약어가 없고 함수 이름도 없다

- 코드 블록인 중괄호 안에 직접 매개변수와 표현식을 작성한다.

- 매개변수와 표현식을 구분하는 기호 (->) 로 구분한다.

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

// 100
// 200

 

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

// 500

변수에 저장된 람다표현식은 나중에 실행할 때 인자를 전달하면 람다표현식을 그대로 실행해서 결과값을 반환한다.

 

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

함수를 정의할 때 매개변수에 함수 자료형을 지정하면 이 함수를 호출할 때 람다표현식을 전달해서 처리할 수 있다.

 

 

람다표현식은 익명함수보다 더 축약된 표현, return문을 사용하지 않다. => 이를 제외하면 동일하다.

// 함수는 정의 후에 호출
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

함수를 인자로 전달하고 반환값을 처리할 때는 람다표현식으로 처리한다.

익명함수는 람다표현식보다 내부 코드가 많아질 때 사용하는 것이 좋다.

 

2-3 클로저

- 외부함수(outer function) : 지역함수를 가진 함수

- 내부함수(inner function) : 외부함수 내에 정의된 지역함수

 

외부함수 내에 내부함수를 정의하고 단순히 내부함수를 실행하지 않고 이 내부함수를 반환한다.

이때 내부함수는 외부함수의 지역변수를 사용할 수 있다.

반환된 내부함수가 실행될 동안 외부함수의 지역변수를 계속 사용한다.

이때 외부함수의 지역변수를 자유변수라고 하고, 이런 환경을 클로저(closure)라고 한다.

 

함수 내의 함수 정의와 실행

함수 코드 블록 내에 내포된 함수를 지역함수(local function) 또는 내부함수(inner function)라고 한다.

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

외부함수 내부에 지역함수인 내부함수를 정의해서 내부함수를 실행하고 결과를 보여준다.

fun outer1(x: Int): Int {
    fun inner(y: Int) = x + y
    return inner(x) // 내부함수 실행 결과
}
println(outer1(10)) // 20

내부함수의 실행된 결과를 반환했다.

fun outer2(x: Int): Int {
    return (fun(y: Int): Int {
        return x + y
    })(10)
}
println(outer2(10)) // 20

fun outer3(x: Int): Int {
    return { y: Int -> x + y }(10)
}
println(outer3(10)) // 20

outer2 : 내부함수를 익명함수로 정의하고 바로 실행한 결과를 반환한다.

outer3 : 람다표현식을 정의하고 바로 실행한다.

 

내포된 내부함수는 함수 내부에서 모두 실행되어 결과를 반환한다.

 

함수 반환처리

내부함수를 실행하지 않고 반환처리해서 클로저 환경을 구성할 수 있다.

fun outer2(x: Int): (Int) -> Int { // 함수를 참조로 변환
    fun inner(y: Int) = x + y
    return ::inner
}

val inner1 = outer2(10)
println(inner1(10)) // 20

지역함수가 일반함수일때는 바로 반환할 수 없다.

리플랙션의 함수 참조를 사용해 함수 레퍼런스(함수 객체)를 조회해서 반환할 수 있다.

fun outer3(x: Int) : (Int) -> Int{
    return {y: Int -> x+y}
}

val inner2 = outer3(10)
println(inner2(10)) // 20

내부함수를 람다표현식으로 정의해서 바로 return문으로 반환한다.

fun outer4(x: Int): (Int) -> Int {
    return fun(y: Int) = x + y
}

val inner3 = outer4(10)
println(inner4(10)) // 20

내부함수를 익명함수로 정의해서 반환한다.

 

위 세 함수 모두 내부함수를 반환하므로 함수를 다시 실행해야 최종 결과를 알 수 있다.

 

렉시컬 스코핑

- 변수 스코프(variable scope) : 일반함수, 익명함수, 람다표현식 모두 함수가 가진 지역영역

- 렉시컬 스코핑(lexical scoping) : 외부함수와 내부함수 각각 스코프를 구성하는데, 내부함수는 외부함수의 스코프를 참조할 수 있다. -> 스코프 계층이 생긴다. 이런 계층에서 변수를 검색하는 방법을 렉시컬 스코핑이라고 한다.

- LGB(local -> global -> built-in) 순으로 변수를 검색

 

3. 함수 자료형

함수 자료형 정의

함수도 변수에 할당할 수 있으므로 함수 자료형을 표시해서 사용한다.

반환 자료형 표시 : ->

 

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

람다표현식으로 작성한 것을 변수에 할당하고, 함수에 함수 자료형을 지정했다.

함수 자료형에 인자가 없을 때도 괄호를 반드시 표시해서 매개변수가 없다는 것을 명확히 표시해야 한다.

 

널이 가능한 함수 자료형 정의

코틀린은 널이 불가능한 자료형을 확장해서 널이 가능한 자료형으로 만들 수 있다.

 

널러블 함수 자료형 : (함수자료형)?  <<< 함수 자료형 전체를 소괄호로 묶고 그 다음에 물음표를 붙인다.

널러블 함수 자료형 함수 호출 : invoke(인자) <<< 널이 들어온 경우 안전호출을 처리하기 위해 invoke 메서드로 처리한다.

 

// 함수 자료형도 널 자료형이 가능
fun nullFunc(action: (() -> Unit)?): Long { // 함수 자료형 전체를 괄호로 묶고 난 후에 물음표
    val start = System.nanoTime() // 시스템 시간을 조회
    action?.invoke() // 널이 들어올 수 있으니 ? 이후 실행 처리

    return System.nanoTime() - start // 최종 처리 시간 표시
}

println(nullFunc(null)) // 널 전달
// 195

println(nullFunc{ println("Hello World") }) // 람다함수 전달
// HelloWorld
// 89914

println(nullFunc(func(): Unit { println("익명함수") })) // 익명함수 전달
// 익명함수
// 39975

fun unitFunc() = println("함수처리") // 함수 참조 전달
println(nullFunc(::unitFunc))
// 함수처리
// 70827

함수 매개변수에 널러블 함수 자료형을 지정하였다.

전달된 함수는 코드 블록 내에서 호출한다.

안전호출 연산이 추가되어 뒤에는 연산자가 아닌 Invoke 메서드로 처리하였다.

 

호출메서드(invoke)

invoke 메서드가 실행되면 함수의 반환 자료형에 속하는 객체를 반환한다.

val toUpperCase = { str: String -> str.uppercase() }
println(toUpperCase.invoke("summer")) // SUMMER

invoke 메서드가 실행되면 함수의 반환 자료형에 속하는 객체를 반환한다. 

 

toUpperCase의 타입은 String을 받고, 다시 String을 반환하는 (String) -> String 타입니다.

이 타입은 코틀린 표준 라이브러리에 정의된 Function1<P1, R> 인터페이스 타입이다.

Function1<P1,R>의 구현을 살펴보면 invoke(P1) : R 연산자 하나만 존재한다.

위의 toUpperCase는 아래의 코드와 같다.

val toUpperCase = object : Function1<String, String> {
    override fun invoke(p1: String): String {
        return p1.upperCase()
    }
}

 

함수 오버로딩

함수 식별자는 함수 이름과 시그니처인 함수 매개변수의 개수와 자료형으로 구성한다.

따라서 같은 이름의 함수를 여러 개 정의할 수 있다. 

함수 오버로딩(function overloading) : 함수 식별자가 다른 것을 여러 개 정의한다.

/* 매개변수 개수가 다른 함수 오버로딩 */
fun func(a: String, b: String): String {
    return "func"
}

fun func(a: String): Int{
    return 100
}

println(func("가","을")) // func
println(func("가")) // 100

/* 매개변수 개수가 동일한 함수 오버로딩 */
fun test1(a: String, b: String? = null) {
    println("test1")
}

fun test1(a: Int, b: String) {
    println("test2")
}

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

 

참고

https://wooooooak.github.io/kotlin/2019/03/21/kotlin_invoke/

 

코틀린 invoke 함수(람다의 비밀) · 쾌락코딩

코틀린 invoke 함수(람다의 비밀) 21 Mar 2019 | kotlin invoke operator invoke 란? 코틀린에는 invoke라는 특별한 함수, 정확히는 연산자가 존재한다. invoke연산자는 이름 없이 호출될 수 있다. 이름 없이 호출된

wooooooak.github.io

 

 

 

=== Q&A'

 

    val function:((String) -> Unit)? = { 
        println(it)
    }
    
    function?.invoke("1")
    if(function!=null){
        function("2")
    }

 

class Sample:(String)-> Unit{
    override fun invoke(p1:String){
        println(p1)
    }
}

// 이렇게 많이 사용
class Sample2{
    operator fun invoke(p1: String){
        println(p1)
    }
}

 

지역함수 많이 쓸까?

함수 내에서만 쓰는 기능들을 만들때 사용한다 -> 접근 제어가능

함수 노출이 많이 된다는 것은 외부에서 접근할 수 있다는 것이기 때문에.

함수 접근을 최소화 하는 편도 고려

 

 

ex) recyclerview에서 스크롤을 기다릴때, 스크롤이 멈췄을때 이 아이템이 보이느냐는 함수를 만든다.

-> 만약, 스크롤이 된 적없으면 기다려야되고, 스크롤이 돼서 아이템이 보이는 상태라면 스크롤이 안보이고 웨이트 하는 시점 리턴하면 된다.

화면 아이템이 보이냐.!! 함수를 호출하면 리스너를 달고.. 뭐 그런 느낌 ? 이 함수 안에서만 사용하는 로직