[Swift] 7. Closures

도윤·2021년 7월 15일
0

Swift

목록 보기
7/21

Closure Expressions

함수나 메서드가 하나 이상의 argument를 가질 때 이름과 선언없이 함수같은 역할을 하는 짧게 쓸 수 있다.

The Sorted Method

swift library가 제공하는 sorted(by:) 메소드는 사용자가 정의한 closure sorting을 기준으로 배열을 정렬해준다.

array.sorted(by:)를 실행하게 된다면 자체적으로 배열이 수정되는 것이 아니라 사용하려면 할당을 새로 해줘야 한다.

let names = ["Chirs","Alex","Ewa","Barry","Daniella"]

sorted(by:) 메서드는 배열의 내용을 같은 타입의 두 인자를 closure가 받아들이고, 오름차순인지 내림차순인지 결정하는 Bool 값을 받는다.

func backward(_ s1: String, _s2:String) -> Bool {
	return s1>s2
}
var reversedNames = names.sorted(by:backward)

s1이 s2보다 크다면, backward(::)가 true를 반환하면서, 이것은 s1이 s2보다 앞에 나타나야함을 의미한다. String에서 크다는 의미는 사전순서로 나중에 나타난다는 의미이다.
예를 들어서 "B"는 "A" 보다 크고, "Tom"은 "Tim"보다 더 크다는 것이다.

하지만 a single-expression function(a>b)는 비효율적이기 때문에 closure을 사용하면 된다.

Closure Expression Syntax

{(paramters) -> return type in
	statements
}

closure 표현에서 parameters는 in-out으로 사용할 수 있지만, 그렇게 되면 default value는 사용 불가.
Variadic parameters도 사용가능하며 Tuple도 매개변수나 반환값에 사용이 가능하다.

위의 backward(::)의 closure버전을 보여주겠다.

reveredNames = names.sorted(by : {(s1:String,s2:String) -> Bool in 
	return s1 > s2
})

더 간단하게도 가능하다.

reveredNames = names.sorted(by: {(s1:String,s2:String) -> Bool in return s1>s2})

Inferring Type From Context

context로 부터 타입을 유추할 수 있어서 type을 쓰지 않아도 된다.

reversedNames = names.sorted(by:{s1,s2 in return s1 > s2})

names의 element는 String Type이므로 s1과 s2 또한 String type임을 알 수 있고 , s1>s2 의 Return type은 true or false인 Bool 타입임을 유추할 수 있다.

Implicit Returns form Single-Expression Closures

return 또한 생략가능하다.

reveredNames = names.sorted(by : s1, s2 in s1>s2)}

Shortand Argument Names

swift는 argument (s1,s2)를 간단하게 사용하는 방법을 제공한다.
인수 이름은 순서에 따라 $0,$1,$2와 같이 사용 가능.
즉 $0,$1과 같이 간편하게 사용할 수 있고, swift가 이를 자동으로 유추한다.

reveredNames = names.sorted(by:{$0>$1})

in 키워드 또한 생략을 할 수 있다. $0과 $1은 s1과 s2과 같은 의미이다.

Operating Methods

더 짧게도 축약이 가능하다. Swift의 String 비교 연산자(<,>)를 사용하여 비교 가능.

reveredNames = name.sorted(by:>)
reveredNames = name.sorted(by: { (s1:String,s2:String) -> Bool in
	return s1>s2
})

Trailing Closures

함수의 마지막 인수로 Closure을 사용할 때 너무 긴 경우 후행 closure로 사용이 가능하다
후행클로져는 함수 호출의 () 뒤 {}에 작성하면 된다.

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
}

위의 sorted를 구현한 것을 후행 closure로 나타낼 수 있다.

reveredNames = names.sorted() { $0 > $1}

만약 클로저 표현식이 함수,메서드의 유일한 매개변수일 때, 작성시 ()를 생략할 수 있다.
후행클로저는 길이가 길 때 사용하기 좋다.

예를 들어 Array 타입의 map(_:) 메서드는 하나의 클로저 표현식을 매개변수로 받는 메서드이다.
클로저는 Array의 항목을 한 번씩 호출하고 해당 항목에 대항 값들을 매핑값에 반환한다.
map에 전달되는 클로저는 매핑의 특성과 반환될 값들을 저장한다.

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]

numbers Array를 digitNames를 사용하여 String type으로 변환할 때

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
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

위 코드처럼 Array의 모든 element에 대해 closure을 수행한다.
매개변수 (number)는 swift가 자동으로 타입을 유추하고, digitNames[number%10]!은 Dictionary의 value는 Optional이므로 강제 추출을 했다.

