오버로딩과 고차 함수
코틀린 Step02 - 오버로딩과 고차 함수
산술 연산자 오버로딩
코틀린에서 관례를 사용하는 가장 단순한 예는
산술 연산자
자바에서는 원시 타입에 대해서만 산술 연산자 사용 가능하고, String에 대해 + 연산자를 사용 가능
이항 산술 연산 오버로딩
operator
변경자를 추가해 plus 함수를 선언하고 나면 + 기호로 두 Point 객체를 더할 수 있음
// case 1. 연산자를 자체 함수로 정의하기
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
// case 2. 연산자를 확장 함수로 정의하기
data class Point2(val x: Int, val y: Int)
operator fun Point2.plus(other: Point2): Point2 {
return Point2(x + other.x, y + other.y)
}
@Test
fun `이항 산술 연산 오버로딩`() {
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)
}
ℹ️ 오버로딩 가능한 이항 산술 연산자
a * b
times
a / b
div
a % b
mod
a + b
plus
a - b
minus
연산자를 정의할 때 두 피연산자가(연산자 함수의 두 파라미터)가 같은 타입일 필요는 없다.
또는 연산자 함수의 반환 타입이 꼭 두 피연산자 중 하나와 일치해야만 하는 것도 아니다.
data class Point(val x: Int, val y: Int)
// case 1. 두 피연산자가 다른 연산자 정의
// Point, Double 타입의 피연산자를 받아 Point 반환
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
// case 2. 반환 타입이 피연산자와 다른 연산자 정의
// Char, Int 타입의 피연산자를 받아 String 반환
operator fun Char.times(count: Int): String {
return toString().repeat(count)
}
@Test
fun `연산자 정의`() {
val point = Point(2, 3)
val scaledPoint = point * 2.5
assertEquals(Point(5, 7), scaledPoint)
val result = 'a' * 3
assertEquals("aaa", result)
}
복합 대입 연산자 오버로딩
+=
,-=
등의 연산자는 복합 대입(compound assignment) 연산자라 불림
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
@Test
fun `복합 대입 연산자`() {
var point = Point(1, 2)
point += Point(3, 4)
assertEquals(Point(4, 6), point)
}
코틀린 표준 라이브러리는 변경 가능한 컬렉션에 대해 plusAssign
을 정의
@Test
fun `test plusAssign operator`() {
val mutableList = mutableListOf(1, 2, 3)
mutableList += 4
assertEquals(listOf(1, 2, 3, 4), mutableList)
}
단항 연산자 오버로딩
단항 연산자 오버로딩하는 절차도 이항 연산자와 동일
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}
@Test
fun `단항 연산자`() {
val p = Point(10, 20)
assertEquals(Point(-10, -20), -p)
}
ℹ️ 오버로딩할 수 있는 단항 산술 연산자
+a
unaryPlus
-a
unaryMinus
!a
not
++a, a++
inc
--a, a--
dec
비교 연산자 오버로딩
동등성 연산자: equals
코틀린이
==
연산자 호출을equals
메소드 호출로 컴파일
≠
연산자를 사용하는 식도equals
호출로 컴파일
class Point(val x: Int, val y: Int) {
override fun equals(obj: Any?): Boolean {
if (obj === this) return true
if (obj !is Point) return false
return obj.x == x && obj.y == y
}
}
@Test
fun `동등성 연산자`() {
assertTrue(Point(10, 20) == Point(10, 20))
assertTrue(Point(10, 20) != Point(5, 5))
assertFalse(null == Point(1, 2))
}
순서 연산자: compareTo
자바에서 정렬, 최댓값, 최솟값 등 값을 비교해야 하는 알고리즘에 사용할 클래스는
Comparable
인터페이스 구현이 필요하듯, 코틀린도 같은 Comparable 인터페이스를 지원
@Test
fun `순서 연산자`() {
class Person(
val firstName: String, val lastName: String
) : Comparable<Person> {
override fun compareTo(other: Person): Int {
return compareValuesBy(
this, other,
Person::lastName, Person::firstName
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Person) return false
return firstName == other.firstName && lastName == other.lastName
}
override fun hashCode(): Int {
return firstName.hashCode() * 31 + lastName.hashCode()
}
}
val person1 = Person("John", "Doe")
val person2 = Person("Jane", "Doe")
val person3 = Person("John", "Smith")
// 같은 성, 다른 이름
assertTrue(person1 > person2)
assertFalse(person1 < person2)
// 다른 성
assertTrue(person1 < person3)
assertFalse(person1 > person3)
// 같은 성, 같은 이름 (equals, hashCode 오버라이드 필요)
val person4 = Person("John", "Doe")
assertTrue(person1 == person4)
}
코틀린 표준 라이브러리의 compareValuesBy
함수를 사용해 compareTo
를 쉽고 간결하게 정의할 수 있다.
@Test
fun `compareValuesBy compareTo`() {
assertTrue("abc" < "bac")
}
컬렉션과 범위에 대해 쓸 수 있는 관례
in 관례
In
은 객체가 컬렉션에 들어있는지 검사.
in
연산자와 대응하는 함수는contains
data class Point(val x: Int, val y: Int)
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.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
}
@Test
fun `in 관례`() {
val rect = Rectangle(Point(10, 20), Point(50, 50))
assertTrue(Point(20, 30) in rect)
assertFalse(Point(5, 5) in rect)
}
rangeTo 관례
범위를 만들려면
..
구문을 사용
예를 들어 1..10은 1부터 10까지 모든 수가 들어있는 범위를 가리킨다.
https://livebook.manning.com/book/kotlin-in-action/chapter-7/65
@Test
fun `rangeTo 관례`() {
val n = 9
assertEquals(IntRange(0, 10), 0..(n + 1))
(0..n).forEach { print(it) } // 0123456789
}
for 루프를 위한 iterator 관례
코틀린에서는
iterator
메소드를 확장 함수로 정의 가능이런 성질로 인해 일반 자바 문자열에 대한 for 루프가 가능
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
object : Iterator<LocalDate> {
var current = start
override fun hasNext() =
current <= endInclusive
override fun next() = current.apply {
current = plusDays(1)
}
}
@Test
fun `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 함수
구조 분해를 사용하면 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화 가능
data class Point(val x: Int, val y: Int)
@Test
fun `구조 분해 선언`() {
val p = Point(10, 20)
val (x, y) = p
assertEquals(10, x)
assertEquals(20, y)
}
구조 분해 선언은 함수에서 여러 값을 반환할 때 유용
👎🏻 여러 값을 한꺼번에 반환해야 하는 함수가 있다면 반환해야 하는 모든 값이 들어갈 데이터 클래스를 정의하고 함수의 반환 타입을 그 데이터 클래스로 바꿔주어야 한다.
👍🏼 구조 분해 선언 구문을 사용하면 이런 함수가 반환하는 값을 쉽게 풀어서 여러 변수에 넣을 수 있다.
data class NameComponents(val name: String,
val extension: String)
fun splitFilename(fullName: String): NameComponents {
val result = fullName.split('.', limit = 2)
return NameComponents(result[0], result[1]) // 함수에서 데이터 클래스의 인스턴스를 반환
}
@Test
fun `component 함수`() {
val (name, ext) = splitFilename("example.kt") // 구조 분해 선언 구문을 사용해 데이터 클래스를 해체
assertEquals("example", name)
assertEquals("kt", ext)
}
구조 분해 선언과 루프
함수 본문 내의 선언문 뿐 아니라,
변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해 선언을 사용 가능
fun printEntries(map: Map<String, String>) {
// 구조 분해 선언
for ((key, value) in map) {
println("$key -> $value")
}
}
@Test
fun `구조 분해 선언과 루프`() {
/**
* Oracle -> Java
* JetBrains -> Kotlin
*/
val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
}
두 가지 코틀린 관례를 활용한 예시
객체를 이터페이션하는 관례
구조 분해 선언
@Test
fun `코틀린 관례`() {
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()
val value = 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
@Test
fun `**AS-IS by lazy**`() {
data class Email(val address: String)
class Person(val name: String) {
// 데이터를 저장하고 emails의 위임 객체 역활을 하는 _emails 프로퍼티
private var _emails: List<Email>? = null
val emails: List<Email>
get() {
if (_emails == null) {
_emails = loadEmails(this) // 최초 접근 시 이메일 가져오기
}
return _emails!! // 저장해 둔 데이터가 있으면 그 데이터를 반환
}
private fun loadEmails(person: Person): List<Email> {
println("Loading emails for ${person.name}")
return listOf(
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
/*
* 지연 초기화해야 하는 프로퍼티가 많아지면 코드가 복잡해지고,
* 스레드 안전하지 않아서 언제나 제대로 작동한다고 말할 수 없다.
* 이 경우 위임 프로퍼티를 사용하면 훨씬 더 간편해진다.
*/
@Test
fun `by lazy`() {
data class Email(val address: String)
class Person(val name: String) {
// lazy 함수는 코틀린 관례에 맞는 시그니처의 getValue 메소드가 들어있는 객체를 반환
// 따라서 lazy를 by 키워드와 함께 사용해 위임 프로퍼티를 만들 수 있다.
val emails by lazy { loadEmails(this) }
private fun loadEmails(person: Person): List<Email> {
println("Loading emails for ${person.name}")
return listOf(
Email("alice@example.com"),
Email("alice.work@example.com")
)
}
}
val p = Person("Alice")
val emails1 = p.emails // 최초로 emails를 읽을 때 단 한번만 이메일을 가져온다.
val emails2 = p.emails
assertEquals(emails1, emails2)
assertEquals(listOf(
Email("alice@example.com"),
Email("alice.work@example.com")
), emails1)
}
고차 함수 정의
고차 함수
는 다른 함수를 인자로 받거나 함수를 반환하는 함수코틀린에서는 람다나 함수 참조를 사용해 함수를 값으로 표현 가능
따라서 고차 함수는 람다나 함수 참조를 인자로 넘길 수 있거나, 람다나 함수 참조를 반환하는 함수
함수 타입
함수 타입을 정의하려면 함수 파라미터의 타입을 괄호 안에 넣고,
그 뒤에 화살표(→)를 추가한 다음, 함수의 반환 타입을 지정
https://livebook.manning.com/book/kotlin-in-action/chapter-8
Unit 타입은 의미 있는 값을 반환하지 않는 함수 반환 타입에 쓰는 특별한 타입
그냥 함수를 정의한다면 함수의 파라미터 목록 뒤에 오는 Unit 반환 타입 지정을 생략해도 되지만,
⚠️ 함수 타입을 선언할 때는 반환 타입을 반드시 명시해야 하므로 Unit을 필수로 명시
인자로 받은 함수 호출
인자로 받은 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 동일
fun twoAndThree(operation: (Int, Int) -> Int): Int { // 함수 타입의 인자를 받는 함수
val result = operation(2, 3)
return result
}
@Test
fun `인자로 받은 함수 호출`() {
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) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(transform(element)) // transform 파라미터로 받은 함수를 호출
}
result.append(postfix)
return result.toString()
}
@Test
fun `파라미터를 함수 타입으로 선언할 때도 디폴트 값 설정 가능`() {
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 식에 람다나 멤버 참조나 함수 타입의 값을 계산하는 식 등을 넣으면 된다.
data class Person(
val firstName: String,
val lastName: String,
val phoneNumber: String?
)
class ContactListFilters {
var prefix: String = ""
var onlyWithPhoneNumber: Boolean = false
fun getPredicate(): (Person) -> Boolean { // 함수를 반환하는 함수를 정의한다.
val startsWithPrefix = { p: Person ->
p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
}
if (!onlyWithPhoneNumber) {
return startsWithPrefix // 함수 타입의 변수 반환
}
return { // 람다 반환
startsWithPrefix(it)
&& it.phoneNumber != null
}
}
}
@Test
fun `함수를 함수에서 반환`() {
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()) // 반환된 함수 타입 변수 적용
)
}
람다를 활용한 중복 제거
함수 타입과 람다 식은 재활용하기 좋은 코드를 만들 때 쓸 수 있는 훌륭한 도구
웹 사이트 방문 기록을 분석하는 예시
@Test
fun `람다를 활용한 중복 제거`() {
/**
* 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. 함수를 사용하여 일반 함수를 통해 중복을 줄이기
*/
fun List<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의 평균 방문 시간을 구하고 싶다거나, 특정 페이지 평균 방문 시간을 구하고 싶을 경우
*/
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
// 모바일 디바이스(IOS, 안드로이드)의 평균 방문 시간
assertEquals(12.15, log.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) })
// IOS 사용자의 /signup 페이지 평균 방문 시간
assertEquals(8.0, log.averageDurationFor { it.os == OS.IOS && it.path == "/signup" })
}
인라인 함수: 람다의 부가 비용 없애기
inline
변경자를 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을함수 본문에 해당하는 바이트 코드로 바꿔준다.
인라이닝이 작동하는 방식
특정 함수를
inline
선언하면 그 함수의 본문이 인라인
함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트 코드로 컴파일
@Test
fun `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
를 통해 리스트 대신 시퀀스를 사용하면 중간 리스트로 인한 부가 비용은 줄어든다.각 중간 시퀀스는 람다를 필드에 저장하는 객체로 표현되며,
최종 연산은 중간 시퀀스에 있는 여러 람다를 연쇄 호출
@Test
fun `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)
}
@Test
fun `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) 자원에 대한 확장 함수며, 람다를 인자로 받는다.
use는 람다를 호출한 다음에 자원을 닫아준다.
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br ->
return br.readLine()
}
}
고차 함수 안에서 흐름 제어
람다 안의 return문: 람다를 둘러싼 함수로부터 반환
람다 안에서 return을 사용하면 람다로부터만 반환되는 게 아니라 그 람다를 호출하는 함수가 실행을 끝내고 반환된다.
자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return 문을 non-local return
이라 부른다.
이렇게 return이 바깥쪽 함수를 반환시킬 수 있는 때는 람다를 인자로 받는 함수가 인라인 함수인 경우뿐
forEach는 인라인 함수이므로 람다 본문과 함께 인라이닝
따라서 return 식이 바깥쪽 함수(lookForAlice)를 반환시키도록 쉽게 컴파일
@Test
fun `람다 안의 return문`() {
data class Person(val name: String, val age: Int)
fun lookForAlice(people: List<Person>): String {
for (person in people) {
if (person.name == "Alice") {
return "Found!"
}
}
return "Alice is not found"
}
fun lookForAliceForEach(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)을 사용해야 한다.
@Test
fun `람다로부터 반환`() {
data class Person(val name: String, val age: Int)
fun lookForAlice(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 뒤에 레이블로 사용해도 된다.
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") return@forEach
}
println("Alice might be somewhere")
}
무명 함수: 기본적으로 로컬 return
무명 함수는 코드 블록을 함수에 넘길 때 사용할 수 있는 다른 방법
@Test
fun `무명 함수`() {
data class Person(val name: String, val age: Int)
fun lookForAlice(people: List<Person>) {
people.forEach(fun (person) {
if (person.name == "Alice") return
println("${person.name} is not Alice")
})
}
val people = listOf(Person("Alice", 29), Person("Bob", 31))
lookForAlice(people) // Bob is not Alice
}
무명 함수는 일반 함수와 비슷해 보이지만, 차이는 함수 이름이나 파라미터 타입을 생략할 수 있다는 점 뿐
무명 함수 안에서 레이블이 붙지 않은 return 식은 무명 함수 자체를 반환시킬 뿐 무명 함수를 둘러싼 다른 함수를 반환시키지 않는다.
return에 적용되는 규칙은 단순히 fun 키워드를 사용해 정의된 가장 안쪽 함수를 반환시킨다는 점
람다 식의 구현 방법이나 람다 식을 인라인 함수에 넘길 때 어떻게 본문이 인라이닝 되는지 등의 규칙을 무명 함수에도 모두 적용 가능
Last updated