고딩왕 코범석
특성 본문
Index
코틀린의 이모저모
Type Alias와 as import
typealias
는 긴 이름의 클래스 혹은 함수 타입이 있을 때 축약하거나 더 좋은 이름을 쓰고싶을 경우 사용한다.
typealias FruitFilter = (Fruit) -> Boolean
fun filterFruits(fruits: List<Fruit>, filter: FruitFilter) {...}
data class UltraSuperGuardianTribe(
val name: String
)
typealias USGTMap = Map<String, UltraSuperGuardianTribe>
다른 패키지의 같은 이름 함수를 동시에 가져오고 싶다면? as import
를 사용한다.
import inflearnjavatokotlin.section5.packageA.helloPrint as helloPrintA
import inflearnjavatokotlin.section5.packageB.helloPrint as helloPrintB
fun main() {
helloPrintA()
helloPrintB()
}
구조분해와 componentN 함수
구조분해는 복합적인 값을 분해하여 여러 변수를 한번에 초기화하는 것을 의미한다.
fun main() {
val person = Person("고범석", 28) // Person은 data class
val (name, age) = person
println("$name, $age")
}
Person
은 Data class
이다. Data class
는 componentN
이란 함수도 자동으로 만들어주기 때문에 가능하다.
componentN
이란 Data class의 필드가 N개 있을 경우 N번째에 대한 프로퍼티를 가져오도록 하는 함수이다. 즉, 프로퍼티의 순서에 의존하게 되며 val (name, age) = person
을 풀면 다음과 같이 된다.
// val (name, age) = person
val name = person.component1()
val age = person.component2()
componentN
함수는 연산자 속성을 갖고 있어 일반 클래스에서 componentN
함수를 작성한다면 operator 키워드를 붙여 작성해야 한다.
class Person(
val name: String,
val age: Int,
) {
operator fun component1() = this.name
operator fun component2() = this.age
}
Jump와 Label
return
, break
, continue
는 다음과 같이 정의하고 있다.
return
: 기본적으로 가장 가까운 enclosing function 또는 익명함수로 값이 반환break
: 가장 가까운 루프 제거continue
: 가장 가까운 루프를 다음 step으로 보냄
for
, while
문에서 break
, continue
의 기능은 자바와 동일하다.
단, forEach
에서 break
, continue
를 사용할 수 없다. break
, continue
를 써야한다면 for문을 사용하자.
TakeIf와 TakeUnless
코틀린에서는 method chaining을 위한 함수를 제공한다.
fun getNumberIfEvenOrNullTakeIf(number: Int) =
number.takeIf { it % 2 == 0 }
fun getNumberIfEvenOrNullTakeUnless(number: Int) =
number.takeUnless { it % 2 != 0 }
takeIf
의 코드를 살펴보자.
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
// 여기
return if (predicate(this)) this else null
}
조건을 만족하면 자신을 반환하고 아니라면 null
을 반환하는 것을 알 수 있다.
scope function
scope function이란?
scope
는 영역, function
은 함수를 의미한다. scope function
은 일시적인 영역을 형성하는 함수를 의미한다. scope function을 이용해 printPerson()
을 리팩토링해보자.
fun printPerson(person: Person?) {
if (person != null) {
println(person.name)
println(person.age)
}
}
// let으로 리팩토링
fun printPersonLet(person: Person?) {
person?.let {
println(it.name)
println(it.age)
}
}
정리해보면 람다를 사용해 일시적인 영역을 만들고 코드를 더 간결하게 만들거나, method chaining
에 활용하는 함수를 scope function
이라 한다.
scope function의 분류
코틀린의 scope function에는 다섯 가지의 종류가 있다.
- let
- run
- also
- apply
- with (확장함수 아님)
it 사용 | this 사용 | |
---|---|---|
람다의 결과 반환 | let | run |
객체 그 자체를 반환 | also | apply |
this
: 생략이 가능한 대신, 다른 이름을 붙일 수 없다.it
: 생략이 불가능한 대신, 다른 이름을 붙일 수 있다.
각 scope function 별로 this
, it
을 사용하는데 왜 이런 차이가 발생할까?
public inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}
public inline fun <T, R> T.run(block: T.() -> R): R {
return block()
}
let
은 일반 함수를 받지만 run
은 확장함수를 받는다. 확장함수는 본인 자신을 this
를 호출하고 생략할 수 있기 때문에 이러한 차이가 발생한다.
with
는 파라미터와 람다를 받는다. 또한 람다에서는 확장함수를 받기 때문에 this
로 접근하고 생략 가능하다.
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
scope function의 적절한 용례
let
하나 이상의 함수를 call chain 결과로 호출할 때 사용한다.
val strings = listOf("APPLE", "CAR")
strings.map{ it.length }
.filter { it > 3 } // 리스트로 반환
.let(::println) // .let { lengths -> println(lengths) }
non-null 값에 대해서만 code block을 실행시킬 때 사용한다.
val length = str?.let {
println(it.uppercase())
it.length
}
일회성으로 제한된 영역에 지역 변수를 만들 때 사용한다.
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first()
.let { firstItem ->
if (firstItem.length >= 5) firstItem else "!${firstItem}"
}.uppercase()
println(modifiedFirstItem)
run
객체 초기화와 반환 값의 계산을 동시에 해야할 때 사용한다. 객체를 만들어 DB에 바로 저장하고 그 인스턴스를 활용할 때가 좋은 예다.
val person = Person("고범석", 28)
.run(personRepository::save)
val person = Person("고범석", 28).run {
hobby = "독서"
personRepository.save(this)
}
자바를 사용했다 보니 repository에서 반환된 값을 사용하는 것이 익숙하다. 반복되는 생성 후처리는 생성자, 프로퍼티, init block으로 넣는 것이 유지보수에 유리하다. 단, 생성자가 너무 길어지는 경우 run
을 사용하면 코드가 깔끔해진다.
apply
, also
객체 설정 시 객체를 수정하는 로직이 call chain 중간에 필요할 때 사용한다.
fun createPerson(
name: String,
age: Int,
hobby: String,
): Person {
return Person(
name = name,
age = age,
).apply {
this.hobby = hobby
}
}
mutableListOf("one", "two")
.also { println("추가 전: $it") }
.add("four")
with
특정 객체를 다른 객체로 변환해야 하는데, 모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때 사용한다.
객체를 변환하는데 한 쪽에 로직을 넣기 어려울 때 사용한다.
return with(person) {
PersonDto(
name = name,
age = age,
)
}
scope function과 가독성
두 가지의 코드를 보고 가독성을 비교해보자.
// 1번 로직
if (person != null && person.isAdult) {
view.showPerson(person)
} else {
view.showError()
}
// 2번 로직
person?.takeIf{ it.isAdult }
?.let(view::showPerson)
?: view.showError()
2번 로직은 코틀린 개발자만 더 알아보기 쉬울 수 있다. 1번의 경우는 특히 디버깅과 수정이 쉽다.
정리하자면 사용 빈도가 적은 관용구는 코드를 더 복잡하게 만들고 한 문장 내에서 조합해서 사용하면 복잡성이 훨씬 증가한다.
하지만 적절한 컨벤션을 적용하면 유용하게 활용할 수 있다. 즉, 팀에 대한 코틀린 숙련도, 취향 차이에 따라 달라질 수 있다.
'Language & Framework > Kotlin' 카테고리의 다른 글
FP (0) | 2022.08.03 |
---|---|
OOP (0) | 2022.08.02 |
코드 제어 (0) | 2022.07.30 |
변수, 타입, 연산자 (0) | 2022.07.30 |