코틀린 표준 함수들 일부는 어디에 사용하는지 명확하게 알지 못한다. 이에 대해 그 차이점을 명확하게 설명하고 어떤걸 사용해야 하는지 소개하고자 한다.
범위함수(Scoping functions)
run, with, T.run, T.let, T.also 그리고 T.apply에 대해 집중할거다.
다음은 run함수의 범위지정기능을 설명하는 간단한 코드이다.
fun test() {
var mood = "I am sad"
run {
val mood = "I am happy"
println(mood) // I am happy
}
println(mood) // I am sad
}
test함수 내의 분리된 범위에서 mood는 재정의 되고 "I am happy"가 프린트된다. run 범위내에 제한된다.
이게 범위지정함수인데 별로 유용해보이지 않지만 범위지정으로써의 장점이 있다. 먼가를 반환할 수 있다.
아래코드를 보면 show()를 두번 호출하지 않고 두개의 view에 적용이 가능하다.
run {
if (firstTimeVIew) introView
else normalView
}.show()
범위지정함수의 3가지 속성
범위지정함수를 좀더 흥미있게 만들기 위해 3가지 속성으로 나누고 이를 통해 각각을 구별할 수 있다.
1. Normal vs. extension function
with, T.run 은 상당히 유사하다.
with(webview.settings) {
javascriptEnabled = true
databaseEnabled = true
}
webview.settings.run {
javascriptEnabled = true
databaseEnabled = true
}
두가지 차이점은 일반함수냐 확장함수냐이다. 그래서 각각의 장점은 멀까?
webview.settings가 null일 수 있다고 생각해보자
// 별로다
with(webview.settings) {
this?.javascriptEnabled = true
this?.databaseEnabled = true
}
// 이게 더 좋아
webview.settings?.run {
javascriptEnabled = true
databaseEnabled = true
}
이 경우는 T.run 사용이 더 유용하다.
2. This vs. it 인자
strVal?.run {
println("The length of this String is $length")
}
strVal?.let {
println("The length of this String is $it.length")
}
T.run의 경우 block: T.()의 형태를 가지며 따라서 범위내에서 this로 참조가 가능하다. this의 경우 일반적으로 생략이 가능하므로 $this.length가 아니라 $length가 가능하다.
T.let의 경우 block: (T)의 형태를 가지며 인자를 따로 명시하지 않으면 it으로 접근이 가능하다.
위 예를 보면 T.run이 좀더 암시적이어서 우세해 보이나 T.let의 장점도 존재한다.
* let은 함수/멤버 변수를 외부 클래스 함수/멤버와 구별하기가 더 좋다.
* 파라메터로 this가 전달되면 생략이 불가한데 it이 더 짧고 명확하다
* let은 변수명을 it이 아니라 다른 이름으로 부여가 가능하다.
value?.let { myname ->
println("The name is $myname");
}
3. this 또는 다른 타입의 반환
value?.let {
println("The length of val is ${it.length}")
}
value?.also {
println("The length of val is ${it.length}")
}
두가지의 차이점은 let은 임의 타입을 반환하고 also는 동일한 this타입을 반환한다.
둘다 체이닝함수에 유용한다. T.let은 연산된 결과를 다음 체인에 전달하고 T.also는 동일한 객체를 전달한다.
val original = "abc"
original.let {
prinln("The original string is $it") // 원래 문자열
it.reversed() // 리버스된 문자열 반환
}.let {
println("Ths reversed string is $it")
it.length. // 문자열길이 반환
}.let {
println("The length is $it") // 전달받은 타입은 숫자
}
original.let {
prinln("The original string is $it")
it.reversed()
}.also {
println("Ths reversed string is $it") // "abc" 프린트
it.length
}.also {
println("The length is $it") // "abc" 프린트
}
original.also {
prinln("The original string is $it")
}.also {
println("Ths reversed string is ${it.reversed()}") // 리버스된 문자열 프린트
}.also {
println("The length is ${it.length}") // 문자열 길이 프린트
}
위 샘플을 보면 also가 더이상 무슨 소용이 있나 싶을 수 있지만 잘생각해 보면 좋은 장점이 있다.
1. 동일 객체에 대한 일련의 처리를 명확하게 구분할 수 있게 해준다.
2. 체이닝 빌드 연산을 만듦으로 객체를 스스로 조작하는데 파워풀한 기능을 제공한다.
두가지를 합쳐서 한번은 변환하고 이어서 그 객체를 조작하는 체이닝을 만들 수 있다.
// 일반적인 접근
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}
// 향상된 접근
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
모든 속성 살펴보기
T.apply로 3가지 속성을 살펴보는것으로 더 많은것을 알 수 있다.
1. 함수의 확장이 필요한가?
2. 인자로서 this를 전달이 필요한가?
3. this를 반환이 필요한가?
// 일반적인 접근
fun createInstance(args: Bundle) : MyFragment {
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
// 향상된 접근
fun createInstance(args: Bundle) = MyFragment().apply { argument = args }
fun createInstance(args: Bundle) = MyFragment().also { it.argument = args }
this를 반환하기 때문에 체이닝이 가능하다.
// 일반적인 접근
fun createIntent(intentData: String, intentAction: String) : MyFragment {
val intent = Intent()
intent.action = intentAction
intent.data = Uri.parse(intentData)
return intent
}
fun createIntent(intentData: String, intentAction: String) = Intent()
.apply { action = intentAction }
.apply { data = intentData }
fun createIntent(intentData: String, intentAction: String) = Intent()
.also { it.action = intentAction }
.also { it.data = intentData }
위에서 설명한 표준함수들을 선택하는데 있어 선택장애자를 위한 표는 다음과 같다.