후행 closure을 여러개 사용하고 싶다면, 첫번째 후행 closure의 인수는 생략하고, 그 후의 클로저들에는 인수 레이블을 지정하면 됩니다.

func loadPicture(from server : Server, completion:(Picture) -> Void,onFailure:() -> Void) {
	if let picture = download("photo.jpg",from : server) {
    	complection(picture)
    } else {
    	onFailure()
    }
}

첫번째 closure은 다운로드 성공 후 사진을 보여주는 completion handler이다.
두번째 closure은 유저에게 에러를 보여주는 부분이다.

loadPicture(from: someServer) { pictrue in
	someView.currentPicture = picture
} onFailure : {
    	print("Couldn't download the next picture")
}

loadPicture함수를 호출하게 되면 completion,onFailure 클로저가 사용.
첫번째 completion부분에는 인수를 생략해도 가능해서 completion이 생략되었지만 두번째 closure는 인수를 생략하면 안되기때문에 onFailure이 쓰였다.

Capturing Values

클로저는 상수나 변수를 캡쳐할 수 있다. 클로저가 주변 코드에서 상수와 변수를 캡쳐하고 그 뒤에 상수와 변수가 접근하게 되더라도 클로저는 해당 상수 및 변수의 값을 참조하고 수정할 수 있다.
swift에서 가장 쉽게 값을 캡처하는 방법은 중첩함수를 사용하는것이다.

func makeIncrementer(forIncrement amount:Int) -> () -> Int {
	var unningTotal = 0
    func incrementer() -> Int {
    	runningTotal += amount
        return runningTotal
    }
    return incrementer
}

incrementer()의 외부함수 makeIncrementer의 매개변수인 amount와 함수 내에서 정의된 runningTotal 변수를 캡쳐하여 사용. 실제로 incrementer함수에는 이러한 변수나 정의가 사용되어 있지 않지만 캡쳐하여 사용할 수 있다.

makeIncrementer의 변환 타입이 ()->Int인데 반환하는 값이 incrementer이므로 결국에는 incrementer의 반환 타입인 Int을 반환하게 된다.

let incrementByTem = makeIncrementer(forIncrement : 10)
incrementByTen()
incrementByTen()
incrementByTen()

increment 함수 호출마다 runningTotal의 값이 10씩 증가하고 이 값이 함수 makeincrementer이 호출된 이후에도 값이 사라지지 않고 계속 유지된다.

원서를 보고 작성했으나 이해가 되지않아서 나중에 추후에 더 디테일하게 작성하겠다.


Closure Are Reference Types

위의 코드에서 incrementByTen은 상수이지만, 계속해서 그 값이 변하는 것을 볼 수 있다.
함수나 클로저를 상수나 변수에 할당하게 되면 복사되는 것이 아닌 메모리 주소 참조를 하게 되기 때문에 가능하다.

let alsoIncrementByTen = incrementByTen
alsoIncerementByTen()
//returns a value of 40
incrementByTEn()
//returns a value of 50

Escaping Closures

클로저는 함수에 인수로써 클로저가 전달될때 클로저는 함수를 Escape한다고 말할 수 있다. 하지만 함수가 return한 후에 호출된다.

클로저를 매개 변수중 하나로 클로저를 사용할 때 매개변수 타입 앞에 @eacaping을 작성하여 클로저가 Escape 될 수 있음을 나타낼 수 있다. 클로저가 escape할 수 있는 한가지 방법은 함수 외부에 정의된 변수에 클로저를 저장하고, 이를 매개변수로 이용하는 것이다.

예를 들어 비동기작업을 시작하는 많은 함수는 완료 핸들러로 클로저를 사용한다. 함수 작업을 시작한 후 반환되지만 작업이 완료될 때 까지 클로저가 호출되지 않는다. 클로저는 Escape해야 나중에 호출할 수 있다.

