-
Closure (2)iOS/Swift 2020. 4. 17. 03:21
@escaping
@escaping 속성은 인자 값으로 전달된 클로저를 저장해 두었다가, 나중에 다른 곳에서도 실행할 수 있도록 허용해주는 속성이다.
func callback(fn: () -> Void) { fn() } callback { print("Closure가 실행되었습니다.") }
정의된 함수 callback(fn:)은 매개변수를 통해 전달된 클로저를 함수 내부에서 실행하는 역할을 한다.
func callback(fn: () -> Void) { let f = fn // 클로저를 상수 f에 대입 f() // 대입된 클로저를 실행 }
만약 함수를 위와 같이 바꾼다면 오류가 출력된다.
전달된 클로저를 변수에 대입한 후 실행하는 것이 안된다는 것이다.
오류의 내용은 Non-escaping 파라미터인 'fn'은 오직 직접 호출하는 것만 가능하다는 의미인데, 어떤 뜻일까?
스위프트에서 함수의 인자값으로 전달되는 클로저는 기본적으로 탈출 불가(non-escaping)의 성격을 가진다.
이는 해당 클로저를 함수내에서 직접 실행을 위해서만 사용해야 하는 것을 의미하고, 이 때문에 함수 내부여도 변수나 상수에 대입할 수 없다.
변수나 상수에 대입하는 것을 허용한다면 내부 함수를 통한 캡처 기능을 이용해 클로저가 함수 바깥으로 탈출할 수 있기 때문이다.
또한, 인자값으로 전달된 클로저는 중첩된 내부 함수에서 사용할 수도 없다.
내부 함수에서 사용할 수 있도록 허용할 경우, 이 역시 콘텍스트의 캡처를 통해 탈출될 수도 있기 때문이다.
하지만 클로저를 변수나 상수에 대입하거나 중첩 함수 내부에서 사용해야할 경우도 있다.
이때 사용되는 것이 @escaping 속성이다.
이 속성을 클로저에 붙여준다면 해당 클로저는 탈출이 가능한 인자값으로 설정된다.
(앞의 탈출불가 제약 조건들이 모두 사라진다.)func callback(fn: @escaping () -> Void) { let f = fn // 클로저를 상수 f에 대입 f() // 대입된 클로저를 실행 }
@escaping 속성을 추가하면 오류 발생 없이 실행이 가능하다.
(@escaping 속성은 인자 값에 설정되는 값이므로, 함수 타입 앞에 넣어주어야 한다.)
그렇다면 인자값으로 전달되는 클로저의 기본 속성이 탈출 불가로 설정된 이유는 어떤 것일까?가장 큰 이점은 컴파일러가 코드를 최적화하는 과정에서의 성능 향상이다.
또한, 탈출불가 클로저 내에서는 self 키워드를 사용할 수 있다.(이 클로저는 해당 함수가 끝나서 리턴되기 전에 호출될 것이 명확하기 때문이다.)
따라서 클로저 내에서 self에 대한 약한 참조를 사용할 필요가 없다.
@autoclosure
@autoclosure 속성은 인자 값으로 전달된 일반 구문이나 함수 등을 클로저로 래핑 하는 역할을 한다.
이 속성이 붙어 있을 경우, 일반 구문을 인자값으로 넣어도 컴파일러가 알아서 클로저로 만들어서 사용한다.
이 속성을 적용하면 인자값을 직접 클로저 형식으로 넣어줄 필요가 없기 때문에 {} 형태가 아니라 () 형태로 사용할 수 있다는 장점이 있다.
func condition(stmt: () -> Bool) { if stmt() == true { print("결과가 참입니다.") } else { print("결과가 거짓입니다.") } } // 실행 방법 1 : 일반 구문 condition(stmt: { 4 > 2 }) // 실행 방법 2 : 클로저 구문 condition { () -> Bool in return (4 > 2) } condition { return (4 > 2) } condition { 4 > 2 }
위에서 작성된 실행 방법 1, 2에서 실제 전달하고 싶은 것은 '4 > 2' 구문이다.
하지만 일반 실행 구문이나 트레일링 클로저 어느 것을 적용해도, 해당 구문을 {} 형태로 감싸 클로저 형태로 만든 후 인자 값으로 전달해야 한다.
func condition(stmt: @autoclosure () -> Bool) { if stmt() == true { print("결과가 참입니다.") } else { print("결과가 거짓입니다.") } } // 실행 방법 condition(stmt: (4 > 2))
하지만 @autoclosure 속성을 적용한다면 위와 같이 간단하게 구문 실행이 가능하다.
(@autoclosure를 사용한다면 더이상 일반 클로저를 인자 값으로 사용할 수 없기 때문에 실행 방법 1, 2와 같은 방법으로 구문 실행이 불가능하다.)
또한, @autoclosure 속성을 적용하면 지연된 실행이 가능하다.
var arrs = [String]() func addVars(fn: @autoclosure () -> Void) { arrs = Array(repeating: "", count: 3) fn() } // arrs.insert("KR", at: 1) 오류 addVars(fn: arrs.insert("KR", at: 1))
arrs.insert("KR", at: 1)은 arrs 배열의 두번째 인덱스에 "KR"을 입력하는 것인데 아직 배열의 인덱스가 그만큼 확장되어있지 않기 때문에 오류가 발생한다.
하지만 addVar(fn:)를 사용해 위의 구문을 적용한다면 오류가 발생하지 않는데, 이것이 지연된 실행이다.
@autoclosure 속성이 부여된 인자값은 컴파일러에 의해 클로저, 즉 함수로 감싸 지기 때문에 위와 같이 작성해도 addVars(fn:) 함수 실행 전까지는 실행되지 않으며, 해당 구문이 실행될 때에는 이미 배열의 인덱스가 확장된 후이므로 오류도 발생하지 않는다.
즉, @autoclosure 속성이 인자값에 부여된다면 해당 인자 값은 컴파일러에 의해 클로저로 자동 래핑 된다.
이 때문에 함수를 실행할 때에는 {} 형식의 클로저가 아닌 () 형식의 일반값을 인자 값으로 사용해야 하며, 인자 값은 코드에 작성된 시점이 아니라 해당 클로저가 실행되는 시점에 맞추어 실행된다.
'iOS > Swift' 카테고리의 다른 글
[Swift] ARC 알아보기 (Auto Reference Counting) (0) 2020.09.04 Closure (0) 2020.04.11 프로토콜 지향언어, Swift (3) (0) 2020.03.21 프로토콜 지향언어, Swift (2) (1) 2020.03.14 프로토콜지향 언어, Swift (1) (0) 2020.03.07