참조 타입의 캡처와 캡처 리스트는 값 타입과 비슷한 형태를 가지고 있지만, 내부 동작 및 결과에 차이점이 있습니다.
클래스로부터 만들어진 인스턴스의 속성값 주소를 클로저가 참조할 때 속성값의 주소가 할당된 변수의 주소를 캡처 하여 클로저의 힙 영역에 저장합니다.
클래스로부터 만들어진 인스턴스의 속성값 주소를 클로저 (캡처리스트)가 참조할 때 속성값의 주소 자체를 캡처하여 클로저의 힙 영역에 저장합니다.
(참조 타입에서 속성값의 주소 자체는 스택 영역에 있습니다)
참조 타입의 캡처와 캡처 리스트 코드 구현
class Number{
var num: Int
init(num: Int){
self.num = num
}
}
var A = Number(num: 10) //인스턴스 A 생성과 동시에 RC(참조 카운팅) 1 증가
var B = Number(num: 10) //인스턴스 B 생성과 동시에 RC(참조 카운팅) 1 증가
print("A의 값: \(A.num), B의 값: \(B.num)") // A의 값: 10, B의 값: 10
var captureList = { [A] in // 캡처 리스트를 위한 클로저 정의 + A 인스턴스의 RC 1 증가
print("A(캡처 리스트)의 값: \(A.num), B의 값: \(B.num)")
}
A.num = 100 // 초깃값 변경
B.num = 100 // 초깃값 변경
captureList() // A(캡처 리스트)의 값: 100, B의 값: 100
A.num = 777 // 캡처 리스트 클로저 동작 후 초깃값 변경
B.num = 777 // 캡처 리스트 클로저 동작 후 초깃값 변경
captureList() // A(캡처 리스트)의 값: 777, B의 값: 777 => "A: 100, B: 777"이 아님
캡처 리스트를 통해 클로저의 힙 영역에 저장된 속성값이 변할 수 있었떤 이유는 근본적으로 참조 타입은 스택 영역의 값이 순수한 값이 아닌 힙을 참조하는 주솟값이기 떄문이다.
클래스로부터 만들어진 인스턴스(객체)또는 클로저가 서로를 참조하여 발생하는 문제이다.
class Man{
var name: String
var run: (()->Void)?
init(name: String){
self.name = name
}
func runClosure(){
run = {
print("\(self.name)이 달리고 있습니다.")
}
}
deinit{
print("\(self.name) 메모리에서 제거되었습니다.")
}
}
func doSomething(){
var kim: Man? = Man(name: "김철수") // kim 인스턴스 생성 (kim RC 1증가)
kim?.runClosure() // 클로저(run)가 메모리의 Heap 영역에 생성
}
doSomething() // 아무 출력 없음
-> 인스턴스의 카운트는 1, 클로저의 카운트는 1
-> kim 인스턴스와 클로저가 강한 참조 사이클을 유지하고 있기 때문에 소멸자가 동작하고 있지 않음
class Man{
var name: String
var run: (()->Void)?
init(name: String){
self.name = name
}
func runClosure(){
run = { [weak self] in
print("\(self?.name)이 달리고 있습니다.")
}
}
deinit{
print("\(self.name) 메모리에서 제거되었습니다.")
}
}
func doSomething(){
var kim: Man? = Man(name: "김철수") // kim 인스턴스 생성 (kim RC 1증가)
kim?.runClosure() // 클로저(run)가 메모리의 Heap 영역에 생성
}
doSomething() // 김철수 메모리에서 제거되었습니다.
후행클로저는 함수의 마지막 인자로 클로저 표현식을 함수에 전달하거나 클로저 표현식이 긴 경우에 사용한다.
func travel(action: () -> Void) {
print("I'm getting ready to go.")
action()
print("I arrived")
}
위 travel 메서드는 인자로 action이라는 Closure을 채택하고 있다. action은 함수 내부에서 두번의 프린트 사이에 실행된다. 우리는 위 travel 메서드를 아래와 같은 방식으로 사용할 수 있다. (인자가 없기 때문에 () 생략 가능)
travel() {
print("I'm driving in my car")
}
사용자가 앱을 사용하는 동안에 앱을 업데이트를 진행하고 업데이트가 끝나면 이를 반드시 사용자에게 알려야 하는 상황이라면 업데이트가 끝났음을 알려야한다. 이렇듯 Completion Handler는 어떠한 일이 끝났을 때 진행할 업무를 담당한다.
ViewController로 화면을 전환하는 코드
import UIKit
let firstVC = UIViewController()
let nextVC = UIViewController()
firstVC.present(nextVC, animated: true, completion: nil)
completion 파라미더에는 () -> () 또는 () -> Void 타입의 클로저 블락을 사용할 수 있다. 아래와 같이 completion 파라미터에 클로저를 적용시켜 본다.
firstVC.present(nextVC, animated: true, completion: { () in print("화면 전환 완료")})
firstVC.present(nextVC, animated: true, completion: { print("화면 전환 완료")})
// traling Closure
firstVC.present(nextVC, animated: true){ print("화면 전환 완료")}
위 메서드가 정상적으로 실행되면 nextVC 가 팝업되면서 콘솔창에는 "화면전환 완료"라는 문자열이 출력된다.
let handleBlock: (Bool) -> Void = { doneWork in
if doneWork {
print("퇴근하겠습니다")
}
}
handleBlock(true) // 퇴근하겠습니다.
let handleBlock: (Bool) -> Void = {
if $0 {
print("퇴근하겠습니다")
}
}
handleBlock(true) // 퇴근하겠습니다.
Alamofire.request("https://google.com").responseJSON { response in
print(response)
}
메서드의 request가 서버로 전송되어 이에 대한 결과값이 response 인자에 담겨 이를 클로저 내부에서 사용할 수 있게 된다.
이렇듯 swift를 이용하다보면 함수의 마지막 인자로 Closure를 적용하는 경우를 심심치 않게 살펴볼 수 있다.
이는 함수가 종료된 직후에 이벤트 처리를 위해 매우 많이 사용된다.
클로저가 함수의 인자로 전달됐을 때, 함수의 실행이 종료된 후 실행되는 클로저, Non-Escaping 클로저는 이와 반대로 함수의 실행이 종료되기 전에 실행되는 클로저
func runClosure(closure: () -> Void) {
closure()
}
이렇게 클로저가 함수가 종료되기 전에 실행되기 때문에 closure는 Non-Escaping 클로저 입니다.
class ViewModel {
var completionhandler: (() -> Void)? = nil
func fetchData(completion: @escaping () -> Void) {
completionHadler = completion
}
}
컴파일러의 퍼포먼스와 최적화때문.
non-escaping 클로저는 컴파일러가 클로저의 실행이 언제 종료되는지 알기 때문에 때에 따라 클로저에서 사용하는 특정 객체에 개한 retain, release 등의 처리를 생략해 객체의 라이프 사이클을 효율적으로 관리할 수 있다.
반면 escaping 클로저는 함수 밖에서 실행되기 때문에 클로저가 함수 밖에서도 적절히 싱행되는 것을 보장하기 위해 추가적인 참조 사이클을 관리해주어야 한다. 이는 퍼포먼스에 영향을 미치기 때문에 필요할때만 사용해야 한다.
새로운 에러 처리 모델이 도입되었고 현재까지 사용되고 있다.
에러가 발생할 수 있는 코드를 throwing function으로 선언하고 do-catch 문에서 try 표현식을 통해 함수를 호출하고 발생한 에러를 처리한다. 에러형식은 특별한 프로토콜을 채용한 형식으로 선언한다.
enum MyError: ErrorType {
case someError
case criticalError
}
func doSomething() throws {
throw MyError.someError
}
do {
try doSomething()
} catch let myError as MyError {
switch myError {
case .someError:
print("someError")
case .criticalError:
print("criticalError")
}
} catch {
print(error.localizedDescription)
}
이 방식의 한계
새롭게 도입된 Result Generic Enumeration으로 선언되어 있다
public enum Result<Success, Failure> where Failure: Error
형식이 정해져 있다 즉 형식에 관한 모호함이 사라지게 된다. Result는 성공과 실패 2가지가 존재한다. Success에는 작업의 결과가 저장되고 Failure에는 에러가 저장된다.
func isOddNumber(number: Int) -> Result<Int, NumberError> {
guard number >= 0 else {
return Result.failure(NumberError.negativeNumber)
guard !number.isMultiple(of: 2) else {
return .faulure(.evenNumber)
}
return .success(oddNumber * 2)
}
let result = isOddNumber(number: 1)
switch result {
case .success(let data):
print(data)
case .failure(let error):
print(error.localization)
}
if let successResult = try? result.get() {
print(successResult)
}