안녕하세요 안드로이드 12기 주혜진입니다~ 회사에서 리뷰 받을 때 주로 제가 스코프 함수를 잘못? 사용해서 권장되는 사용법을 알려주시는 코멘트를 많이 받았었는데요~ 그래서 요번 블로그 주제로 스코프 함수로 정해봤습니다! 이전에 스코프 함수 글이 있긴 하지만! 저는 공식문서를 번역하면서 공부한 글이라 쉽게 쓱 읽기 좋을 것 같아요! 그럼 본론으로 고고!
코틀린 스코프 함수는 객체의 context 내에서 코드를 실행하는 유일한 목적이 있습니다. 람다식을 갖는 객체에 대해 이러한 함수를 call 할 때, 임시적인 스코프로 형성합니다. 스코프 안에서 객체를 표현하는 이름 없이 접근할 수 있습니다. 이런 함수를 스코프 함수라 부르고 종류는 5개입니다.
let, run, with, apply, also
기본적으로 함수는 같은 역할을 수행합니다. : 객체위에 코드 블락을 실행함
다른 점은 이 객체가 블록 내부에서 어떻게 사용 가능하게 되는지와 전체 표현식의 결과가 무엇인지입니다.
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
Person(name=Alice, age=20, city=Amsterdam) Person(name=Alice, age=21, city=London)
만약 같은 기능을 let 없이 사용한다면 새로운 변수를 만들고 사용할 때 마다 그 이름을 반복해야 합니다.
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
Person(name=Alice, age=20, city=Amsterdam) Person(name=Alice, age=21, city=London)
스코프 함수의 유사한 특성 때문에 이들 사이의 주요 차이점을 아는 것이 중요합니다 주요 차이점은 두 가지가 있습니다.
람다 스코프 함수 안에 context 객체는 그것의 실제 이름을 사용하는 대신에 짧은 참조를 이용될 수 있습니다. 각 스코프 함수는 context object에 접근하는 방법인 두 개 중 하나를 사용합니다. this: 람다 수신자 it : 람다 변수
각각 같은 기능을 제공합니다. 이제 각각 상황에 대한 장단점을 설명하고, 추천사용법을 알려드리겠습니다.
fun main() {
val str = "Hello"
// this
str.run {
println("The string's length: $length")
//println("The string's length: ${this.length}") // does the same
}
// it
str.let {
println("The string's length is ${it.length}")
}
}
The string’s length: 5 The string’s length is 5
run, with 그리고 apply는 context this 키워드로부터 객체를 람다 수신자로 참조합니다. 그러므로 람다 안에서 객체는 일반 클래스 함수 안에서 사용할 수 있습니다. 대부분의 경우 수신 객체의 멤버에 접근할 때 this를 제거할 수 있습니다. 그러나 this는 수신 객체와 외부 객체 또는 함수를 구별하기 어렵습니다. 그래서 context object를 수신 객체인 this로 갖는 것은 주로 객체의 멤버를 계산하는 람다씩(객체 함수의 호출, 또는 프로퍼티에 값 할당)에 추천합니다.
val adam = Person("Adam").apply {
age = 20 // same as this.age = 20 or adam.age = 20
city = "London"
}
println(adam)
Person(name=Adam, age=20, city=London)
결국 let, also는 context 객체를 람다 변수로 갖습니다. 만약 변수 이름이 명시되지 않았다면, 객체는 암시적 기본 이름인 it으로 접근할 수 있습니다. it은 this보다 더 짧고 it을 갖은 표현은 읽기 편합니다. 하지만 객체의 함수나 프로퍼티들을 호출할 때 this처럼 암시적으로 이용할 수 있는 객체가 없습니다. 그러나 context 객체를 it으로 갖는 것은 객체가 함수 호출 안에서 변수로 사용될 때 더 좋습니다. it은 또한 코드 블록의 다양한 변수를 사용할 때 더 좋습니다.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
println(i)
INFO: getRandomInt() generated value 99 99
아래 예제는 it을 사용하여 코드 블록의 다양한 변수를 사용한 경우 입니다.
data class Apple(var weight: Int)
class AppleTree(val appleTree: List<Apple>) {
fun pick(): Apple {
return appleTree[0]
}
}
//여기서 apply를 사용하는 경우 this는 가려지게 되고 this.weight은 fruteBasket이 아닌 apple을 참조한다.
class FruitBasket {
private var weight = 0
fun addFrom(appleTree: AppleTree) {
val apple = appleTree.pick().let { it ->
this.weight += it.weight
add(it)
}
}
fun add(apple: Apple) {
}
}
추가로 context object를 변수로 전달할 때 스코프 안에서 context object의 custom name을 전달할 수 있습니다.
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
println(i)
스코프 함수는 리턴되는 값에 따라 다릅니다
apply와 also는 자기자신의 context 객체를 리턴합니다. 그러므로 그것은 call chain에 사이드 스텝으로써 포함될 수 있습니다: 함수 호출 후 동일한 개체에 대한 함수 호출을 계속할 수 있습니다.
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()
또한 context 객체를 리턴하는 함수의 문으로 사용될 수 있습니다.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
INFO: getRandomInt() generated value 55
let, run, and with은 람다 결과를 반환합니다. 따라서 변수에 결과를 할당하거나 결과에 대한 연산을 연결하는 등의 작업을 수행할 때 사용할 수 있습니다.
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")
There are 3 elements that end with e.
추가로 리턴 값을 무시하거나 변수에 대한 임시적인 스코프를 갖는 스코프 함수를 사용할 때 사용합니다.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
First item: one, last item: three
일반적이 사용 스타일을 정의하는 규칙을 아래 예제에서 확인 가능 합니다.
호출 체인의 결과에 대해 하나 이상의 함수를 호출할 수 있습니다. 예를 들어, 다음 코드는 컬렉션에 대한 두 가지 작업의 결과를 출력합니다.
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
[5, 4, 4]
let 사용 시
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// and more function calls if needed
}
[5, 4, 4]
null 검사에도 사용합니다. null이 될 수 없는 타입을 파라미터로 받는 함수에 널이 될 수 있는 타입의 값을 넘길 수는 없습니다. 따라서 let함수를 통해 널이 될 수 있는 타입의 값을 널이 될 수 없는 타입의 값으로 바꿔서 전달하기 위해 사용합니다.
val str: String? = "Hello"
//processNonNullString(str) // compilation error: str can be null
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
it.length
}
let() called on Hello
또한 코드 가독성을 높이기 위해서 제한된 범위의 로컬변수로도 사용합니다. 이를 위해서는 it대신 lambda 변수 이름으로 제공해야 합니다.
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'")
The first item of the list is ‘one’ First item after modifications: ‘!ONE!’
비 확장 함수 : context 객체는 인수로 전달되지만, lambda 내부에 수신기로 사용할 수 있습니다. 반환 값은 lambda 결과입니다. 람다 결과를 제공하지 않고 context 객체의 함수, 프로퍼티를 호출하려면 사용하는 것이 좋습니다. 코드에서는 “이 객체를 사용하여 다음을 수행합니다”로 읽습니다.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
‘with’ is called with argument [one, two, three] It contains 3 elements
또 다른 사용 사례는 값을 계산하는 데 사용할 프로퍼티 또는 함수를 사용하는 도우미 객체를 도입하는 것입니다.
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
The first element is one, the last element is three
context 객체를 lambda 수신기로 사용할 수 있습니다. 반환 값은 람다 결과입니다. 실행은 let과 동일한 기능을 수행하지만, 문맥 개체의 확장 함수로 let을 호출합니다. (추가적인 연산을 진행하고 그값을 반환하는…) run은 람다에 객체 초기화 및 반환 값 계산이 모두 포함된 경우에 유용합니다.
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
Result for query ‘Default request to port 8080’ Result for query ‘Default request to port 8080’
확장자가 아닌 함수로 사용할 수 있습니다. 비 확장 실행을 사용하면 식이 필요한 경우 여러 문의 블록을 실행할 수 있습니다.
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
println(match.value)
}
+123 -FFFF 88
값을 반환하지 않고 주로 수신기 객체의 멤버에서 작동하는 코드 블록에 대해 apply를 사용합니다. 일반적인 경우는 객체 구성입니다. 이러한 호출은 “객체에 다음 할당을 적용”으로 읽을 수 있습니다. 또한 chain도 적용할 수 있습니다.
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
Person(name=Adam, age=32, city=London)
객체를 인수로 사용하고 객체를 반환합니다. 객체의 속성 및 기능 대신 참조 객체의 참조가 필요한 작업이나 외부 범위에서 this에 의해 가려지지 않으려면 사용합니다. 코드에서도 “객체에 대해 다음 작업을 수행할 수도 있습니다.”라고 읽을 수 있습니다.
l numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
The list elements before adding new one: [one, two, three]
https://kotlinlang.org/docs/scope-functions.html#function-selection