오늘은 문제를 다시 풀어보며, 아직 제대로 자리잡지 못한 개념들을 정리하는 시간을 가졌다. 아직, 완전하게 이해했다라고는 할 수 없지만 천천히 개념을 정리하고 동작 순서에 관한 생각을 직접 그려보며 정리하다보니 조금은 이해가 되는 것 같다. 또한, 과제 해설 영상을 통해 본인이 생각했던 방식과 다른 방식의 해설을 이해하고 하나의 문제를 여러 방면으로 접근하는 것에 대해 공부할 수 있었다.
도전 문제 4. 를 풀이하면서 제대로 이해가 가지 않는 부분들이 존재했었다. 또한, 1차로 풀이를 제출하기까지 개념을 제대로 정리하지 못해서 조금 부족하게 제출했던 것 같다.
앞서, 8일차에 제출 코드가 존재하니 당시 코드는 첨부하지 않고 변경된 코드를 첨부하고 이에 대해 학습한 개념들을 정리하도록 하겠다.
class A{
var name: String
init(name: String){
self.name = name
}
var b: B?
deinit{ print("\(name) is deinitialized")}
}
class B{
let age: Int
init(age: Int) {
self.age = age
}
var b: A?
deinit{ print("\(age) is deinitialized")}
var closure: (()->Void)?
}
var a: A? = A(name: "A")
var b: B? = B(age: 26)
a?.b = b
b?.closure = {[a] in
print("\(a?.name ?? "없음")")
}
a = nil
b = nil
제출할 당시의 코드를 살펴보면 문제의 요구사항에 맞지 않는 점이 존재한다.
"클로저 기반의 순환 참조를 발생시켜보라".
당시의 코드는 인스턴스 기반 순환 참조와 클로저 기반의 순환 참조 두가지가 모두 적용되는 순환 참조를 발생시키고 있었다. 위와 같이 변경을 진행하며, 클로저의 캡쳐와 캡쳐 리스트에 관해 보다 깊이 공부하였다. 물론, 아래의 정리는 본인의 문제를 토대로 진행한 정리이다.
이 부분이 초기에 가장 이해가 되지 않던 부분이었다. 분명, deinit이 출력되지 않고 순환 참조가 발생해야 하는데 deinit이 출력되는 경우가 발생했다. 본인이 학습한 내용을 정리해보자.
지역 변수
클로저가 지역 변수를 참조할 경우(Class), 지역 변수는 함수가 종료될 경우 사라지기 때문에 클로저가 저장하는 방식에서 전역 변수 참조와의 차이점이 존재한다. 클로저는 "이 변수는 함수가 종료되면 사라지기 때문에 a라는 변수 대신 A라는 인스턴스의 주소를 저장해둬야겠다."라고 생각하여 동작한다. 때문에, a = nil이 될 경우에도, 클로저는 A라는 인스턴스의 주소를 참조하고 있기 때문에, 고리가 끊어지지 않게 되어 순환 참조가 발생한다.
전역 변수
클로저가 전역 변수를 참조할 경우(Class), 전역 변수는 대개 변수가 사라지지 않기 때문에, 클로저가 a라는 변수를 참조 타입으로 저장한다. 때문에, a = nil이 될 경우, 클로저가 A라는 인스턴스에 접근할 방법이 사라지게 되며 연결된 고리도 끊어지게 되어 순환 참조가 발생하지 않는다.
지역 변수 참조 시에 기본은 참조 캡쳐를 사용한다.
캡쳐 리스트 없이 사용한다.
동작 : 밖에서 참조하는 값을 변경하면 변경되어서 출력된다.
특징 : 지역 변수는 함수가 종료되면 사라진다. Swift에서는 참조하고 있는 변수가 사라지지 않도록 Heap 메모리로 옮겨서 저장한다.(Variable Capture라고 부른다.)
캡쳐 리스트를 사용한다.
캡쳐 리스트([ ]), 대괄호 안에 들어가는 복사 대상이 어떤 타입이냐에 따라 Value Capture가 다르게 일어난다. 내부에 Class가 들어가면 Class는 참조 타입이므로 데이터가 아닌 데이터가 저장된 주소가 복사되며 Int가 들어가게 될 경우에는 값 타입이므로 데이터가 복사된다. 즉, 값 캡쳐를 사용하더라도 참조 타입의 복사 대상을 사용하게 된다면 참조 캡쳐와 마찬가지로 주소를 저장하기에 데이터의 변경이 반영된다.
그렇다면 내가 작성한 코드에서 deinit이 출력되었던 이유에 대해서 생각해보자.
b?.closure = {
print("\(a?.name ?? "없음")")
}
b?.closure?()
위 코드는, 전역 변수 a를 참조하고 있으며, 참조 캡쳐를 사용하고 있다. 전역 변수 a를 참조한다는 말은, "a는 사라지지 않으니까 A라는 인스턴스의 주소를 직접 저장할 필요는 없어."라는 생각으로 이어지게 되며, 이로 인해 a = nil이 되면 A와의 연결 고리가 끊어지게 되어 deinit이 출력되게 된다.
반면, 클로저에 "[a] in"이라는 값 캡쳐를 사용하게 된다면 "a라는 상자 내부의 데이터를 지금 바로 복사해서 저장하겠다"라는 뜻이 된다. 결국, a는 Class로 참조 타입이기 때문에 데이터가 저장되지 않고 인스턴스의 주소가 저장되게 되어 순환 참조가 발생하게 되는 것이다.
알고리즘, 접근 방법에 관해서 크게 변경된 부분은 없었다. 문법 문제이기 때문에 접근 방법은 기존에 공부해오던 부분들과 크게 다르지 않아서 변경된 부분은 for문을 이용해서 반복했던 것들을 고차함수를 상황에 맞게 적용시키는 것이었다. 그렇다면 수정된 코드를 정리하여 기록하고 다음부터 사용할 수 있도록 기억해두자.
result = numbers.map(String.init)
//result = numbers.map{String($0)}
var res = num
.filter{ $0.isMultiple(of: 2)}
.map(String.init)
//var res:[String] = num.filter{$0 % 2 == 0}.map{String($0)}
func myMap(_ arr:[Int], toStr: (Int) -> String) -> [String]{
arr.map(toStr)
}
//func myMap(_ arr:[Int], toStr: (Int) -> String) -> [String]{
// var myres: [String] = []
// for idx in arr{
// myres.append(toStr(idx))
// }
// return myres
//}
func a(arr: [Int]) -> [Int]{
arr.enumerated()
.filter{(idx,_) in
!idx.isMultiple(of: 2)
}
.map{ (_,value) in
value
}
}
let iArray = [1,2,3,4,5]
print(a(arr: iArray))
//func a(arr: [Int])->[Int]{
// var iArr: [Int] = []
// for idx in 0..<arr.count{
// if idx % 2 != 0{
// iArr.append(arr[idx])
// }
// }
// return iArr
//}
arr.forEach{
if let r = $0 as? Robot{
r.batteryCharge()
return
}
if let c = $0 as? Cat{
c.meow()
return
}
if let d = $0 as? Dog{
d.bark()
return
}
}
//for idx in arr{
// print(idx.introduce())
// if let r = idx as? Robot{
// r.batteryCharge()
// } else if let c = idx as? Cat{
// c.meow()
// } else if let d = idx as? Dog{
// d.bark()
// }
//}