[kotlin] inline 함수, 클로저

inline 함수

고차 함수에 람다 함수를 전달하고 이 람다 함수를 이용하는 코드가 많아져서 런타임 때 성능 문제가 발생할 수 있다. 자바로 변환 시 함수가 새로 생성되고 호출하므로 비효율적이다. 고차 함수 호출이 빈번하여 성능에 영향을 미칠 때는 인라인 함수가 대안일 수 있다. 인라인 함수는 함수 선언 앞에 inline 예약어를 추가한 함수로 컴파일 단계에서 정적으로 포함되므로 실행 때 함수 호출이 발생하지 않는다. 자바 변환 시 함수가 새로 생성되지 않고 함수를 호출한 곳에 정적으로 내용이 포함된다. 런타임때 함수 호출이 그만큼 줄고 성능에 도움이 된다는 것이다. inline 함수는 고차 함수와 함께 활용하는 것이 정상적인 활용 방법이다.

    inline fun hoFunTest(argFun: (x1: Int, x2: Int) -> Int){
    	argFun(10, 20)
    }
    
    fun main(args: Array<String>){
    	hoFunTest { x1, x2 -> x1 + x2 }
    }
    public static final void main(@NotNull String[] args){
    	//...
    	int x2 = 20;
    	int x1 = 10;
    	int var1000 = x1 + x2;
    }

고차 함수에 함수 타입 매개변수가 여러 개일 때 noinline 예약어를 이용하여 inline에 적용되는 람다 함수와 적용되지 않는 람다 함수를 구분할 수 있다.

    inline fun hoFunTest(argFun1: (x1: Int) -> Int, noinline argFun2: (x: Int) -> Int){
    	argFun1(10)
    	argFun2(10)
    }
    
    fun main(args: Array<String>){
    	hoFunTest({it * 10}, {it * 20})
    }

Non-local return

논로컬 반환이란, 람다 함수 내에서 람다 함수를 포함하는 함수를 벗어나게 해주는 기법이다. 코틀린에서는 이름이 정의된 일반 함수와 익명 함수에서만 사용할 수 있으며 람다 함수에서는 사용할 수 없다.

    fun inlineTest2(argFun: (x: Int, y: Int) -> Int): Int{
    	return argFun(10, 0)
    }
    
    fun callfun(){
    	println("callFun.. top")
    	val result = inlineTest2 { x, y ->
    		if(y <= 0) return // Error!!
    		x / y
    	}
    	println("$result")
    	println("callFun.. bottom")
    }

람다 함수가 callFun() 함수 내에 작성되어 있더라도 런타임 시 람다 함수의 내용이 callFun() 내에 포함된 것은 아니므로 여기서 return은 callFun() 함수를 벗어나게 하지 못한다.

    inline fun inlineTest2(argFun: (x: Int, y: Int) -> Int): Int{
    	return argFun(10, 0)
    }
    
    fun callfun(){
    	println("callFun.. top")
    	val result = inlineTest2 { x, y ->
    		if(y <= 0) return
    		x / y
    	}
    	println("$result")
    	println("callFun.. bottom")
    }
    
    // callFun.. top

inline이 추가된 고차 함수에 전달하는 람다 함수에서는 return 구문을 사용할 수 있다. inline이 적용된 고차 함수의 내용이 컴파일 단계에서 callFun() 함수 내부에 포함되므로 return을 명시해 callFun() 함수를 벗어나도록 할 수 있다.

crossinline

    open class TestClass {
        open fun some() {}
    }
    
    inline fun inlineTest3(argFun: () -> Unit) {
        val obj = object : TestClass() {
            override fun some() = argFun() // Error!!
        }
    }

위의 코드처럼 함수를 inline으로 선언하면 해당 함수에 매개변수로 전달하는 람다 함수에 return을 명시할 수 있다. 그런데 return을 명시할 수 있는 람다 함수를 inline 함수 내부에서만 사용하지 않고, 또 다른 객체에 대입하면 그 객체의 코드가 inline되지 않아서 에러가 발생한다. 이처럼 다른 객체에 대입하여 이용하는 매개함수를 선언할 때 crossinline 예약어를 추가하여 작성할 수 있다.

    open class TestClass {
        open fun some() {}
    }
    
    inline fun inlineTest3(crossinline argFun: () -> Unit) {
        val obj = object : TestClass() {
            override fun some() = argFun()
        }
    }
    
    fun crossInlineTest(){
    	inlineTest3{
    		return // Error!!
    	}
    }

