작성
·
369
·
수정됨
4
람다가 불려지는 시점에서 존재하는 것을 포획해서 정보를 갖고 있는다고 하셨는데요
제가 이해한 바로면 아래와 같은 코드에서 계속 1 1 1 1 1 1 1 이 출력되어야 하는데 1 1 1 2 2 2 가 출력됩니다.
포획해서 갖고 있는 다는 것의 정확한 의미가 무엇일까요?
이거 의미 생각해보고 여러가지로 테스트해보느라 벌써 한시간 동안 해맸습니다 ㅠ
질문1. 포획하는 시점이 runFilter가 호출될때일까요? runFilter내에서 filter가 호출되는 시점일까요? (강의에서 말씀하신 의미는 후자같긴합니다.)
질문2(*). 제일 햇갈리는 부분입니다. 포획하는 지점이 어쨋든 간에 아래의 코드의 경우에는 obj1이 참조하고 있는 객체(obj.numberStr이 1인 것)일텐데 그럼 람다내에서는 계속 포획한 객체 obj1에 해당하는 값인 1이 출력되어야 될텐데(300ms뒤에 obj1의 레퍼런스참조가 obj2로 바뀐다고 하더라도).... 왜 포획을 했음해도 불구하고 출력이 2가 나오는 것일까요? 클로져에서 포획한다는 것의 의미를 잘 모르겠습니다.
fun main(args: Array<String>) {
var obj1 = Obj()
obj1.numberStr = "1"
var obj2 = Obj()
obj2.numberStr = "2"
Thread() {
Thread.sleep(300)
obj1 = obj2
}.start()
runFilter { // 질문1. 포획하는 시점 이곳?
println(obj1.numberStr)
Thread.sleep(100)
println(obj1.numberStr)
Thread.sleep(100)
println(obj1.numberStr)
Thread.sleep(100)
println(obj1.numberStr)
Thread.sleep(100)
println(obj1.numberStr)
Thread.sleep(100)
println(obj1.numberStr)
}
}
fun runFilter(filter: () -> Unit) {
filter() // 질문1. 포획하는 시점 이곳?
}
class Obj{
var numberStr :String? = null
}
감사합니다.
답변 2
3
2
안녕하세요 용조님!! 너무너무 좋은 질문 감사드립니다!! 🙏🙏
질문 주신 3가지 내용에 대해 천천히 답변해보도록 하겠습니다!!!
[1. 포획의 시점]
우선 코드를 단순화 화면 아래와 같습니다!
fun main() {
var obj1 = Obj()
runFilter { // 1번 지점
println(obj1.numberStr)
}
}
fun runFilter(exec: () -> Unit) {
exec() // 2번 지점
}
여기서 obj1이 포획되는 시점을 질문 주셨는데요!! '포획'이라는 행위를 어떤 주체 입장에서 보냐에 따라 시점이 달라질 수 있을 것 같아요!!!
어떤 말씀이냐면요, 포획당하는 입장 (obj1) 에서 보면 최초로 람다 안으로 들어간 것은 1번 지점입니다. 어쨌거나 runFilter { }
안에 obj1
이 들어있고 runFilter로 넘겨진 함수가 실행되는지와 무관하게, 그 함수에서 obj1
을 참조할 수 있게 되니까요!
반면 포획해서 실행하는 입장 (exec) 에서 보면 2번 지점에 포획을 한 것처럼 느껴집니다. obj1
을 가져오긴 했지만 실제 사용하는 것은 함수가 호출되는 시점 (exec()
) 에 진정한 포획을 확인할 수 있기 때문이죠!
이 부분은 아래에서 답변 이어 드려보도록 하겠습니다.
[2. 도대체 왜 2가 중간에 튀어나오는가]
크으~~ 👍 너무 좋은 질문이십니다! 이 현상을 이해하기 위해서는 도대체 Java와 다르게 Kotlin이 어떻게 var 변수를 가져올 수 있는지를 이해해야 합니다
https://medium.com/@yangweigbh/how-kotlin-lambda-capture-variable-ef90e11e531d
에 자세히 소개되어 있지만, 간략하게 말씀드리자면, lambda가 var 변수를 참조하는 순간, var 변수를 Object로 한 번 감싸(블로그에 나오는 ObjectRef입니다) 그 변수를 기억하게 됩니다.
그리고 이는 단순한 값의 복사가 아니라 해당 변수의 '레퍼런스'자체를 가지고 있게 되기 때문에 그 변수가 변경되면 그 변수를 사용하는 람다에서도 똑같이 바뀌게 되는 것이죠!
아래 case와 비슷하다고 생각하시면 됩니다!!!
fun bugFunction(list: MutableList<Int>) {
list.add(3) // 3을 넣어버린다!! list는 함수가 끝나도 3이 추가되어 있다!!!!!
}
fun notBugFunction(num: Int) {
num + 3 // 3을 더해버린다!! 하지만 함수가 끝나면 놀라울 정도로 아무 상관이 없다.
}
// 이런 차이는 list는 MutableList의 레퍼런스를 전달하고, num은 값 자체를 전달하기 때문.
좋습니다!! '포획(capture)'라고 하는 것이 단순히 값을 복사한다는게 아니라 Object 형태로 해당 값을 가라키게끔 하고 있기 때문에, 그 레퍼런스를 타고 들어가 원본을 바꿀 수 있고 반대로 바껴진 원본이 람다 안에서도 영향을 끼친다고 정리할 수 있을 것 같습니다!!
그렇다면, 자연스럽게 1번에 대한 답변도 가능할 것 같아요! 포획의 시점은 사실 중요하지 않습니다! (관점에 따라 달라지니까요) 중요한건, 포획이라는 것이 단순한 값-복제가 아니라는 것입니다!
[3. 설명하신 것이 위와 동일한 내용인가]
네네, 비슷합니다! 적어주신 사례에서도 a = 1
이라는 값을 바로 가지고 가는게 아니라 ObjectRef(a = 1)
과 같이 람다에서는 생각하게 되고, 때문에 같은 ObjectRef를 가리키는 람다에서는 1 + 1을 원본에도 영향을 주어 1 / 2가 나오게 되는 것이죠!
그리고 mylambda1과 mylambda2는 서로 다른 지역변수 (둘다 a = 1이었음!!) 를 가리키는 상황이기에 1 2 1 2 가 출력되게 되는 것입니다.
아마 캡쳐라는 것을 decompile 로직과 함께 확인해보신다면 바로 어떤 느낌인지 감을 잡으실 것 같아요!!
아이고~ 제 답변이 도움이 되었으면 좋겠네요!! 🙏🙏
혹시 아직 감이 잘 오지 않는 부분이 있으시다면 편하게 댓글 남겨주세요!!
항상~ 감사합니다!! 🙇🙇