rangeTo, iterator 함수를 정의하면 범위를 만들거나 컬렉션과 배열의 원소를 이터레이션 가능
classCustomRange(val start: Int, val end: Int) {operatorfunrangeTo(other: CustomRange): IntRange {return start..other.end }operatorfuniterator(): Iterator<Int> {return (start..end).iterator() }}@Testfun`test rangeTo and iterator`() {val range =CustomRange(1, 5)val result =mutableListOf<Int>()for (i in range) { // iterator() result.add(i) }assertEquals(listOf(1, 2, 3, 4, 5), result)val range2 =CustomRange(6, 10)val combinedRange = range..range2 // rangeTo()assertEquals((1..10), combinedRange)}
구조 분해 선언을 통해 한 객체의 상태를 분해해서 여러 변수에 대입 가능
함수가 여러 값을 한꺼번에 반환해야 하는 경우 유용
dataclassPerson(val name: String, val age: Int)@Testfun`test destructuring declaration`() {val person =Person("Alice", 30)val (name, age) = personassertEquals("Alice", name)assertEquals(30, age)}
위임 프로퍼티를 통해 프로퍼티 값을 저장, 초기화, 읽거나 변경할 때 사용하는 로직을 재활용 가능
위임 프로퍼티는 프레임워크를 만들 때 아주 유용
Delegates.observable 함수를 사용하면 프로퍼티 변경 관찰자를 쉽게 추가 가능
classUser {var name: StringbyDelegates.observable("<no name>") { _, old, new ->println("Name changed from $old to $new") }}@Testfun`test delegated property`() {val user =User()assertEquals("<no name>", user.name) user.name ="Alice"assertEquals("Alice", user.name) user.name ="Bob"assertEquals("Bob", user.name)}
표준 라이브러리 함수인 lazy를 통해 지연 초기화 프로퍼티를 쉽게 구현
classLazyInitialization {val lazyValue: Stringbylazy {println("Computed!")"Hello" }}@Testfun`test lazy property`() {val instance =LazyInitialization()assertEquals("Hello", instance.lazyValue) // 최초로 접근 시 초기화}
📕 고차 함수 요약
함수 타입을 사용해 함수에 대한 참조를 담는 변수, 파라미터, 반환 값을 만들 수 있다.
// 함수 타입을 사용한 변수val add: (Int, Int) -> Int= { a, b -> a + b }// 함수 타입을 파라미터로 받는 함수funoperate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {returnoperation(a, b)}// 함수 타입을 반환 값으로 사용하는 함수fungetOperation(type: String): (Int, Int) -> Int {returnwhen (type) {"add"-> { a, b -> a + b }"multiply"-> { a, b -> a * b }else-> { _, _ ->0 } }}@Testfun`test function type as variable`() {assertEquals(5, add(2, 3))}@Testfun`test function type as parameter`() {val result =operate(2, 3, add)assertEquals(5, result)}@Testfun`test function type as return value`() {val addOperation =getOperation("add")val multiplyOperation =getOperation("multiply")assertEquals(5, addOperation(2, 3))assertEquals(6, multiplyOperation(2, 3))}
고차 함수는 다른 함수를 인자로 받거나 함수를 반환한다.
// 함수를 인자로 받는 고차 함수funapplyOperation(a: Int, b: Int, operation: (Int, Int) -> Int): Int {returnoperation(a, b)}// 함수를 반환하는 고차 함수fungetOperation(type: String): (Int, Int) -> Int {returnwhen (type) {"add"-> { a, b -> a + b }"subtract"-> { a, b -> a - b }else-> { _, _ ->0 } }}@Testfun`test applyOperation with add`() {val result =applyOperation(3, 4) { a, b -> a + b }assertEquals(7, result)}@Testfun`test applyOperation with subtract`() {val result =applyOperation(10, 4) { a, b -> a - b }assertEquals(6, result)}@Testfun`test getOperation with add`() {val addOperation =getOperation("add")val result =addOperation(3, 4)assertEquals(7, result)}@Testfun`test getOperation with subtract`() {val subtractOperation =getOperation("subtract")val result =subtractOperation(10, 4)assertEquals(6, result)}
인라인 함수를 컴파일할 때 컴파일러는 그 함수의 본문과 그 함수에게 전달된 람다의 본문을 컴파일한 바이트 코드를 모든 함수 호출 지점에 삽입해준다.
inlinefunperformOperation(a: Int, b: Int, operation: (Int, Int) -> Int): Int {returnoperation(a, b)}@Testfun`test performOperation with add`() {val result =performOperation(3, 4) { x, y -> x + y }assertEquals(7, result)}@Testfun`test performOperation with multiply`() {val result =performOperation(3, 4) { x, y -> x * y }assertEquals(12, result)}
고차 함수를 사용하면 컴포넌트를 이루는 각 부분의 코드를 더 잘 재사용할 수 있다.
또 고차 함수를 활용해 강력한 제네릭 라이브러리를 만들 수 있다.
// 고차 함수: 두 리스트를 합치는 함수fun <T> combineLists(list1: List<T>, list2: List<T>, combine: (T, T) -> T): List<T> {return list1.zip(list2, combine)}// 고차 함수: 리스트의 모든 요소에 연산을 적용하는 함수fun <T, R> mapList(list: List<T>, transform: (T) -> R): List<R> {return list.map(transform)}@Testfun`test combineLists with addition`() {val list1 =listOf(1, 2, 3)val list2 =listOf(4, 5, 6)val result =combineLists(list1, list2) { a, b -> a + b }assertEquals(listOf(5, 7, 9), result)}@Testfun`test combineLists with string concatenation`() {val list1 =listOf("a", "b", "c")val list2 =listOf("d", "e", "f")val result =combineLists(list1, list2) { a, b -> a + b }assertEquals(listOf("ad", "be", "cf"), result)}@Testfun`test mapList with square`() {val list =listOf(1, 2, 3, 4)val result =mapList(list) { it * it }assertEquals(listOf(1, 4, 9, 16), result)}@Testfun`test mapList with string length`() {val list =listOf("apple", "banana", "cherry")val result =mapList(list) { it.length }assertEquals(listOf(5, 6, 6), result)}
인라인 함수에서는 람다 안에 있는 return 문이 바깥쪽 함수를 반환시키는 non-local return을 사용할 수 있다.
inlinefunperformOperationWithNonLocalReturn(a: Int, b: Int, operation: (Int, Int) -> Int): Int {returnoperation(a, b)}@Testfun`test non-local return in inline function`() {val result1 =performOperationWithNonLocalReturn(3, 4) { x, y ->if (x > y) return@performOperationWithNonLocalReturn x x + y }assertEquals(7, result1)val result2 =performOperationWithNonLocalReturn(3, 1) { x, y ->if (x > y) return@performOperationWithNonLocalReturn x x + y }assertEquals(3, result2)}
무명 함수는 람다 식을 대신할 수 있으며 return 식을 처리하는 규칙이 일반 람다 식과는 다르다.
본문 여러 곳에서 return 해야 하는 코드 블록을 만들어야 한다면 람다 대신 무명 함수를 쓸 수 있다.
funperformOperationWithAnonymousFunction(a: Int, b: Int, operation: (Int, Int) -> Int): Int {returnoperation(a, b)}@Testfun`test anonymous function with multiple returns`() {val result =performOperationWithAnonymousFunction(3, 4, fun(x, y): Int {if (x > y) return xreturn x + y })assertEquals(7, result)val result2 =performOperationWithAnonymousFunction(5, 2, fun(x, y): Int {if (x > y) return xreturn x + y })assertEquals(5, result2)}
산술 연산자 오버로딩
코틀린에서 관례를 사용하는 가장 단순한 예는 산술 연산자
자바에서는 원시 타입에 대해서만 산술 연산자 사용 가능하고, String에 대해 + 연산자를 사용 가능
이항 산술 연산 오버로딩
operator 변경자를 추가해 plus 함수를 선언하고 나면 + 기호로 두 Point 객체를 더할 수 있음
// case 1. 연산자를 자체 함수로 정의하기dataclassPoint(val x: Int, val y: Int) {operatorfunplus(other: Point): Point {returnPoint(x + other.x, y + other.y) }}// case 2. 연산자를 확장 함수로 정의하기dataclassPoint2(val x: Int, val y: Int)operatorfunPoint2.plus(other: Point2): Point2 {returnPoint2(x + other.x, y + other.y)}@Testfun`이항 산술 연산 오버로딩`() {val p1 =Point(10, 20)val p2 =Point(30, 40)assertEquals(Point(40, 60), p1 + p2)val p3 =Point2(10, 20)val p4 =Point2(30, 40)assertEquals(Point2(40, 60), p3 + p4)}
ℹ️ 오버로딩 가능한 이항 산술 연산자
Expression
Function name
a * b
times
a / b
div
a % b
mod
a + b
plus
a - b
minus
연산자를 정의할 때 두 피연산자가(연산자 함수의 두 파라미터)가 같은 타입일 필요는 없다.
또는 연산자 함수의 반환 타입이 꼭 두 피연산자 중 하나와 일치해야만 하는 것도 아니다.
dataclassPoint(val x: Int, val y: Int)// case 1. 두 피연산자가 다른 연산자 정의// Point, Double 타입의 피연산자를 받아 Point 반환operatorfunPoint.times(scale: Double): Point {returnPoint((x * scale).toInt(), (y * scale).toInt())}// case 2. 반환 타입이 피연산자와 다른 연산자 정의// Char, Int 타입의 피연산자를 받아 String 반환operatorfunChar.times(count: Int): String {returntoString().repeat(count)}@Testfun`연산자 정의`() {val point =Point(2, 3)val scaledPoint = point *2.5assertEquals(Point(5, 7), scaledPoint)val result ='a'*3assertEquals("aaa", result)}
복합 대입 연산자 오버로딩
+=, -= 등의 연산자는 복합 대입(compound assignment) 연산자라 불림
operatorfunPoint.plus(other: Point): Point {returnPoint(x + other.x, y + other.y)}@Testfun`복합 대입 연산자`() {var point =Point(1, 2) point +=Point(3, 4)assertEquals(Point(4, 6), point)}
dataclassPoint(val x: Int, val y: Int)dataclassRectangle(val upperLeft: Point, val lowerRight: Point)operatorfunRectangle.contains(p: Point): Boolean {// 20 in (10 <= true < 50) && 30 in (20 <= true < 50)return p.x in upperLeft.x until lowerRight.x && p.y in upperLeft.y until lowerRight.y}@Testfun`in 관례`() {val rect =Rectangle(Point(10, 20), Point(50, 50))assertTrue(Point(20, 30) in rect)assertFalse(Point(5, 5) in rect)}
operatorfunClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =object : Iterator<LocalDate> {var current = startoverridefunhasNext() = current <= endInclusiveoverridefunnext() = current.apply { current =plusDays(1) } }@Testfun`for 루프를 위한 iterator 관례`() {val newYear = LocalDate.ofYearDay(2024, 1)val daysOff = newYear.minusDays(1)..newYear/** * 2023-12-31 * 2024-01-01 */for (dayOff in daysOff) { println(dayOff) }}
구조 분해 선언과 component 함수
구조 분해를 사용하면 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화 가능
dataclassPoint(val x: Int, val y: Int)@Testfun`구조 분해 선언`() {val p =Point(10, 20)val (x, y) = passertEquals(10, x)assertEquals(20, y)}
구조 분해 선언은 함수에서 여러 값을 반환할 때 유용
👎🏻 여러 값을 한꺼번에 반환해야 하는 함수가 있다면 반환해야 하는 모든 값이 들어갈 데이터 클래스를 정의하고 함수의 반환 타입을 그 데이터 클래스로 바꿔주어야 한다.
👍🏼 구조 분해 선언 구문을 사용하면 이런 함수가 반환하는 값을 쉽게 풀어서 여러 변수에 넣을 수 있다.
dataclassNameComponents(val name: String,val extension: String)funsplitFilename(fullName: String): NameComponents {val result = fullName.split('.', limit =2)returnNameComponents(result[0], result[1]) // 함수에서 데이터 클래스의 인스턴스를 반환}@Testfun`component 함수`() {val (name, ext) =splitFilename("example.kt") // 구조 분해 선언 구문을 사용해 데이터 클래스를 해체assertEquals("example", name)assertEquals("kt", ext)}
구조 분해 선언과 루프
함수 본문 내의 선언문 뿐 아니라,
변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해 선언을 사용 가능
funprintEntries(map: Map<String, String>) {// 구조 분해 선언for ((key, value) in map) {println("$key -> $value") }}@Testfun`구조 분해 선언과 루프`() {/** * Oracle -> Java * JetBrains -> Kotlin */val map =mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")printEntries(map)}
두 가지 코틀린 관례를 활용한 예시
객체를 이터페이션하는 관례
구조 분해 선언
@Testfun`코틀린 관례`() {val map =mapOf(1 to "one", 2 to "two", 3 to "three")val keys =mutableListOf<Int>()val values =mutableListOf<String>()for (entry in map.entries) {// 코틀린 라이브러리는 Map.Entry에 대한 확장 함수로 component1, component2 제공val key = entry.component1()valvalue= entry.component2() keys.add(key) values.add(value) }assertEquals(listOf(1, 2, 3), keys)assertEquals(listOf("one", "two", "three"), values)}
프로퍼티 접근자 로직 재활용: 위임 프로퍼티
위임 프로퍼티(delegated property)를 사용하면 값을 뒷받침하는 필드에 단순히 저장하는 것보다
더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현 가능하다.
또한, 그 과정에서 접근자 로직을 매번 재구현할 필요도 없다.
위임은 객체가 직접 작업을 수행하지 않고, 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴을 말한다.
이때 작업을 처리하는 도우미 객체를 위임 객체라고 부른다.
위임 프로퍼티 사용: by lazy()를 사용한 프로퍼티 초기화 지연
// AS-IS@Testfun`**AS-IS by lazy**`() {dataclassEmail(val address: String)classPerson(val name: String) {// 데이터를 저장하고 emails의 위임 객체 역활을 하는 _emails 프로퍼티privatevar _emails: List<Email>? =nullval emails: List<Email>get() {if (_emails ==null) { _emails =loadEmails(this) // 최초 접근 시 이메일 가져오기 }return _emails!!// 저장해 둔 데이터가 있으면 그 데이터를 반환 }privatefunloadEmails(person: Person): List<Email> {println("Loading emails for ${person.name}")returnlistOf(Email("alice@example.com"),Email("alice.work@example.com") ) } }val p =Person("Alice")assertEquals(listOf(Email("alice@example.com"),Email("alice.work@example.com") ), p.emails) // 최초로 emails를 읽을 때 단 한번만 이메일을 가져온다.assertEquals(listOf(Email("alice@example.com"),Email("alice.work@example.com") ), p.emails)}// TO-BE/* * 지연 초기화해야 하는 프로퍼티가 많아지면 코드가 복잡해지고, * 스레드 안전하지 않아서 언제나 제대로 작동한다고 말할 수 없다. * 이 경우 위임 프로퍼티를 사용하면 훨씬 더 간편해진다. */@Testfun`by lazy`() {dataclassEmail(val address: String)classPerson(val name: String) {// lazy 함수는 코틀린 관례에 맞는 시그니처의 getValue 메소드가 들어있는 객체를 반환// 따라서 lazy를 by 키워드와 함께 사용해 위임 프로퍼티를 만들 수 있다.val emails bylazy { loadEmails(this) }privatefunloadEmails(person: Person): List<Email> {println("Loading emails for ${person.name}")returnlistOf(Email("alice@example.com"),Email("alice.work@example.com") ) } }val p =Person("Alice")val emails1 = p.emails // 최초로 emails를 읽을 때 단 한번만 이메일을 가져온다.val emails2 = p.emailsassertEquals(emails1, emails2)assertEquals(listOf(Email("alice@example.com"),Email("alice.work@example.com") ), emails1)}
고차 함수 정의
고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수
코틀린에서는 람다나 함수 참조를 사용해 함수를 값으로 표현 가능
따라서 고차 함수는 람다나 함수 참조를 인자로 넘길 수 있거나, 람다나 함수 참조를 반환하는 함수
그냥 함수를 정의한다면 함수의 파라미터 목록 뒤에 오는 Unit 반환 타입 지정을 생략해도 되지만,
⚠️ 함수 타입을 선언할 때는 반환 타입을 반드시 명시해야 하므로 Unit을 필수로 명시
인자로 받은 함수 호출
인자로 받은 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 동일
funtwoAndThree(operation: (Int, Int) -> Int): Int { // 함수 타입의 인자를 받는 함수val result =operation(2, 3)return result}@Testfun`인자로 받은 함수 호출`() {assertEquals(5, twoAndThree { a, b -> a + b })assertEquals(6, twoAndThree { a, b -> a * b })}
디폴트 값을 지정한 함수 타입 파라미터나 널이 될 수 있는 함수 타입 파라미터
파라미터를 함수 타입으로 선언할 때도 디폴트 값을 정할 수 있다.
fun <T> Collection<T>.joinToString( separator: String=", ", prefix: String="", postfix: String="", transform: (T) -> String= { it.toString() } // 함수 타입 파라미터를 선언하면서 람다를 디폴트 값으로 지정): String {val result =StringBuilder(prefix)for ((index, element) inthis.withIndex()) {if (index >0) result.append(separator) result.append(transform(element)) // transform 파라미터로 받은 함수를 호출 } result.append(postfix)return result.toString()}@Testfun`파라미터를 함수 타입으로 선언할 때도 디폴트 값 설정 가능`() {val letters =listOf("Alpha", "Beta")assertEquals("Alpha, Beta", letters.joinToString()) // 디폴트 변환 함수 사용assertEquals("alpha, beta", letters.joinToString { it.lowercase() }) // 람다를 인자로 전달assertEquals("alpha! beta! ", letters.joinToString(separator ="! ", postfix ="! ", transform = { it.lowercase() }) ) // 이름 붙은 인자 구문을 사용해 람다를 포함하는 여러 인자를 전달}
함수를 함수에서 반환
다른 함수를 반환하는 함수를 정의하려면 함수의 반환 타입으로 함수 타입을 지정
함수를 반환하려면 return 식에 람다나 멤버 참조나 함수 타입의 값을 계산하는 식 등을 넣으면 된다.
dataclassPerson(val firstName: String,val lastName: String,val phoneNumber: String?)classContactListFilters {var prefix: String=""var onlyWithPhoneNumber: Boolean=falsefungetPredicate(): (Person) -> Boolean { // 함수를 반환하는 함수를 정의한다.val startsWithPrefix = { p: Person -> p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix) }if (!onlyWithPhoneNumber) {return startsWithPrefix // 함수 타입의 변수 반환 }return { // 람다 반환startsWithPrefix(it)&& it.phoneNumber !=null } }}@Testfun`함수를 함수에서 반환`() {val contacts =listOf(Person("Dmitry", "Jemerov", "123-4567"),Person("Svetlana", "Isakova", null) )val contactListFilters =ContactListFilters()with(contactListFilters) { prefix ="Dm" onlyWithPhoneNumber =true }assertEquals(listOf(Person("Dmitry", "Jemerov", "123-4567")), contacts.filter(contactListFilters.getPredicate()) // 반환된 람다 적용 )with(contactListFilters) { prefix ="Sv" onlyWithPhoneNumber =false }assertEquals(listOf(Person("Svetlana", "Isakova", null)), contacts.filter(contactListFilters.getPredicate()) // 반환된 함수 타입 변수 적용 )}
람다를 활용한 중복 제거
함수 타입과 람다 식은 재활용하기 좋은 코드를 만들 때 쓸 수 있는 훌륭한 도구
웹 사이트 방문 기록을 분석하는 예시
@Testfun`람다를 활용한 중복 제거`() {/** * Case 01. 중복이 발생하는 코드 */val averageWindowsDuration = log .filter { it.os == OS.WINDOWS }.map(SiteVisit::duration).average()val averageMacDuration = log .filter { it.os == OS.MAC }.map(SiteVisit::duration).average()assertEquals(23.0, averageWindowsDuration)assertEquals(22.0, averageMacDuration)/** * Case 02. 함수를 사용하여 일반 함수를 통해 중복을 줄이기 */funList<SiteVisit>.averageDurationFor(os: OS) =filter { it.os == os }.map(SiteVisit::duration).average()assertEquals(23.0, log.averageDurationFor(OS.WINDOWS))assertEquals(22.0, log.averageDurationFor(OS.MAC))/** * Case 02. 고차 함수를 이용하여 함수를 확장 * > 특정 OS의 평균 방문 시간을 구하고 싶다거나, 특정 페이지 평균 방문 시간을 구하고 싶을 경우 */funList<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =filter(predicate).map(SiteVisit::duration).average()// 모바일 디바이스(IOS, 안드로이드)의 평균 방문 시간assertEquals(12.15, log.averageDurationFor { it.os insetOf(OS.ANDROID, OS.IOS) })// IOS 사용자의 /signup 페이지 평균 방문 시간assertEquals(8.0, log.averageDurationFor { it.os == OS.IOS && it.path =="/signup" })}
인라인 함수: 람다의 부가 비용 없애기
inline 변경자를 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을
함수 본문에 해당하는 바이트 코드로 바꿔준다.
인라이닝이 작동하는 방식
특정 함수를 inline 선언하면 그 함수의 본문이 인라인
함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트 코드로 컴파일
@Testfun`test inline function`() {/* * inline 변경자를 사용한 함수 호출 시 컴파일러가 해당 함수 호출을 실제 함수 본문으로 대체 * 함수 호출의 오버헤드를 줄이고 성능을 향상 */val addition =inlineFunction { a, b -> a + b }assertEquals(5, addition)// 컴파일러는 val addition = { a: Int, b: Int -> a + b }(2, 3) 로 변환assertEquals(5, { a: Int, b: Int -> a+ b }(2, 3))val multiplication =inlineFunction { a, b -> a * b }assertEquals(6, multiplication)// 컴파일러는 val multiplication = { a: Int, b: Int -> a * b }(2, 3) 로 변환assertEquals(6, { a: Int, b: Int -> a* b }(2, 3))}
컬렉션 연산 인라이닝
코틀린 표준 라이브러리의 컬렉션 함수는 대부분 람다를 인자로 받는다.
filter, map은 인라인 함수
두 함수의 본문은 인라이닝되며, 추가 객체나 클래스 생성은 없다.
하지만 이 코드는 리스트를 걸러낸 결과를 저장하는 중간 리스트를 만든다.
처리할 원소가 많아지면 중간 리스트를 사용하는 부가 비용도 걱정할 만큼 커진다.
asSequence를 통해 리스트 대신 시퀀스를 사용하면 중간 리스트로 인한 부가 비용은 줄어든다.
각 중간 시퀀스는 람다를 필드에 저장하는 객체로 표현되며,
최종 연산은 중간 시퀀스에 있는 여러 람다를 연쇄 호출
@Testfun`test filter and map with list`() {val list =listOf(1, 2, 3, 4, 5)val result = list.filter { it %2==0 }.map { it *2 }assertEquals(listOf(4, 8), result)}@Testfun`test filter and map with sequence`() {val list =listOf(1, 2, 3, 4, 5)val result = list.asSequence().filter { it %2==0 }.map { it *2 }.toList()assertEquals(listOf(4, 8), result)}
함수를 인라인으로 선언해야 하는 경우
⚠️ inline 키워드의 이점을 배우고 나면 코드를 더 빠르게 만들기 위해 여기저기에서 inline을 사용하고 싶어질 것이다. 하지만❗️ 이는 좋은 생각이 아니다.
inline 키워드를 사용해도 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 높다.
1️⃣ 일반 함수 호출의 경우 JVM은 이미 강력하게 인라이닝을 지원한다.
JVM은 코드 실행을 분석해서 가장 이익이 되는 방향으로 호출을 인라이닝한다.
이런 과정은 바이트 코드를 실제 기계어 코드로 번역하는 과정(JIT)에서 일어난다.
이런 JVM의 최적화를 활용한다면 바이트 코드에서는 각 함수 구현이 정확히 한 번만 있으면 되고, 그 함수를 호출하는 부분에서 따로 함수 코드를 중복할 필요가 없다.
2️⃣ 인라인 함수는 바이트 코드에서 각 함수 호출 지점을 함수 본문으로 대치하기 때문에 코드 중복이 생긴다.
게다가 함수를 직접 호출하면 스택 트레이스가 더 깔끔해진다.
3️⃣ inline 변경자를 함수에 붙일 때는 코드 크기에 주의를 기울여야 한다.
인라이닝하는 함수가 큰 경우 함수의 본문에 해당하는 바이트 코드를 모든 호출 지점에 복사해 넣고 나면 바이트코드가 전체적으로 아주 커질 수 있다.
🎬 그러면 함수를 인라인으로 언제 선언할까 ❓
람다를 인자로 받는 함수
람다를 인자로 받는 함수는 인라인으로 선언하면 성능이 향상될 수 있다.
함수 호출의 오버헤드를 줄이고, 람다를 인라인으로 대체하여 추가 객체 생성을 피할 수 있다.
고차 함수
고차 함수(다른 함수를 인자로 받거나 반환하는 함수)는 인라인으로 선언하면 호출 비용을 줄일 수 있다.
성능이 중요한 경우
함수 호출이 빈번하고, 성능이 중요한 경우 호출 오버헤드를 줄일 수 있다.
작은 함수
함수 본문이 작고, 여러 번 호출되는 경우 코드 중복이 크게 증가하지 않으면서 성능을 향상
자원 관리를 위해 인라인된 람다 사용
자바의 try-with-resource 문은 코틀린에서 제공하지 않는다.
대신 같은 기능을 제공하는 use 라는 함수가 코틀린 표준 라이브러리 안에 있다.
use 함수는 닫을 수 있는(closeable) 자원에 대한 확장 함수며, 람다를 인자로 받는다.
람다 안에서 return을 사용하면 람다로부터만 반환되는 게 아니라 그 람다를 호출하는 함수가 실행을 끝내고 반환된다.
자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return 문을 non-local return이라 부른다.
이렇게 return이 바깥쪽 함수를 반환시킬 수 있는 때는 람다를 인자로 받는 함수가 인라인 함수인 경우뿐
forEach는 인라인 함수이므로 람다 본문과 함께 인라이닝
따라서 return 식이 바깥쪽 함수(lookForAlice)를 반환시키도록 쉽게 컴파일
@Testfun`람다 안의 return문`() {dataclassPerson(val name: String, val age: Int)funlookForAlice(people: List<Person>): String {for (person in people) {if (person.name =="Alice") {return"Found!" } }return"Alice is not found" }funlookForAliceForEach(people: List<Person>): String { people.forEach {if (it.name =="Alice") {return"Found!" } }return"Alice is not found" }val people =listOf(Person("Alice", 29), Person("Bob", 31))assertEquals("Found!", lookForAlice(people))assertEquals("Found!", lookForAliceForEach(people))val people2 =listOf(Person("Bob", 31))assertEquals("Alice is not found", lookForAlice(people2))assertEquals("Alice is not found", lookForAliceForEach(people2))}
람다로부터 반환: 레이블을 사용한 return
람다 식에서도 local return을 사용할 수 있다.
람다 안에서 local return은 for루프의 break와 비슷한 역할을 한다.
단, local return과 non-local return을 구분하기 위해 레이블(label)을 사용해야 한다.
@Testfun`람다로부터 반환`() {dataclassPerson(val name: String, val age: Int)funlookForAlice(people: List<Person>): String { people.forEach label@{// 람다를 호출하는 함수가 끝나지 않고 람다로부터 반환if (it.name =="Alice") return@label }return"Alice might be somewhere" }val people =listOf(Person("Alice", 29), Person("Bob", 31))assertEquals("Alice might be somewhere", lookForAlice(people))val people2 =listOf(Person("Bob", 31))assertEquals("Alice might be somewhere", lookForAlice(people2))}
✅ 람다에 레이블을 붙여서 사용하는 대신 람다를 인자로 받는 인라인 함수의 이름을 return 뒤에 레이블로 사용해도 된다.
@Testfun`무명 함수`() {dataclassPerson(val name: String, val age: Int)funlookForAlice(people: List<Person>) { people.forEach(fun (person) {if (person.name =="Alice") returnprintln("${person.name} is not Alice") }) }val people =listOf(Person("Alice", 29), Person("Bob", 31))lookForAlice(people) // Bob is not Alice}
무명 함수는 일반 함수와 비슷해 보이지만, 차이는 함수 이름이나 파라미터 타입을 생략할 수 있다는 점 뿐
무명 함수 안에서 레이블이 붙지 않은 return 식은 무명 함수 자체를 반환시킬 뿐 무명 함수를 둘러싼 다른 함수를 반환시키지 않는다.
return에 적용되는 규칙은 단순히 fun 키워드를 사용해 정의된 가장 안쪽 함수를 반환시킨다는 점
람다 식의 구현 방법이나 람다 식을 인라인 함수에 넘길 때 어떻게 본문이 인라이닝 되는지 등의 규칙을 무명 함수에도 모두 적용 가능