var completionHandlers = [() -> Void]()
function someFunctionWithEscapingClosure(completionHanlder : @escaping() -> Void{
	completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_ :) 함수는 매개변수로 클로저를 받고 완료된 작업을 외부에선 Array로 선언하는 함수이다. 만약 @escaping을 작성하지 않는다면 컴파일 에러가 발생한다.

self를 참조하는 Escaping 클로저는 더 신중하게 사용되어야 한다.
Escape 클로저에 self를 캡쳐하면 실수로 강력한 참조를 만들 수 있다. 강력 참조를 만들게되면 Swift의 AFC로 인해 메모리 낭비가 발생할 수 있다.

  • ARC(Auto Reference Counting) : 간략히 말하면 메모리를 자동으로 관리해주고 메모리 참조 횟수를 count하여 0이 되면 메모리 해제를 한다.
var completionHandler = [() -> Void]()
func someFunctionWithEscapingClosure(completionHanlder: @escaping() -> Void) {
	completionHandlers.append(completionHandler)
}
func someFunctionNonescapingClosure(closure:() -> Void){
	closure
}
class SomeClass {
	var x = 10
    func doSometing() {
    	someFunctionWithEscapingClosure{self.x = 100}
        someFunctionWithNonescapingClosure{x = 200}
    }
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandler.first?()
print(instance.x)
// Prins "100"

someFunctionWithEscapingClosure함수에 전달된 클로저는 명시적으로 self를 나타냈는데, 반대로 밑의 함수는 excape 클로저가 아니기때문에 암시적으로 self를 참조할 수 있다.

class SomeOtherClass {
	var x = 10
    func doSometing() {
    	someFunctionWithEscapingClosure{ [self] in x = 100}
        someFunctionWithNonescapingClosure{ x = 200}
    }
}

위의 코드는 클로저의 캡쳐 목록에 포함해서 self를 캡쳐하고 암시적으로 self를 참조하는 방식이다.
self가 구조체,열거형 인스턴스인 경우 언제든지 self를 참조할 수 있다. 하지만 Excape 클로저는 이러한 가변적인 것에는 캡쳐할 수 없다.

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

excape 클로저를 사용하지 않고 구조체의 인스턴스를 참조할 수 있다.

이 부분 또한 이해하기 너무 어려워서 좀 더 공부를 해야될 것 같다.


Autoclosures

함수에 인수로 전달되는 표현식을 자동으로 감싸기 위해 생성되는 클로저이다.
매개변수를 갖지 않으며 호출될 때 그 안에 있는 표현식의 값을 반환한다. 편의를 위해 AutoClosure를 사용할 때는 명시적인 클로저 대신 정규 표현식을 작성해서 함수의 매개변수를 감싸는 괄호를 생략할 수 있다.

Autoclosure를 수행하는 함수를 호출하는 것은 일반적이지만 함수를 구현하는 것은 일반적이진 않다.
예를 들어 assert(condition:message:file:line:) 함수는 condition,message 매개변수를 위해 오토 클로저를 사용합니다. condition 매개변수는 디버그 빌드에서만 사용되고, message 매개변수는 condition이 false인 경우에만 사용

Autoclosure를 사용하면 클로저를 호출할 때 까지 내부 코드가 실행되지 않기 때문에 사용이 지연된다.

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’

customProvider에서 클로저 내부코드로 at:0의 Element를 제거하지만 실제로는 갯수가 그대로이다.
클로저가 실제로 호출되지 않는 이상 수행되지 않는것이다. 실제로 호출하게 뒤면 그 이후 개수가 한개 줄었음을 알 수 있다.

func serve(customer customerProvider: @autoclosure() -> String {
	print("Now Serving \(customerProvider())!")
}

serve함수는 매개변수를 명시적으로 받고, Array의 항목을 하나 제거하면 제거된 값을 print로 출력하는 함수이다.

func serv(customer customerProvider : @autoclosure() -> String {
	print("Now serving \(customerProvider())!")
}
serve(customer:customersInLine.remove(at : 0))
//prints "Now serving Ewa!"

serve에 @autoclosure 키워드를 사용해 autoclosure을 사용한 것.
이렇게 하면 클로저를 호출할 때 까지 String타입을 사용하는 것처럼 함수 호출.
@autoclosure를 붙이게 되면 인수가 자동으로 클로저를 변환한다.

autoclosure과 escape 클로저를 함께 사용하고 싶다면 둘다 사용할 수 있다

// 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!"

위와 같이 collectCustomerProviders의 매개변수 customerProvider로 전달된 클로저를 호출하는 대신 collectCustomerProviders 함수는 클로저를 customerProviders Array에 추가합니다. Array는 collectCustomerProviders 밖에서 선언되었기 때문에 함수가 반환된 후 Array의 클로저가 실행될 수 있습니다. 즉 매개변수 customerProvider의 값이 함수의 범위를 벗어날 수 있어야 하는 것입니다.

처음 공부하고 무작정 해석하면서 쓰기만 해서 이해를 30%도 못한 것 같다.
계속 공부해서 보완해나가도록 하겠다.

0개의 댓글