DID 스터디 3회차(kotlin let/run/also/apply/with)
DID 안드로이드 앱개발을 위해 kotlin 을 다시 공부하는 중이다.
let 확장함수 코드를 보고 이전에 배웠던 let/run/apply/also/with 들을 다시 정리했다.
그전에 코틀린에서 많이 보이는 함수 패턴 중 하나를 간단히 살펴보자.
아래 func 함수는 두개의 인자를 받는다.
1. Int 타입의 age
2. Int 를 받아 Unit 을 리턴(Void 리턴) 하는 process(함수 타입)
process 변수에 담길 함수는 전달받은 age 만 출력하는 단순한 로직이지만 확장성 있는 구조를 보여준다. preProcess 와 postProcess 를 계속 재활용하면서 process 부분만 새롭게 바꿀 수 있다.
@Test
fun test1() {
// 코틀린에서 많이 보이는 패턴
func(34) { age ->
println("age : $age")
}
}
private fun func(age: Int, process: (Int) -> Unit) {
// preProcess
process(age)
// postProcess
}
이제 let 확장함수를 살펴보자. Sample 클래스에서 let 함수를 만든적이 없지만 Sample(1, 1, 1).let 을 사용한걸 확인할 수 있다. let 정의를 보면 T.let 으로 되어 있기 때문에 가능한 일이다. let 두번째 인자인 block 함수에서는 만든 Sample 객체를 전달받아 별도 R 타입으로 리턴이 가능하다. 다시말해 리턴타입을 별도로 지정할 수 있다. 아래 코드에서는 "ybs" 문자열을 리턴시켰다(리턴 지시자를 안써도 된다 마지막줄이 리턴될 값이다).
class UnitTest {
@Test
fun testLet() {
val name : String = Sample(1, 1, 1).let { sample ->
sample.a
sample.b // private 이라 접근 못함
sample.c // Unresolved reference: c
"ybs"
}
print(name)
}
}
class Sample(
var a: Int,
private val b: Int,
c: Int
)
- - - - - - - - -
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
이제 let 을 사용하는 안드로이드 코드를 살펴보자. onCreate 함수에서 arguments 변수 값이 존재하면 null 이었던 item 값이 채워진다.
class credentialDetailFragment : Fragment() {
private var item: JSONObject? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
item = JSONObject(it.getString(ARG_CREDENTIAL))
activity?.toolbar_layout?.title = getString(R.string.title_credential_detail)
}
}
...
그리고 onCreateView 함수에서 item 값이 있으면(변수명은 item 이지만 JSONObject 객체) 중괄호 안에서 it 으로 전달받고 사용하는것을 볼 수 있다.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.credential_detail, container, false)
item?.let {
val attrs = it.getJSONObject("attrs")
val date = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH).parse(attrs.getString("date")).toLocaleString()
rootView.credential_detail.text =
"""
Issuer organization: ${attrs.getString("organization")}
Vaccine name: ${attrs.getString("vaccine")}
Total doses: ${attrs.getString("doses")}
Recipient name: ${attrs.getString("target")}
Date of issueance: $date
Revocation ID: ${it.getString("cred_rev_id")}
""".trimIndent()
}
return rootView
}
위 코드들을 보면서 let 확장함수는 null 이 아닐 경우에 이어지는 작업을 자연스럽게 진행시켜준다는 느낌을 받았다.
cf) let 과 비슷한 확장함수로 run 도 있다.
@Test
fun testRun() {
// fun <T, R> T.run(block: T.() -> R): R {
val name: String = Sample(1, 1, 1).run {
this.a
"ybs"
}
println(name)
}
다음은 also / apply 에 대해서 알아보자. let / run 과는 다르게 리턴타입을 바꿀 수 없다. 항상 T 타입으로 고정이므로 Sample 객체로 고정이다. 그래서 Sample 객체를 생성한 후 추가적인 셋팅이 필요할 때 사용하면 좋다.
@Test
fun testAlso() {
// fun <T> T.also(block: (T) -> Unit): T , 현재 사용중인 객체에 also 를 집어 넣는 확장함수
val sample = Sample(1, 1, 1).also { sample ->
sample.a = 10
// return 값은 항상 sample 객체다. 왜냐면 also 확장함수 return 이 항상 this 이므로.
}
// also 는 객체를 만들고 나서 추가적인 set 이 필요할 때 쓰면 좋다.
println(sample.a)
}
@Test
fun testApply() {
// fun <T> T.apply(block: T.() -> Unit): T {
// also 는 파라미터로 this 를 넣어줬는데 apply 는 안넣어준다.
val sample = Sample(1, 1, 1).apply {
this.a = 10
}
// also 와 큰 차이없고, 마찬가지로 추가적인 set 할 때 쓰면 좋다.
println(sample.a)
}
마지막으로 with 은 T 타입의 receiver 파라미터를 받아 block 함수를 호출한다. block 함수는 R 타입을 리턴하므로 sample 객체와는 다른 값으로 리턴이 가능하다.
@Test
fun testWith() {
val sample = Sample(1, 1, 1)
val result: Int = with(sample) {
println(a) // a 프로퍼티는 sample 에 있는것
println(this.a)
println(sample.a)
100 + a // 마지막이 return 값
}
println(result)
}
- - - - - - - - - - - - - -
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}