정의
일반적으로 함수는 선언 -> 호출 -> 작동의 과정을 거친다.
인라인 함수는 일반 함수와 다르게 호출을 하면 선언된 위치로 가서 작동하지 않고 선언한 내용을 호출한 위치에 치환한다.
코드를 보면 이해하기 쉽다.
fun main() {
println(add(1, 2))
println(min(2, 1))
}
fun add(i: Int , b: Int): Int {
return i+b
}
inline fun min(i: Int, j: Int): Int{
return i-j
}
-result-
3
1
min()은 인라인, add()는 일반 함수로 생성했다. 결과를 보면 큰 차이가 없을 것이다. 하지만 자바로 디컴파일 하게 되면 컴파일러가 어떻게 처리하는지 알 수 있다.
public final class MainKt {
public static final void main() {
int var0 = add(1, 2); //<- 함수 호출
System.out.println(var0);
byte i$iv = 2; // <- 함수선언부를 내용으로 치환
int j$iv = 1;
int $i$f$min = false;
var0 = i$iv - j$iv;
System.out.println(var0);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final int add(int i, int b) {
return i + b;
}
public static final int min(int i, int j) {
int $i$f$min = 0;
return i - j;
}
}
add()는 호출됬지만 min()의 호출은 어디에서도 볼 수 없다. 대신 min()에 정의한 내용이 main()에 적혀있는 것을 볼 수 있다. 이것으로 인라인 함수는 선언 -> 호출 -> 작동 이 아니라 선언 -> 호출 -> 치환 -> 작동인 것을 알 수 있다. 인라인 함수는 C에서부터 있던 개념이다. (참고 : C언어 코딩 도장)
인라인 함수
고차 함수를 사용하면 런타임에 일부 불이익이 발생한다. 각 함수는 객체이고 함수의 몸체에서 접근하는 변수인 클로저를 캡처한다. 메모리 할당(함수 객체와 클래스 둘에 대해)과 가상 호출로 런타임 오버헤드가 증가한다. 쉽게 말하면 고차함수를 쓸 때마다 새로운 함수 객체를 생성한다는 의미다. 하지만 많은 경우 람다식을 인라인 해서 이런 종류의 오버헤드를 없앨 수 있다. 아래의 예제를 보면 이해하기 쉽다.
fun main() {
test { add(2, 3) }
test2 { add(2, 3) }
}
fun add(i: Int , b: Int): Int {
println("add = ${i+b}")
return i+b
}
inline fun test(a: () -> Unit){
a()
}
fun test2(a: () -> Unit){
a()
}
-result-
add = 5
add = 5
사용법은 간단하다. 고차 함수 앞에 inline 키워드를 붙여주면 된다. 그렇게 되면 고차 함수에서 선언한 내용을 호출한 부분에 치환한다. 컴파일되면 새로운 인스턴스를 생성하는 것이 아닌 add() 함수로 치환된다.
public final class MainKt {
public static final void main() {
int $i$f$test = false; //test()
int var1 = false;
add(2, 3);
test2((Function0)null.INSTANCE); //test2()
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final int add(int i, int b) {
String var2 = "add = " + (i + b);
System.out.println(var2);
return i + b;
}
public static final void test(@NotNull Function0 a) {
int $i$f$test = 0;
Intrinsics.checkNotNullParameter(a, "a");
a.invoke();
}
public static final void test2(@NotNull Function0 a) {
Intrinsics.checkNotNullParameter(a, "a");
a.invoke();
}
}
컴파일된 코드를 보면 알 수 있듯이 인라인 고차 함수인 test()를 호출했을 때 패러미터로 넘겨진 add()로 치환된 것을 볼 수 있다. 반면 일반 고차함수인 test2()를 호출하면 새로운 인스턴스가 생성되는 것을 볼 수 있다.
인라인 안 하기
인라인 함수에 전달되는 람다 중 일부만 인라인 되길 원할 경우, 함수 패러미터에 noinline 수식어를 붙이면 인라인 되는 것을 막을 수 있다.
fun main() {
test({add(3, 2)}, {min(3, 2)})
}
fun add(i: Int , b: Int): Int {
println("add = ${i+b}")
return i+b
}
fun min(i: Int, j: Int): Int{
println("min = ${ i - j }")
return i-j
}
inline fun test(a: () -> Int, noinline b: () -> Int){
a()
b()
}
-result-
add = 5
min = 1
함수 패러미터 b만 noinline 키워드를 붙여줬다.
public final class MainKt {
public static final void main() {
Function0 b$iv = (Function0)null.INSTANCE;
int $i$f$test = false;
int var2 = false;
add(3, 2);
b$iv.invoke();
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final int add(int i, int b) {
String var2 = "add = " + (i + b);
System.out.println(var2);
return i + b;
}
public static final int min(int i, int j) {
String var2 = "min = " + (i - j);
System.out.println(var2);
return i - j;
}
public static final void test(@NotNull Function0 a, @NotNull Function0 b) {
int $i$f$test = 0;
Intrinsics.checkNotNullParameter(a, "a");
Intrinsics.checkNotNullParameter(b, "b");
a.invoke();
b.invoke();
}
}
main()을 보면 noinline키워드를 붙인 b는 새 인스턴스를 생성했지만 a는 add()로 치환된 것을 볼 수 있다.
비-로컬 리턴
코틀린에서 함수나 익명 함수에서 나가려면 return을 사용해야 한다. 람다를 나가러면 라벨을 사용하는 것과 같은 맥락이다. 람다에서 순수 return은 금지하는데 람다가 둘러싼 함수를 리턴할 수 없기 때문이다.
만약, 람다를 전달한 함수가 인라인이 되면, 리턴도 인라인이 되기 때문에 가능하다.

사진의 코드를 실행하게 되면
C:\Users\hwan4\IdeaProjects\BackJoon\src\main.kt:9:9
Kotlin: 'return' is not allowed here
위와 같이 욕한다.
fun main() {
inlineTest {
println("inLine")
return
}
noInlineTest {
println("noInline")
return@noInlineTest
}
}
inline fun inlineTest(a: () -> Unit) {
a()
}
fun noInlineTest(a: () -> Unit) {
a()
}
-result-
inLine
inlineTest()는 인라인 함수라서 정상적으로 리턴이 된 모습을 볼 수 있다.
라벨을 통해서 리턴을 해줘도 이미 main()이 리턴 됐기 때문에 noInlineTest()는 출력되지 않는다.
두 함수의 순서를 바꾸게 되면 프린트 문이 두 개다 정상적으로 출력된다. 라벨을 통한 리턴은 람다를 리턴하기 때문에 하위 코드를 실행할 수 있기 때문이다.
이런 리턴을 비-로컬(non-local) 리턴이라고 부른다.
일부 인라인 함수는 파라미터로 그 함수에 전달한 람다를 함수 몸체가 아닌 다른 실행 컨텍스트에서 호출할 수 있다. 그런 경우 람다에서 비-로컬 흐름 제어를 허용하지 않는다. 이를 명시하려면 람다 파라미터에 crossinline 수식어를 붙여야 한다.

fun main() {
inlineTest {
println("inLine")
// return
}
}
inline fun inlineTest(crossinline a: () -> Unit) {
val f = object: Runnable{
override fun run() = a()
}
f.run()
}
-result-
inLine
구체화한(Reified) 타입 파라미터
종종 파라미터로 전달한 타입에 접근해야 할 때가 있다.
fun <T> TreeNode.findParentOfType(clazz: Class<T>): T?{
var p = parent
while (p != null && !clazz.isInstance(p)){
p = p.parent
}
@Suppress("UNCHECKED_CASE")
return p as T?
}
이 코드는 트리를 탐색하고 노드가 특정 타입인지 검사하기 위해 리플렉션을 사용한다. 잘되는데 안 이쁘다
treeNode.findParentOfType(MyTreeNode::class.java)
원하는 것은 아래 코드처럼 함수에 타입을 전달하는 것이다.
treeNode.findParentOfType<MyTreeNode>()
이를 위해 구체화한(reified) 타입 패러미터를 지원하며, 다음과 같은 코드를 작성할 수 있다.
inline fun <reified T> TreeNode.findParentOfType(): T?{
var p = parent
while (p != null && p !is T){
p = p.parent
}
return p as T?
}
reified의 장점은 함수 안에서 마치 일반 클래스처럼 타입 패러미터에 접근할 수 있다. 함수를 인라인 하기 때문에 리플렉션이 필요 없고! is나 as와 같은 보통의 연산자가 동작한다. 또한 구체화한 타입 파라미터에 대해 리플렉션을 사용할 수 있다
inline fun <reified T> memberOf() = T::class.members
fun main() {
println(memberOf<StringBuilder>().joinToString("\n"))
}
-result-
fun java.lang.StringBuilder.toString(): kotlin.String
fun java.lang.StringBuilder.append(kotlin.CharSequence!, kotlin.Int, kotlin.Int): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.CharArray!): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.CharArray!, kotlin.Int, kotlin.Int): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.Boolean): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.CharSequence!): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(java.lang.StringBuffer!): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.String!): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.Float): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.Double): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.Any!): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.Char): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.Int): java.lang.StringBuilder!
fun java.lang.StringBuilder.append(kotlin.Long): java.lang.StringBuilder!
fun java.lang.StringBuilder.compareTo(java.lang.StringBuilder!): kotlin.Int
fun java.lang.StringBuilder.indexOf(kotlin.String!, kotlin.Int): kotlin.Int
fun java.lang.StringBuilder.indexOf(kotlin.String!): kotlin.Int
fun java.lang.StringBuilder.lastIndexOf(kotlin.String!): kotlin.Int
fun java.lang.StringBuilder.lastIndexOf(kotlin.String!, kotlin.Int): kotlin.Int
fun java.lang.StringBuilder.replace(kotlin.Int, kotlin.Int, kotlin.String!): java.lang.StringBuilder!
fun java.lang.StringBuilder.delete(kotlin.Int, kotlin.Int): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.CharSequence!, kotlin.Int, kotlin.Int): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.Boolean): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.Int): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.Char): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.CharArray!, kotlin.Int, kotlin.Int): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.Any!): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.String!): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.CharArray!): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.CharSequence!): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.Double): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.Float): java.lang.StringBuilder!
fun java.lang.StringBuilder.insert(kotlin.Int, kotlin.Long): java.lang.StringBuilder!
fun java.lang.StringBuilder.appendCodePoint(kotlin.Int): java.lang.StringBuilder!
fun java.lang.StringBuilder.deleteCharAt(kotlin.Int): java.lang.StringBuilder!
fun java.lang.StringBuilder.reverse(): java.lang.StringBuilder!
fun java.lang.StringBuilder.readObject(java.io.ObjectInputStream!): kotlin.Unit
fun java.lang.StringBuilder.writeObject(java.io.ObjectOutputStream!): kotlin.Unit
fun java.lang.StringBuilder.equals(kotlin.Any?): kotlin.Boolean
fun java.lang.StringBuilder.hashCode(): kotlin.Int
fun java.lang.StringBuilder.append(java.lang.AbstractStringBuilder!): java.lang.AbstractStringBuilder!
fun java.lang.StringBuilder.get(kotlin.Int): kotlin.Char
fun java.lang.StringBuilder.subSequence(kotlin.Int, kotlin.Int): kotlin.CharSequence
fun java.lang.StringBuilder.codePoints(): java.util.stream.IntStream!
fun java.lang.StringBuilder.chars(): java.util.stream.IntStream!
fun java.lang.StringBuilder.getChars(kotlin.Int, kotlin.Int, kotlin.CharArray!, kotlin.Int): kotlin.Unit
fun java.lang.StringBuilder.compareTo(java.lang.AbstractStringBuilder!): kotlin.Int
fun java.lang.StringBuilder.codePointAt(kotlin.Int): kotlin.Int
fun java.lang.StringBuilder.codePointBefore(kotlin.Int): kotlin.Int
fun java.lang.StringBuilder.codePointCount(kotlin.Int, kotlin.Int): kotlin.Int
fun java.lang.StringBuilder.offsetByCodePoints(kotlin.Int, kotlin.Int): kotlin.Int
fun java.lang.StringBuilder.getBytes(kotlin.ByteArray!, kotlin.Int, kotlin.Byte): kotlin.Unit
fun java.lang.StringBuilder.substring(kotlin.Int, kotlin.Int): kotlin.String!
fun java.lang.StringBuilder.substring(kotlin.Int): kotlin.String!
fun java.lang.StringBuilder.isLatin1(): kotlin.Boolean
fun java.lang.StringBuilder.getValue(): kotlin.ByteArray!
fun java.lang.StringBuilder.getCoder(): kotlin.Byte
fun java.lang.StringBuilder.setLength(kotlin.Int): kotlin.Unit
fun java.lang.StringBuilder.capacity(): kotlin.Int
fun java.lang.StringBuilder.ensureCapacity(kotlin.Int): kotlin.Unit
fun java.lang.StringBuilder.trimToSize(): kotlin.Unit
fun java.lang.StringBuilder.setCharAt(kotlin.Int, kotlin.Char): kotlin.Unit
fun java.lang.StringBuilder.initBytes(kotlin.CharArray!, kotlin.Int, kotlin.Int): kotlin.Unit
var java.lang.StringBuilder.value: kotlin.ByteArray!
var java.lang.StringBuilder.coder: kotlin.Byte
var java.lang.StringBuilder.count: kotlin.Int
val java.lang.StringBuilder.length: kotlin.Int
val serialVersionUID: kotlin.Long
배열의 출력 형식이 변경된 것을 볼 수 있다.
인라인 프로퍼티
지원 필드가 없는 프로퍼티 접근자에 inline 수식어를 사용할 수 있다. 개별 프로퍼티 접근자에 사용한다.
fun main(){
println(Test().foo)
println(Test().foo1)
Test().bar = 3
Test().bar2 = 5
}
class Test(){
val foo: Int
inline get() = 0
val foo1: Int
get() = 0
inline var bar: Int
get() = 2
set(value) { println(value) }
var bar2: Int
get() = 2
set(value) { println(value) }
}
-result-
0
0
3
5
코드와 결과를 보면 무슨 차인지 모른다. 자바로 변환된 코드를 보자
public final class Test {
public final int getFoo() {
int $i$f$getFoo = 0;
return 0;
}
public final int getFoo1() {
return 0;
}
public final int getBar() {
int $i$f$getBar = 0;
return 2;
}
public final void setBar(int value) {
int $i$f$setBar = 0;
System.out.println(value);
}
public final int getBar2() {
return 2;
}
public final void setBar2(int value) {
System.out.println(value);
}
}
public final class MainKt {
public static final void main() {
new Test();
int $i$f$getFoo = false;
byte var0 = 0;
System.out.println(var0); //foo
int var3 = (new Test()).getFoo1();
System.out.println(var3); //foo1
new Test();
int value$iv = 3;
int $i$f$setBar = false;
System.out.println(value$iv); //bar
(new Test()).setBar2(5); //bar2
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
확연히 차이가 난다. inline을 붙인 프로퍼티 들은 인스턴스를 생서해서 출력하는 것이 아닌 코드가 치환돼서 출력되고 있다.
'코틀린' 카테고리의 다른 글
스코프 함수 (0) | 2022.07.24 |
---|---|
16. 코루틴 기본 (0) | 2022.06.13 |
14. 고차함수와 람다 (0) | 2022.06.12 |
13. 함수 (0) | 2022.06.12 |
12. 위임 (0) | 2022.06.12 |