[Swift] Closures(1)

상 원·2022년 7월 7일
0

Swift

목록 보기
11/31
post-thumbnail

수업 때 가장 어려웠던 클로저!
코드 안에서 사용되고 전달될 수 있는 코드 블럭이다.
클로저는 어떤 변수나 상수의 값을 캡쳐해 저장할 수 있다고 하는데, 무슨 말인지 잘 이해가 안된다.
밑으로 더 내려가보자.

클로저는 3가지 형태를 가질 수 있는데,

  • 전역 함수(global function)는 이름이 있지만 어떤 값도 캡쳐하지 않는 클로저
  • 중첩 함수(nested function)는 이름이 있고 그것을 포함한 함수로부터 어떤 값을 캡쳐할 수 있는 클로저
  • Closure expression은 이름이 없고, 경량화된 표현으로 쓰여지고 주변 문맥으로부터 값을 캡쳐할 수 있는 클로저

Swift에서는 이 클로저가 코드를 clean, clear style, brief, clutter-free syntax로 만들어 준다고 한다.
여러 가지 장점들이 있는데,

  • 문맥에서 매개변수 타입과 반환값 타입의 추론
  • 단일 표현 클로저에서 암시적 변환
  • 축약된 인자 이름
  • 후위 클로저 문법

Closure Expressions

중첩 함수는 그보다 상위 함수의 일부로 독립적인 코드 블록을 명명하고 정의하는 편리한 수단이다.
그러나 그보다 짧게 쓰는 함수가 때로는 더 효과적이고 필요할 때가 있음. 이때 클로저를 사용하면 좋다.

Closure Expression 은 클로저를 의도와 명확성을 떨어트리지 않고 더 짧게 쓸 수 있는 방법들을 제공한다.

Closure Expression Syntax

클로저를 기본적으로 선언하는 방법은 다음과 같음.

클로저 안의 매개변수는 in-out 변수가 될 수 있지만, 초기값은 가질 수가 없다.

배열을 sorted로 정렬할 때, by: 인자로 정렬 방법을 전달할 수 있다.

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

이걸 클로저를 활용해 간단하게 만들어주면 다음과 같이 쓸 수 있다.

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

sorted 안의 by에 클로저를 전달함으로써 names 배열을 오름차순 정렬한다.

문맥으로부터 타입 추론하기

지금 만들어둔 클로저가 메소드의 인자로 들어가기 때문에 Swift는 이 클로저가 가지는 매개변수와 반환하는 값의 타입을 추론할 수가 있다.

위에서 sorted(by:) 의 인자로 클로저를 전달했는데, 이 메소드는 (String, String) -> Bool 타입의 인자를 받아야 하는 것을 알고 있다.
따라서 이 클로저에서 타입을 생략할 수가 있다.

타입을 다 생략할 수가 있기 때문에, 매개변수를 감싸는 괄호와 반환값을 표현하는 화살표( -> )도 마찬가지로 생략할 수 있다.

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

이런 식으로 쓸 수 있다.

이렇게 함수나 메소드에 인라인 클로저 형태로 클로저를 전달하면 항상 매개변수 타입과 반환값 타입을 생략하는 것이 가능하다.
그니까 함수에 인자로 inline closure을 넣을 때는 타입을 안 써줘도 된다는 거임!! 개편함

근데 코드 읽는 사람이 헷갈리지 않게 하려면 타입을 써 주는 것도 장려되긴 함.
위의 sorted에서도 이게 String타입을 정렬하는 것이라고 독자에게 말해주는 것이 더 안전할 수도 있다.

Implicit Returns from Single-Expression Closures

클로저 안 문장이 한 개라면 return을 생략해도 상관없다.

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

Shorthand Argument Names

Swift는 shorthand argument를 제공하는데, $0, $1, $2, .... 이런식임.
이걸 사용하면 클로저의 인자 정의를 없앨 수 있다.

shorthand argument의 타입은 함수 타입에서 추론이 가능하고, 가장 큰 shorthand argument의 번호가 사용하는 매개변수의 갯수를 의미한다.
in 키워드도 없앨 수 있으므로, 다음과 같이 표시할 수 있음!

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

Operator Methods

여기서 더 짧게 쓸 수도 있음!!
Swift에서 String을 비교할 때 > 로 비교하는데, 요 연산자는 Bool을 반환한다.
sorted(by:) 가 원하는 반환값 타입과 동일하므로!이거만 전달해줘도 Swift가 알아서
"아~문자열 연산을 사용하고 싶구나~"를 추론하게 되는것

reversedNames = names.sorted(by: >)

이렇게 된다.

사실 이렇게까지 줄일 필요가 있나 싶기도 하고, 어렵기도 하고..
shorthand argument까지는 이해하겠는데 operator method를 사용하는 건 좀 힘들 것 같다.

Trailing Closures(후위 클로저)

함수의 마지막 인자로 클로저를 넣을 때 Closure Expression이 좀 길다면, 이 Trailing Closure를 사용하면 좋다.
함수 호출 괄호 뒤에 클로저를 쓸 수 있음!
만약 이런 함수가 있다고 치자.

func someFunc(closure: () -> Void) {
    // 함수 실행문 어쩌구 저쩌구
}

이 함수를 후위 클로저 없이 사용하려면

someFunctionThatTakesAClosure(closure: {
    // 클로저 실행문
})

후위 클로저를 사용하면?

someFunctionThatTakesAClosure() {
    // 클로저 실행문
}

요런 식으로 간단하게 바뀐다.

위에서 했던 걸 이런 식으로 바꾸면

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

이렇게 바뀐다.

근데 만약에! 클로저가 함수의 유일한 인자라면?
소괄호도 없애도 된다!!!

reversedNames = names.sorted { $0 > $1 }

map(_:) 를 사용할 때, 이놈은 클로저 하나만 전달받는다. 그래서 후위 클로저를 사용하면 좋음!
이 클로저는 배열의 원소 하나마다 호출되고, mapping된 새로운 값을 뱉어준다.

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

이런 식의 코드로 변환이 된다.
digitNames[number % 10]! 에서 느낌표가 달린 이유는?
딕셔너리의 subscript([ ] 처럼 인덱스같은거) 는 딕셔너리 검색이 실패할 수도 있기 때문에 옵셔널 타입이기 때문이다!
위에서는 항상 값이 있을 것이 명확하기 때문에 강제 언래핑으로 값을 벗겨낸 것.

만약 함수 인자가 여러 개의 클로저를 가진다면?
첫 번째 클로저의 이름을 생략할 수 있고, 나머지 클로저들은 그대로 쓰면 된다.

// 이런 함수가 있다면, 
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

// completion을 생략할 수 있고, 그 후 onFailure을 그대로 쓰면 된다.
loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}
profile
ios developer

0개의 댓글