클로저에 대해서 알아보자!!!
https://h2kangrok.tistory.com/4
[Swift] Swift의 $0구문은 어떻게 나온걸까?
최근 코테 공부를 하다가 $0를 사용하게 되었다!! 도대체 $0 뭐지??? 어디서 나온 거지??? 같이 알아보자! selectArr = arr.filter { $0 != arr[i] && $0 != arr[j] } 무엇인지 찾아보니 🤔 '$0은 클로저에서 "첫 번
h2kangrok.tistory.com
예전에 클로저(Closures)에 대해서 정말 간단하게 알아봤었는데
시간이 지난 지금 클로저도 많이 사용해 보았기에 조금 더 깊게 알아보자!!
클로저는 명명된 함수 생성 없이 실행되는 코드 그룹!!
명명되다는 말이 사람, 물건, 일 등에 이름이 붙여지는 것을 말함. 망명된 함수 생성 없이 실행된다는 걸 보니 Unnamed Closure 즉 익명함수라고 부름.
보통 클로저라고 하면 Unnamed Closure를 말함!!
클로저는 1급 객체인데, 이는 스위프트 함수가 1급 객체(1st class object)의 특성을 가지고 있기 때문임.
변수 또는 상수에 '클로저'를 담을 수 있고, 인자(파라미터)로 '클로저'를 전달할 수 있고, 반환값(리턴벨류)으로 '클로저'를 전달할 수 있음!!!
클로저는 정의된 컨텍스트에서 모든 상수와 변수에 대한 참조를 캡처하고 저장할 수 있음. 이러한 상수와 변수를 폐쇄(closing over)라고 함. Swift는 캡처의 모든 메모리 관리를 처리함.
클로저는 3가지중 하나를 취함.
- 전역 함수는 이름을 가지고 어떠한 값도 캡처하지 않는 클로저임.
- 중첩함수는 이름을 가지고 둘러싼 함수로부터 값을 캡처할 수 있는 클로저임.
- 클로저 표현식은 주로 컨텍스트에서 값을 캡처할 수 있는 경량 구문으로 작성된 이름이 없는 클로저임.
Swift의 클로저 표현식은 일반 시나리오에서 간단하고 깔끔한 구문을 장려하는 최적화를 통해 깔끔하고 명확한 스타일을 가지고 있음.
이러한 최적화에는 다음이 포함됨.
- 콘텍스트에서 파라미터와 반환값 타입 유추
- 단일 표현식 클로저의 암시적 반환
- 약식 인수 이름
- 후행 클로저 구문
🎯 클로저 표현식 (Closure Expressions)
중첩함수에서 소개된 중첩 함수는 더 큰 함수에 부분으로 자체 포함된 코드 블록의 이름을 지정하고 정의하기 편리한 수단임. 그러나 완전한 선언과 이름 없이 함수와 유사한 구조의 짧은 버전을 작성하는 것이 때때로 유용함. 함수를 하나 이상의 인수로 사용하는 함수 또한 메서드로 작업할 때 특히 그럼.
클로저 표현식은 간단하고 집중적인 구문으로 인라인 클로저로 작성하는 방법임. 클로저 표현식은 명확성이나 의도를 잃지 않고 짧은 형태로 클로저를 작성하기 위한 몇 가지 구문 최적화를 제공.
한 가지 예를 들어보자!! 아래 클로저 표현식 예제는 여러 반복에 걸쳐 sorted(by:) 메서드의 단일 예제를 구체화하는 최적화를 나태냄. 각 예제는 동일한 기능을 보다 간결한 방식으로 표현함!!
🎯 정렬 메서드 (The Sorted Method)
Swift의 표준 라이브러리는 사용자가 제공하는 정렬 클로저의 출력을 기반으로 알려진 타입의 값 배열을 정렬하는 sorted(by: ) 라는 메서드를 제공. 정렬 프로세스가 완료되면 sorted(by: ) 메서드는 원본 배열과 같은 타입과 같은 크기의 올바르게 정렬된 요소의 새로운 배열로 반환. 기본 배열은 수정되지 않음!
아래 예제의 클로저 표현식은 알파벳 역순으로 String 값의 배열을 정렬하기 위해 sorted(by: ) 메서드를 사용함.
sorted(by: ) 메서드는 배열 내용과 동일한 타입의 두 인수를 사용하는 클로저를 허용하고 값이 정렬된 후 첫 번째 값이 두 번째 값의 앞 또는 뒤에 표시되어야 하는지 여부를 나타내는 Bool 값을 반환. 정렬 클로저는 첫 번째 값이 두 번째 값이 두 번째 값 앞에 나타나야 하는 경우 true를 반환하고 그렇지 않으면 false를 반환해야 함.
정렬 클로저를 제공하는 한 가지 방법은 올바른 타입의 일반 함수를 작성하고 sorted(by: ) 메서드에 인수로 전달하는 것임.
import Foundation
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func backword(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
let reversedNames = names.sorted(by: backword)
print(reversedNames)
// ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
첫 번째 문자열 (s1)이 두 번째 문자열 (s2) 보다 크다면 backward(_:_:) 함수는 true를 반환하고 이것은 정렬된 배열에 s1 은 s2 전에 나타남. 문자열의 문자가 "더 크다"는 "알파벳 순으로 더 뒤에 나타난다"는 의미함.
이것은 문자 B는 문자 A 보다 "더 크다"이고 문자열 "Tom"은 문자열 "Tim" 보다 더 큼. 알파벳 역순으로 정렬될 때 "Barry"는"Alex" 보다 앞에 위치함.
그러나 이것은 본질적으로 단일 표현식 함수 (a > b)를 작성하는 다소 긴 방식임.
정렬 클로저를 인라인으로 사용하는 것임 좋음!!
🎯 클로저 표현구(Closure Expression Syntax)
클로저 표현구는 아래와 같이 일반적인 형태를 가지고 있음
{ (<#parameters#>) -> <#return type#> in
<#statements#>
}
클로저 표현구의 파라미터는 in-out 파라미터일 수 있지만 기본값을 가질 수 없음. 가변 파라미터의 이름을 지정하면 가변 파라미터를 사용할 수 있음. 튜플은 파라미터 타입과 반환 타입으로 사용될 수도 있음.
let reversedNames = names.sorted(by: {(s1: String, s2: String) -> Bool in
return s1 > s2
})
이 인라인 클로저를 위한 파라미터와 반환 타입 선언은 backward(_:_:) 함수에서 선언한 것과 동일함. 두 경우 모두 (s1: String, s2: String) -> Bool로 작성함. 그러나 인라인 클로저 표현식을 위한 파라미터와 반환 타입은 중괄호 바깥이 아닌 안에 작성함.
클로저의 본문의 시작은 in 키워드로 시작함. 이 키워드는 클로저의 파라미터와 리턴 타입 정의가 끝남을 나타내며 클로저의 본문이 시작됨을 나타냄.
위 예시는 클로저의 본문이 짧기 때문에 한 줄로 작성할 수 있음!!!
let reversedNames = names.sorted(by: {(s1: String, s2: String) -> Bool in return s1 > s2})
이것은 sorted(by: ) 메서드에 대한 전체 호출이 동일하게 유지되었음을 보여줌. 소괄호는 여전히 메서드의 전체 인수를 둘러싸고 있음. 그러나 인수는 인제 인라인 클로저임 !!!
🎯 컨텍스트로 타입 추론(Inferring Type From Context)
정렬 클로저는 메서드에 인수로 전달되기 때문에 Swift는 파라미터 타입과 반환되는 값의 타입을 유추할 수 있음!!!
sorted(by: ) 메서드는 문자열 배열에서 호출되므로 인수는 (String, String) -> Bool 타입의 함수이어야 함.
이는 (String, String)과 Bool 타입을 클로저 표현식 정의에 일부러 작성할 필요가 없음을 의미. 모든 타입은 유추할 수 있기 때문에 반환 화살표 ( -> )와 파라미터의 이름을 둘러싼 소괄호를 생략할 수 있음.
let reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })
함수나 메서드에 클로저를 인라인 클로저 표현식으로 전달할 때 항상 파라미터 타입과 반환 타입을 유추할 수 있음.
결과적으로 클로저가 함수 또는 메서드 인수로 사용될 때 완전한 형태로 인라인 클로저를 작성할 필요가 없음.
그럼에도 불구하고 원하는 경우 타입을 명시적으로 만들 수 있으며 코드를 읽는 자가 모호성을 피할 수 있다면 그렇게 하는 것이 좋음.
sorted(by: ) 메서드의 경우 정렬이 발생하는 사실에서 클로저의 목적이 명확하여 문자열 배열의 정렬을 지원하기 때문에 코드를 읽는 사람이 클로저가 String 값으로 작동할 가능성이 있다고 가정하는 것이 안전함.
🎯 단일 표현 클로저의 암시적 변환 (Implicit Returns from Single-Expression Closures)
단일 표현 클로저 (Single-expression closures)는 이전 예제에서 return 키워드를 생략하여 단일 표현식으로 암시적으로 값을 반환할 수 있음.
let reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })
여기서 sorted(by: ) 메서드의 인수의 함수 타입은 클로저에서 Bool 값이 반환되어야 하기 때문에 명확함.
클로저의 본문에 Bool 값을 반환하는 단일 표현식 (s1 > s2)가 포함되므로 모호하지 않고 return 키워드를 생략할 수 있음 !!
🎯 짧은 인수 이름 (shorthand Argument Names)
Swift는 인라인 클로저에 $0, $1, $2 등 클로저의 인수값으로 참조하는 데 사용할 수 있는 자동적으로 짧은 인수 이름 (shorthand argument names)을 제공함.
클로저 표현식에 이런 짧은 인수 이름을 사용한다면 선언에 클로저의 인수 리스트를 생략할 수 있고 짧은 인수 이름의 수와 타입은 함수 타입에서 유추됨. 클로저 표현식이 본문으로 전체가 구성되기 때문에 in 키워드를 생략할 수 있음.
let reversedNames = names.sorted(by: { $0 > $1 })
여기서 $0와 $1 은 클로저의 첫 번째와 두 번째 String 인수를 참조함. $1 이 짧은 인수에서 가장 높은 숫자이므로 클로저는 2개의 인수가 있다고 이해함. 여기서 sorted(by: ) 함수는 인수가 모두 문자열인 클로저로 기대하므로 짧은 인수 $0과 $1 은 모두 타입 String임.
🎯 연산자 메서드 (Operator Methods)
실제로 위의 클로저 표현식을 더 짧게 작성하는 방법이 있음!! Swift의 String 타입은 보다 큰 연산자 (>)의 문자열별 구현을 String 타입의 파라미터 2개가 있는 메서드로 정의하고 Bool 타입의 값을 반환함. 이것은 sorted(by:) 메서드에 필요한 메서드 타입과 정확하게 일치함. 따라서 간단하게 보다 큰 연산자를 전달할 수 있고 Swift는 문자열 특정 구현을 사용하기 원한다고 유추함.
let reversedNames = names.sorted(by: >)
🎯 후행 클로저 (Trailling Closures)
함수의 마지막 인수로 함수에 클로저 표현식을 전달해야 하고 클로저 표현식이 긴 경우 후행 클로저로 작성하는 것이 유용할 수 있음.
후행 클로저는 함수의 인수이지만 함수 호출의 소괄호 다음에 작성함. 후행 클로저 구문을 사용할 때 함수 호출의 일부로 첫 번째 클로저 인수 라벨을 작성하지 않아도 됨.
함수 호출은 여러 개의 후행 클로저를 포함할 수 있지만 아래 몇 가지 예제에서는 단일 후행 클로저를 사용.
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
// Here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
// Here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
위의 클로저 표현구(Closure Expression Syntax) 섹션에 문자열 정렬 클로저는 후행 클로저로 sorted(by: ) 메서드의 소괄호 바깥에 작성될 수 있음.
let reversedNames = names.sorted() {$0 > $1}
후행 클로저로 표현식이 함수와 메서드의 유일한 인수일 경우 함수를 호출할 때 함수 또는 메서드 이름 뒤에 소괄호 ()를 작성하지 않아도 됨.
let reversedNames = names.sorted {$0 > $1}
후행 클로저는 클로저가 길어서 한 줄로 인라인으로 작성이 불가능할 때 유용함!!
예를 들어 Swift의 Array타입은 단일 인수로 클로저 표현식을 가지는 map(_:) 메서드가 있음. 이 클로저는 배열의 각 아이템에 대해 한번 호출되고 아이템에 대해 매핑된 대체값 (다른 타입일 수 있음)이 반환됨. map(_:)에 전달한 클로저에 작성된 코드에 따라 매핑 특성과 반환된 값의 타입을 지정함.
제공된 클로저엑 각 배열의 요소를 적용한 후에 map(_ : ) 메서드는 기존 배열에 해당값과 같은 순서로 새로 매핑된 값의 새로운 배열을 반환함.
다음은 Int 값의 배열을 String값의 배열로 변환하기 위해 후행 클로저와 map(_: ) 메서드를 사용해 보자!!!
배열 [16, 58, 510] -> ["OneSix", "FiveEight", "FiveOneZero"]로 변경해 볼 거임!
import Foundation
let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four", 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
let strings = numbers.map { (number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
print(strings)
// ["OneSix", "FiveEight", "FiveOneZero"]
map(_: ) 메서드는 배열에 각 아이템을 위해 클로저 표현식을 호출함. 매핑할 배열의 값에서 유추할 수 있으므로 클로저의 입력 파라미터 인 number타입을 지정할 필요가 없음.
위 예시에서 변수 number는 클로저의 number 파라미터의 값으로 초기화되기 때문에 값은 클로저 본문 내에서 수정될 수 있음 (함수와 클로저의 파라미터는 항상 상수). 클로저 표현식은 출력 매핑된 출력 배열에 저장될 타입을 나타내기 위해 String 타입도 반환 타입으로 지정.
클로저 표현식은 호출될 때마다 output이라는 문자열을 만듦. 나머지 연산자 (number % 10)를 이용하여 number의 마지막 숫자를 계산하고 digitNames 딕셔너리에 적절한 숫자 문자열을 찾음. 클로저는 0보다 큰 정수에 대한 문자열 표현을 생성하는 데 사용할 수 있음.
딕셔너리 서브 스크립트는 키가 존재하지 않는 경우에 값을 찾는 것을 실패하기 위해 옵셔널 값을 반환함. 그래서 digitNames
딕셔너리의 서브 스크립트를 호출할 때는 느낌표를 붙여 줌 {!). 위의 예제에서 digitNames딕셔너리에 number % 10 은 항상 유효한 서브 스크립트 키를 보장하므로 느낌표는 서브 스크립트의 옵셔널 반환 값에 저장된 String값을 강제로 원래핑 하는 데 사용됨.
digitNames 딕셔너리에서 반환된 문자열이 output 앞에 추가되어 숫자의 문자열 버전을 역순으로 효과적으로 빌드함 (표현식 number % 10 은 16의 경우 6, 58의 경우 8, 510의 경우 0을 제공합니다).
그러면 number 변수는 10으로 나누어 짐. 이것은 정수이기 때문에 나누면 버림이 되고 16 은 1, 58 은 5, 510 은 51 이 됨.
이 프로세스는 number 가 0 이 될 때까지 반복하고 그때 output 문자열은 클로저로부터 반환되고 map(_:) 메서드로부터 출력 배열에 추가됨.
후행 클로저 구문을 사용하면 클로저가 원하는 함수 바로 뒤에 있는 클로저의 기능을 깔끔하게 캡슐화함. 전체 클로저를 map(_: ) 메서드의 바깥 소괄호로 감쌀 필요가 없음.
함수가 여러 개의 클로저를 가지고 있다면 첫 번째 후행 클로저의 인수 라벨을 생략하고 남은 후행 클로저의 라벨은 표기함. 예를 들어 사진 갤러리에서 사진 하나를 불러와보자~!
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
if let picture = download("photo.jpg", from: server) {
completion(picture)
} else {
onFailure()
}
}
하나의 사진을 불러오기 위해 이 함수를 호출할 때 2개의 클로저를 제공함. 첫 번째 클로저는 사진 다운로드 완료 후에 사진을 보여주기 위한 완료 처리임. 두 번째 클로저는 오류를 표시하는 오류 처리기임.
loadPicture(from: someServer) { picture in
someView.currentPicture = picture
} onFailure: {
print("Couldn't download the next picture.")
}
이 예제에서 loadPicture(ƒrom: completion:onFailure:) 함수는 네트워크 작업을 백그라운드로 전달하고 네트워크 작업이 완료되면 두 완료 처리기 중 하나를 호출.
이러한 방식으로 함수를 작성하면 두 상황을 모두 처리하는 하나의 클로저를 사용하는 대신 성공적인 다운로드 후 사용자 인터페이스를 업데이트하는 코드에서 네트워크 오류를 처리하는 코드를 명확하게 분리할 수 있음.
완료 핸들러 (Completion handlers)는 특히 여러 핸들러가 중첩되어 있으면 읽기 어려울 수 있음. 이것을 대체하기 위해서는
비동기 코드를 사용하면 된다고 함.!
🎯 캡처값 (Capturing Values)
클로저는 정의된 둘러써인 콘텍스트에서 상수와 변수를 캡처(capture)할 수 있음. 그러면 클로저는 상수와 변수를 정의한 원래 범위가 더 이상 존재하지 않더라도 본문 내에서 해당 상수와 변수의 값을 참조하고 수정할 수 있음.
Swift에서 값을 캡처할 수 있는 가장 간단한 클로저 형태는 다른 함수의 본문 내에 작성하는 중첩함수임. 중첩함수는 바깥 함수의 어떠한 인수도 캡처할 수 있고 바깥 함수 내에 정의된 상수와 변수를 캡처할 수도 있음.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
위는 incrementer라는 중첩 함수가 포함된 makeIncrementer라는 함수의 예임. 중첩된 incrementer() 함수는 둘러싸인 콘텍스트에 runningTotal과 amount 인 2개의 값을 캡처함. 이 값을 캡처한 후에 incrementer는 호출될 때마다 amount로 runningTotal을 증가시키는 클로저로 makeIncrementer에 의해 반환.
makeIncrementer의 반환 타입은 () -> Int임. 이것은 단순한 값이 아닌 함수를 반환한다는 의미임. 반환하는 함수에는 파라미터가 없으며 호호출될 때마다 Int값을 반환함.
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer() 함수는 파라미터가 없으며 함수 본문 내에 runningTotal과 amount를 참조하고 있음. 둘러싸인 함수에 runningTotal과 amount 대한 참조를 캡처하고 함수 내에서 사용함.
참조를 캡처하는 것은 makeIncrementer 호출이 종료될 때 runningTotal과 amount가 사라지지 않고 다음에 incrementer 함수가 호출될 때 runningTotal을 사용할 수 있음.
최적화로 Swift는 값이 클로저에 의해 변경되지 않고 클로저가 생성된 후 값이 변경되지 않는 경우 값의 복사본을 캡처하고 저장할 수 있음. Swift는 더 이상 필요하지 않을 때 변수를 처리하는 것과 관련된 모든 메모리 관리도 처리한다고 함.
다음은 makeIncrementer 동작에 대한 예시
let incrementByTen = makeIncrementer(forIncrement: 10)
예제에서는 호출될 때마다 runningToatal 변수에 10을 더하는 증가 한수를 참조하도록 incrementByten이라는 상수 설정.
호출될 때마다 runningTotal 변수에 10을 더하는 증가함수를 참조하도록 incrementByTen이라는 상수를 설정. 함수를 여러 번 호출하면 이 동작이 수행되는 동작을 보여줌.
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
두 번째 증가를 생성하면 새로운 분리된 runningTotal 변수에 참조 저장!
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
기존 증가 (incrementByTen)을 다시 호출하면 그것의 runningTotal 변수는 이어서 증가되고 incrementBySeven으로 캡처된 변수는 영향을 주지 않음!!
incrementByTen()
// returns a value of 40
🎯 클로저는 참조 타입 (Closures Are Reference Types)
위 예제에서 incrementByten은 상수이지만 이러한 상수가 참조하는 클로저는 캡처한 runningTotal 변수를 계속 증가시킬 수 있음. 이는 함수와 클로저가 참조 타입이기 때문임.
함수 또는 클로저를 상수 또는 변수에 할당할 때마다 실제로 해당 상수 또는 변수를 함수 또는 클로저에 대한 참조로 설정함. 위의 예에서 incrementByten은 클로저 자체에 내용이 아니라 상수를 가리키는 클로저의 선택임.
이것은 또한 서로 다른 2개의 상수 또는 변수에 클로저를 할당한다면 2개의 상수 또는 변수는 모두 같은 클로저를 참조한다는 의미임.
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50
incrementByTen()
// returns a value of 60
위 예제는 alsoIncrementByTen 호출은 incrementByTen 호출과 같음을 보여줌. 2개 모두 같은 클로저를 참조하기 때문에 둘 다 증가하고 같은 러닝 합계를 반환함.
🎯 탈출 클로저 (Escaping Closures)
함수에 인수로 클로저를 전달하지만 함수가 반환된 후 호출되는 클로저를 함수를 탈출 (escape)하다고 말함. 클로저를 파라미터로 갖는 함수를 선언할 때 이 클로저는 탈출을 허락한다는 의미로 파라미터의 타입 전에 @escaping을 작성할 수 있음.
클로저가 탈출할 수 있는 한 가지 방법은 함수 바깥에 정의된 변수에 저장되는 것임. 예를 들어 비동기적 작업을 시작하는 대부분의 함수는 완료 핸들러로 클로저를 사용함. 이 함수는 작업을 시작한 후에 반환되지만 작업이 완료될 때까지 클로저가 호출되지 않음. 클로저는 나중에 호출하려면 탈출해야 함. 예를 들어
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:) 함수는 인수로 클로저를 가지고 있고 함수 바깥에 선언된 배열에 추가함. 함수의 파라미터에 @escaping을 표시하지 않으면 컴파일 시 에러가 발생함!!!
self를 참조하는 이스케이프 클로저는 self 가 클래스의 인스턴스를 참조하는 경우 특별한 고려가 필요함. 이스케이프 클로저에 self 캡처는 강한 참조 사이클이 생기기 쉬움.
일반적으로 클로저는 클로저 내부에서 변수를 사용하여 암시적으로 변수를 캡처하지만 이 경우에는 명시적이어야 함. self를 캡처하려면 사용할 때 명시적으로 self를 작성하거나 클로저의 캡처 리스트에 self를 포함. self를 명시적으로 작성하는데 의도를 표현하고 참조 사이클이 없음을 확인하도록 상기시켜 줌. 예를 들어 아래 코드에서 someFunctionWithEscapingClosure(_:)에 전달된 클로저는 명시적으로 self를 참조함. 반대로 someFunctionWithNonescapingClosure(_:)에 전달된 클로저는 비이스케이프 클로저임. 즉 암시적으로 self를 참조할 수 있음.
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"
다음은 클로저의 캡처 리스트에 포함하여 self를 캡처하고 암시적으로 self를 참조하는 doSomething() 임.
class SomeOtherClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { [self] in x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
self 가 구조체 또는 열거형 인스턴스이면 항상 암시적으로 self를 참조할 수 있음. 그러나 이스케이프 클로저는 구조체 또는 열거형 인스턴스이면 self에 대한 변경 가능한 참조를 캡처할 수 없음.
struct SomeStruct {
var x = 10
mutating func doSomething() {
someFunctionWithNonescapingClosure { x = 200 } // Ok
someFunctionWithEscapingClosure { x = 100 } // Error
}
}
위의 예제에서 someFunctionWithEscapingClosure 함수 호출은 변경 가능한 메서드 내부에 있기 때문에 에러이고 self는 변경 가능함. 이것은 이스케이프 클로저는 구조체인 self를 변경가능한 참조로 캡처할 수 없다는 규칙을 위반함.
🎯 자동 클로저 (Autoclosures)
자동 클로저 (autoclosure)는 함수에 인수로 전달되는 표현식을 래핑 하기 위해 자동으로 생성되는 클로저임. 인수를 가지지 않으며 호출될 때 내부에 래핑 된 표현식의 값을 반환함. 이러한 구문상의 편의를 통해 명시적 클로저 대신에 일반 표현식을 작성하여 함수의 파라미터 주위의 중괄호를 생략할 수 있음. 자동 클로저를 가지는 함수를 호출하는 것은 일반적이지만 이러한 함수를 구현하는 것은 일반적이지 않음. 예를 들어 assert(condition:message:file:line:) 함수는 condition과 message 파라미터에 대한 자동 클로저를 가짐. condition 파라미터는 오직 디버그 빌드인지 판단하고 message 파라미터는 condition 이 false 인지만 판단됨. 클로저가 호출될 때까지 코드 내부 실행이 되지 않기 때문에 자동 클로저는 판단을 지연시킬 수 있음. 판단 지연은 코드 판단 시기를 제어할 수 있기 때문에 사이드 이펙트가 있거나 계산이 오래 걸리는 코드에 유용함. 아래 코드는 클로저가 어떻게 판단을 지연하는지 보여줌.
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"
클로저 내부의 코드에 의해 customersInLine 배열의 첫 번째 요소는 삭제되지만 클로저가 실제로 호출되기 전까지 삭제되지 않음. 클로저가 호출되지 않으면 클로저 내부의 표현식은 판단되지 않음. 이것은 배열의 요소가 삭제되지 않는다는 의미임. customerProvider 타입은 String 이 아니고 파라미터가 없고 문자열을 반환하는 () -> String 임. 함수의 인수로 클로저를 전달하면 위와 같은 지연 판단과 동일한 동작을 가질 수 있음.
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"
serve(customer:) 함수는 소비자의 이름을 반환하는 명시적 클로저를 가짐. 아래 serve(customer:)의 버전은 같은 동작을 수행하지만 명시적 클로저를 가지는 대신에 파라미터 타입에 @autoclosure 속성을 표기하여 자동 클로저를 가짐. 이제 클로저 대신 String 인수를 받는 것처럼 함수를 호출할 수 있음. customerProvider 파라미터의 타입은 @autoclosure 속성으로 표시되므로 인수는 자동으로 클로저로 변환됨.
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"
자동 클로저 남용은 코드 이해를 어렵게 만들 수 있음. 콘텍스트와 함수 이름은 판단이 연기되고 있음을 분명히 해야 함.
자동 클로저가 이스케이프를 허용하길 원한다면 @autoclosure와@escaping 속성을 둘 다 사용하면 됨.
// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"
위의 코드에서 customerProvider 인수로 전달된 클로저를 호출하는 대신에 collectCustomerProviders(_:) 함수는 클로저를 customerProviders 배열에 추가함. 이 배열은 함수의 범위 밖에 선언됨. 이것은 배열에 클로저는 함수가 반환된 후에 실행될 수 있다는 의미임. 그 결과 customerProvider 인수의 값은 함수의 범위를 벗어날 수 있어야 함.
애플 공식문서를 보면서 클로저에 대해서 깊게 알아보았음!!
강한 참고 사이클이라는 걸 처음 알게 되었는 자세히 알아봐야 할 거 같음.
참고 및 인용
https://bbiguduk.gitbook.io/swift/language-guide-1/closures#trailing-closures
클로저 (Closures) | Swift
명명된 함수 생성없이 실행되는 코드 그룹입니다. 클로저 (Closures) 는 코드에서 주변에 전달과 사용할 수 있는 자체 포함된 기능 블럭입니다. Swift의 클로저는 다른 프로그래밍 언어에서 클로저,
bbiguduk.gitbook.io
'Swift' 카테고리의 다른 글
| [Swift] Unit Test는 무엇인가?? (3) | 2024.05.15 |
|---|---|
| [Swift] replacingOccurrences(of:with:) (2) | 2024.05.12 |
| [Swift] 옵셔널(Optional)[4] nil 병합 연산자(??) (Nil-Coalescing Operation) (0) | 2024.03.28 |
| [Swift] 옵셔널(Optional)[3] 옵셔널 체이닝(Optional Chaining), 암시적 언래핑 옵셔널(Implicitly Unwrapped Optionals) (1) | 2024.03.27 |
| [Swift] 옵셔널(Optional)[2] 강제 추출(Forced Unwrapping), 옵셔널 바인딩(Optional Binding) (2) | 2024.03.26 |