crossinline은 함수를 inline으로 선언했더라도 람다 함수에 return을 사용하지 못하게 하는 예약어이다. inline함수 내에서 다른 객체에 대입되어 이용되는 람다 함수에 아예 return을 사용할 수 없게 하는 것이다. crossinline으로 선언한 매개변수에 전달되는 람다 함수에 return을 사용하면 에러가 발생한다.

Label

람다 함수에서 return을 이용하여 자신이 대입되는 고차 함수의 수행을 끝내야 할 때가 있다. 단순히 return만 사용해서는 프로그램의 수행 흐름이 예기치 않게 흐를 수 있다.

    val array = arrayOf(1, -1, 2)
    fun arrayLoop(){
    	println("loop top..")
    	array.forEach{
    		if(it < 0) return
    		println(it)
    	}
    	println("loop bottom..")
    }
    
    // loop top..
    // 1

return이 forEach()가 아닌 arrayLoop()에서 발생하므로 의도와 맞지 않는다. 여기서 람다 함수만 벗어나게 하고 싶다면 label을 사용한다.

    val array = arrayOf(1, -1, 2)
    fun arrayLoop(){
    	println("loop top..")
    	array.forEach aaa@{
    		if(it < 0) return@aaa
    		println(it)
    	}
    	println("loop bottom..")
    }
    
    // loop top..
    // 1
    // 2
    // loop bottom..

코틀린에서는 개발자가 별도로 label을 지정하지 않아도 고차 함수에 자동으로 붙는 label이 있다.

    val array = arrayOf(1, -1, 2)
    fun arrayLoop(){
    	println("loop top..")
    	array.forEach{
    		if(it < 0) return@forEach
    		println(it)
    	}
    	println("loop bottom..")
    }
    
    // loop top..
    // 1
    // 2
    // loop bottom..

고차 함수는 자동으로 고차 함수 이름의 label이 추가되므로 함수명을 그대로 이용하여 label을 사용할 수 있다.

    inline fun hoTest(argFun: (String) -> Unit){
        // ...
    }
    
    fun labelTest(){
        hoTest { 
            return@hoTest
            // ...
        }
    }

고차 함수 이용시 익명 함수도 이용할 수 있는데 익명 함수에서의 return은 익명 함수를 벗어나게 하므로 label은 사용하지 않아도 된다.

    val array = arrayOf(1, -1, 2)
    fun arrayLoop(){
    	println("loop top..")
    	array.forEach(fun(value: Int){
    		if(value < 0) return
    		println(value)
    	})
    	println("loop bottom..")
    }
    
    // loop top..
    // 1
    // 2
    // loop bottom..

클로저

클로저(closure)는 프로그래밍 언어에서 흔히 함수의 스코프에 사용된 변수를 바인딩하기 위한 기법으로 소개한다. 함수가 호출될 때 발생하는 데이터를 함수 호출 후에도 그대로 유지해 이용하는 기법이다.

    fun closureTest(x :Int): (Int) -> Int{
    	println("argument $x")
    	return { it * x }
    }
    
    fun main(args: Array<String>){
    	val someFun1 = closureTest(2)
    	val someFun2 = closureTest(3)
    
    	println("${someFun1(10)}")
    	println("${someFun2(10)}")
    }

람다 함수에서 사용한 지역 변수가 유지되는 이유가 클로저이다. 함수 내에 선언된 데이터를 함수 종료 후에도 사용할 수 있게 바인딩 해주는 기법을 통칭한다. 람다 함수 내에서 사용한 closureTest() 함수의 지역 변수가 closureTest() 함수 종료 후에도 유지되는 것은 내부적으로 이 변수까지 포함(캡처링)해 람다 함수를 반환하기 때문이다.

    public static final Funtion1 closureTest(final int x){
    	String var1 = "argument " + x;
    	System.out.println(var1);
    	return (Function1)(new Function1() {
    		// $FF: synthetic method
    		// $FF: bridge method
    		public Object invoke(Object var1){
    			return this.invoke(((Number)var1).intValue());
    		}
    
    		public final int invoke(int it) {
    			return it * x;
    		}
    	});
    }

코틀린에서는 람다 함수에서 외부 함수의 데이터 접근뿐 아니라 변경도 가능하다. 자바에서는 람다에서 외부 변수가 final이기 때문에 참조하여 이용은 하되 변경은 불가능하다.

    fun closureTest2(): (Int) -> Unit{
    	var sum = 0
    	return {
    		for(i in 1..it){
    			sum += i
    		}
    	}
    }