PS문제를 하나씩 풀다보니 공부가 필요한 문법에 대해서 정리한 글입니다.
Kotlin에는 특수한 스코프 함수가 있다. 해석을 하자면 범위함수이다.
공식 문서에 따르면
특정 객체의 컨텍스트 안에서 특정 동작(속성 초기화, 활용 등)을 실행하기 위한 목적 만을 가진 함수를 말하며
호출 시 람다(lambda) 표현식을 형성하며, 이 람다식 안에는 객체 이름 없이 엑서스 할 수 있다.
- 그럼 왜 사용할까?
범위함수를 사용하면 객체를 참조하는데 있어 이름없이 참조가 가능하다.
하지만 과도하게 사용하면 코드를 복잡하게 만들 수 있으니 주의해야 한다.
스코프 함수에는
let,run,also,apply,with5가지 종류가 있다.
특징을 표로 정리해 보자
| 함수 | 참조 객체 | 리턴 값 | 확장기능 |
|---|---|---|---|
| let | it | Lambda result | 가능 |
| run | this | Lambda result | 가능 |
| run | - | Lambda result | 불가능 : 컨텍스트 객체 없이 호출됨 |
| with | this | Lambda result | 불가능 : 컨텍스트 객체를 인수로 사용함 |
| apply | this | Context object | 가능 |
| also | let | Context object | 가능 |
의도한 목적에 따라 범위 기능을 선택하기 위한 가이드
- null이 불가능한 객체의 람다 실행 :
let- 로컬 범위에서 변수로 표현식을 소개 :
let- 객체 구성 :
apply- 객체 구성 및 결과 계산 :
run- 표현식이 필요한 곳에서 문장 실행 : 비확장
run- 추가 효과 :
also- 객체에 대한 함수 호출 그룹화 :
with
let은 객체의 결과 값에 하나 이상의 함수를 호출하는 경우 사용된다.예제를 보면 리스트를 map으로 만들어
그 안의 인수들의 글자 수가 3보다 큰 것을 컬렉션을 담고 있는 코드이다.
fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
}
let을 사용하여 코드를 바꿔보면
코드 블록에 it을 인자로 갖는 단일 함수가 존재한다면 람다 대신 메서드 참조(::)를 사용할 수 있다.
fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
}
let은 null이 아닌 값으로만 코드 블록을 실행 시키고 싶을 때 자주 사용된다.null 이 아닌 객체에 대해 작업을 수행하려면 안전한 호출 연산자 (?.)를 let에 사용해야 한다.
fun main() {
val str: String? = "Hello"
val length = str?.let {
println("let()에 의해 호출됨 $it")
it.length
}
}
let의 또 다른 사용방법은 코드 가독성을 높이기 위해 제한된
scope내에서 지역변수로 사용하는 것이다.
새로운 변수를 정의하기 위해서는 기본적으로 사용되는 it 대신 람다의 인자로 변수 이름을 넣어서 사용하면 된다.
fun main() {
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")
}
}
firstItem 에는 one이 들어 가고 그것의 길이가 5이상이 아닐시 앞 뒤로 !가 붙고 대문자로 변할 것이다.
여기서 firstItem은 스코프 내의 새로운 지역변수가 된 것이다.
- apply 함수는 인스턴스를 새로 생성하고 특정 변수에 할당하기 전에 초기화 작업을 해주는 스코프를 만든다.
class Book(var name : String, var price: Int){
fun discount() {
price -= 5000
}
}
fun main() {
val a = Book("Kotlin의 모험", 30000).apply{
name = "[세일중] $name"
discount()
}
println("상품명 : ${a.name}, 가격 : ${a.price}")
}
위 코드는 Book 객체에 name과 price 프로퍼티를 갖고
price를 핸들링하는 discount() 함수를 가진다.
apply 함수는 스코프 안의 모든 명령이 수행되고 나면 명령들이 적용되어
새로 생성된 인스턴스를 반환한다는 특징을 기억하자.
- run은 apply와 다르게 바로 반환하는 것이 생성된 인스턴스가 아닌
스코프 내 명령 실행 결과 값이라는 점이다.이미 만들어진 인스턴스의 값 혹은 그를 이용한 특정 계산 결과를 필요로 하는 경우 사용된다.
class Book(var name : String, var price: Int){
fun discount() {
price -= 5000
}
}
fun main() {
val a = Book("Kotlin의 모험", 30000).apply{
name = "[세일중] $name"
discount()
}
println("상품명 : ${a.name}, 가격 : ${a.price}")
val bookCost = a.run {
price + 5000
}
println("원가는 $bookCost!")
}
위
apply에는 새로 인스턴스를 생성하여 값을 넣어주었다면
run은 특정 인스턴스의 프로퍼티를 출력하거나 계산 값으로 활용하는 등의 핸들링을 사용할 때 사용한다.
with도 run과 마찬가지로 문법만 다를 뿐 특성 상 차이점이 없다.
run과with의 차이점은 팡의 Safe Call(?.)을 붙여서 null을 체크할 수 있기에
with보다는 run을 많이 사용한다.
//run 사용
val bookCost = a.run {
price + 5000
}
println("원가는 $bookCost!")
//with 사용
val bookCost = with(a) {
price + 5000
}
println("원가는 $bookCost!")
also는let과 비슷하긴 한데
기존 객체를 수정하거나 변경하지 않고, 디버깅을 위한 로깅 등의 추가적인 작업을 하려고 할 때 사용한다.
let은it키워드를 사용할 수 있지만 also는 사용하지 못한다.
fun main() {
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("새로운 원소를 추가하기전 리스트 출력 : $it") }
.add("four")
println("$numbers")
}
오늘은 스코프 함수에 대해 알아보았다.
코드를 사용하다 보면 스코프 함수를 사용할 때가 빈번한데
상황과 용도에 맞는 스코프 함수를 사용해 보자!