반응형
인라인(inline) 코드란?
- 본래라면 컴파일 시 별도의 함수 or 클래스로 만들어져야하는 것을 호출하는 본문 안에서 정의하도록 한 것
- 인라인 클래스는 주로 Wrapping Class를 만들 때 사용한다
- Boxing 과정이 빈번하게 이뤄지면 인라인 클래스를 사용하는 의미가 없어진다
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
인라인 코드는 무엇인가?
본래라면 컴파일 시 별도의 함수 or 클래스로 만들어져야하는 것을 호출하는 본문 안에서 정의하도록 한 것이다.
인라인 일때와 아닐 때 코드 비교
- 인라인을 사용하지 않고 정의한 함수를 호출
fun fn(n1: Int, n2: Int): Int {
return n1 + n2
}
fun main() {
val result = fn(1, 2)
println(result)
}
- 컴파일 결과
// 코틀린에서 정의한 것과 똑같이 별도의 함수가 생성된다
public static final int fn(int n1, int n2) {
return n1 + n2;
}
public static final void main() {
int result = fn(1, 2);
boolean var1 = false;
System.out.println(result);
}
- 인라인으로 정의한 함수를 호출
inline fun fn(n1: Int, n2: Int): Int {
return n1 + n2
}
fun main() {
val result = fn(1, 2)
println(result)
}
- 컴파일 결과
public static final int fn(int n1, int n2) {
int $i$f$fn = 0;
return n1 + n2;
}
// 정의한 함수를 main 안에 넣어서 실행하준다
public static final void main() {
byte n1$iv = 1;
int n2$iv = 2;
int $i$f$fn = false;
int result = n1$iv + n2$iv;
boolean var4 = false;
System.out.println(result);
}
이처럼 인라인 코드는 정의한 클래스 / 함수를 호출한 곳에 넣어주기 때문에
함수형 인자를 받을 때 사용하면 많은 성능 개선이 이루어진다
주의사항 및 팁
- 인라인 함수를 사용하면 컴파일된 코드가 길어진다
- 대상 함수가 상대적으로 작을 때 더욱 효과적이다
- 코틀린 표준 라이브러리가 제공하는 여러 고차 함수 중 상당수가 인라인 함수이다
- 다음과 같은 고차 함수에 적용하면 효과적이다
fun main(args: Array<String>) {
println(indexOf(intArrayOf(4, 3, 2, 1)){ it< 3}) // 2
// inline indexOf() 함수가 컴파일된 모습을 보면 아래와 같다
val number =intArrayOf(4,3,2,1)
var index = -1
for (i in number.indices){
if (number[i] < 3){
index = i
break
}
}
println(index) // 2
}
inline fun indexOf(numbers: IntArray, condition: (Int) -> Boolean): Int {
for (i in numbers.indices) {
if (condition(numbers[i])) return i
}
return -1
}
하지만 인라인 함수는 다음과 같은 제약이 따른다
- null이 될 수 있는 인자를 받을 수 없다
- private 인자를 받을 수 없다
- 캡슐화를 해야할 경우 사용할 수 없다 = 이유는 해당 함수가 클래스 안으로 들어가기 때문이다
인라인 클래스는 왜 필요할까?
인라인 클래스는 주로 Wrapping Class를 만들 때 사용한다
예를 들어 UnixMillis같은 기본적으로 Long 형이지만 이를 java.util.Calendar로 변환 한다던가 날짜와 시간에 대한 변환을
하는 작업을 하고싶다고 가정한다
첫 번째 방법은 일반적인 Wrapper Class를 만드는 것이다.
class UnixMillis(private val millis: Long) {
// 받은 mills 데이터를 Calendar로 변환한다
fun toCalendar(): Calendar {
return Calendar.getInstance().also {
it.timeInMillis = millis
}
}
}
이를 디컴파일 하면 다음과 같은 자바 코드를 얻을 수 있다
public static final void main() {
// UnixMillis 객체를 만들고
UnixMillis unix = new UnixMillis(System.currentTimeMillis());
// 해당 객체를 사용해서 Calendar에 대한 변환을 실시한다
Calendar calendar = unix.toCalendar();
// 이는 매번 Calendar 객체를 만들기 때문에 최적화된 방법이라 할 수 없다
}
이러한 방법은 toCalendar() 함수를 사용할 때 마다 객체를 만들기 때문에 부담이 간다
이번에는 인라인 클래스로 만들어서 확인한다
// 그냥 앞에 inline만 붙이고 끝
inline class UnixMillis(private val millis: Long) {
fun toCalendar(): Calendar {
return Calendar.getInstance().also {
it.timeInMillis = millis
}
}
}
이를 디컴파일 하여 자바 코드로 바꾸면 다음과 같다
public static final void main() {
// UnixMillis 객체가 아닌 Long 객체를 만듬
// 또한 파라미터를 주생성자(constructor)로 전달하는 것을 알 수 있다
long unix = UnixMillis.constructor-impl(System.currentTimeMillis());
// 호출한 함수 내의 코드가 그대로 가져옴(최적화만 했음)
Calendar calendar = UnixMillis.toCalendar-impl(unix);
}
인라인 클래스로 사용하면 매번 객체를 만들지 않는 것을 확인할 수 있다.
주의사항
- 인라인 클래스는 주 생성자로 한 개의 인자만 가져야하며 이를 var나 val을 붙여줘야한다
- 인라인 클래스는 init을 가질 수 없음
- backing field(ex: lateinit, delegated 등)을 가질 수 없음
- 코틀린 1.5 이후에는 value 클래스로도 사용할 수 있다
- JVM 백엔드의 경우에는 반드로 @JvmInline을 붙여줘야한다
인라인 클래스를 함부로 사용하면 안되는 이유
앞에서 말했다시피 인라인 클래스는 Wrapping Class를 만들 때 주로 사용된다
Wrapping Class는 사용될 때 내부에 있는 값을 boxing, unboxing 한다.
아래의 인라인 클래스로 예시를 들어보면
inline class Password(val value: String)
- Boxing은 value 를 Password객체로 만들어 저장한다는 의미
- Unboxing은 Password에서 value를 가져온다는 의미이다
- 즉, Boxing 과정이 빈번하게 이뤄지면 인라인 클래스를 사용하는 의미가 없어진다
Boxing을 여러 번 하는 예시
interface I
inline class Foo(val i: Int) : I
fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}
// id 함수는 그냥 받은 값을 그대로 반환하는 함수임
fun <T> id(x: T): T = x
fun main() {
val f = Foo(42)
// unboxed: f가 Foo의 객체이기 때문에 그대로 사용
// 그렇기 때문에 f를 사용할 때 다시 Boxing할 필요 없이 바로 사용 가능
asInline(f)
// 기본적으로 함수 인자로 인라인 클래스 객체를 그대로 받지 않는 경우에는 전부
// 한 번씩 boxing을 한 다음에 다시 unboxing을 해야한다
// boxed: 제네릭 타입 T를 사용하여 Foo 객체를 받기 때문에 Boxing 필요
// 그렇기 때문에 한 번 Boxing을 하고 다시 Unboxing을 해야 f를 사용 가능
asGeneric(f)
// boxed: Foo가 상속받은 I를 인자로 받는 함수이기 때문에 Boxing 필요
asInterface(f)
// boxed: null을 받을 수 있는 경우에도 Boxing을 한 다음에 Unboxing을 실시해야한다
asNullable(f)
// 'f'를 넣을 때 한 번 boxing 한다 ('id' 함수로 보내질 때)
// 그리고 'id' 함수에서 값을 반환할 때 Unboxing 한다
// 마지막에 'c' 는 unboxed된 f 값을 가지게된다
val c = id(f)
}
반응형
'코틀린' 카테고리의 다른 글
안드로이트 코틀린 Reflection(리플렉션) 기초 정의 (0) | 2023.03.09 |
---|---|
코틀린에서 자주 사용하는 어노테이션(Annotation)@ 정리-1 (0) | 2023.03.06 |
코틀린에서 변성(variance)이란 무엇인가 - 상세 설명 (0) | 2023.02.18 |
코틀린 확장함수 Scope함수 apply, with, let, also, run 이란? (0) | 2023.01.16 |
안드로이드 코틀린은 같은 변수를 계속 만들면 재활용할까? (0) | 2023.01.08 |
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
댓글