본문은 야곰 아카데미 커리어 스타터 캠프를 통해 학습한 내용을 회고한 글입니다.
Decimal
Decimal과 Double은 다음과 같은 차이를 가진다:
Decimal | Double |
|
|
- 프로젝트를 진행하면서 값이 정확히 나오지 않는 경우들이 발생하였다. 이런 경우에는 Double보다 Decimal 타입으로 사용해주면 보다 정확한 값을 가질 수 있다. (머.. 우리 계산기의 경우에는 애초에 Double로 연산하게 코드를 짜긴 했다만..ㅎㅎ)
클로저 Closures
지난 이야기
https://c-git-erg-sum.tistory.com/39
[TIL] 20230112: Closures
본문은 야곰 아카데미 커리어 스타터 캠프를 통해 학습한 내용을 회고한 글입니다. 클로저 Closures 클로저는 코드 안에서 전달되고 사용될 수 있는 함수의 독립적인 블록이다. 스위프트에서의 클
c-git-erg-sum.tistory.com
후행 클로저 Trailing 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 섹션에서의 String 정렬 클로저는 sorted(by:) 메서드의 괄호 밖에서 후행 클로저로서 쓰일 수 있다:
reversedNames = names.sorted() { $0 > $1 }
클로저 표현이 함수나 메서드의 유일한 인자로서 제공되고 그것을 후행 클로저로 제공하고 있다면, ()와 같은 괄호를 쓸 필요 없다:
reversedNames = names.sorted { $0 > $1 }
후행 클로저는 클로저가 굉장히 길어서 한 줄에 쓰기 어려울 때 유용하다. 예를 들어서 swift의 Array 유형에는 단일 인자로 표현식을 사용하는 map(_:)이라는 메서드가 있다. 이 클로저는 각각의 항목에 대해 한 번 호출되며, 해당 항목에 대해 대체로 매핑된 값을을 반환한다. 매핑과 반환된 값의 타입의 특성을 map(_:)에 전달하는 클로저에 코드를 작성해서 지정한다.
제공된 클로저를 각각의 Array element에 적용한 후, map(_:) 메서드는 원래 배열의 해당 값과 동일한 순서로 모든 새롭게 매핑된 값을 포함하는 새로운 array를 반환한다.
다음의 예시는 어떻게 후행 클로저가 있는 map(_:) 메서드가 Int 값으로 이루어진 Array를 String 값으로 이루어진 Array로 변환하는지를 보여준다. [16, 58, 510]이라는 Array는 ["OneSix", "FiveEight", "FiveOneZero"]라는 Array를 만들기 위해 사용된다:
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]
위의 코드는 정수 숫자와 영어 버전 간의 매핑 Dictionary를 만든다. 그리고 문자열로 변환할 준비가 된 정수 배열을 정의한다.
이제 클로저 표현을 숫자 Array의 map(_:) 메서드에 후행 클로저로서 전달하여 String 값으로 이루어진 Array를 만들 수 있다.
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"]
map(_:) 메서드는 Array의 각 항목에 대해 클로저 표현을 호출한다. 매핑할 Array의 값에서 타입을 유추할 수 있기 때문에, 클로저의 입력 매개변수, 숫자 타입을 지정할 필요가 없다.
이 예제에서, number 변수는 클로저의 숫자 매개변수로 초기화된다. 그래서 클로저 본문 내에서 값이 수정될 수 있다. (함수와 클로저의 매개변수는 언제나 상수이다.) 클로저 표현은 매핑된 출력 Array안에 저장될 타입을 명시하기 위해 String 타입을 반환할 것을 지정한다.
이 클로저 표현은 호출 될 때마다 호출되는 String을 만든다. 이것은 나머지 연산자 (number % 10)을 사용해서 마지막 숫자를 계산하고 digitNames Dictionary에서 적절한 String을 찾는다. 이 클로저는 0보다 큰 정수를 대신할 String을 만드는 데에 사용될 수 있다.
NOTE
digitNames Dictionary의 서브스크립트의 호출 뒤에 느낌표(!)가 붙는다. Dictionary 서브스크립트가 만약 key가 존재하지 않으면 Dictionary 조회가 실패한다는 것을 나타내기 위하여 옵셔널한 값을 반환할 수 있기 때문이다. 위의 예제에서, digitNames Dictionary에 대해서 number % 10이 항상 유효한 서브스크립트 key를 가질 것을 보장한다. 그래서 서브스크립트의 옵셔널한 반환 값에 저장된 String 값을 강제 언래핑하기 위해서 느낌표가 사용된다.
digitNames Dictionary에서 얻은 String은 결과물의 앞에 더해져 String 버전의 숫자를 역순으로 작성한다. (number % 10이라는 표현은 16에 대해서는 6을, 58에 대해서는 8을, 510에 대해서는 0을 제공한다.)
그런 후 그 number 변수를 10으로 나눈다. 이것이 정수이기때문에, 나누기를 통해서 버림이 되고, 16은 1이 되고, 58은 5가 되고, 510은 51이 된다.
이 과정은 number가 0이 될 때까지 반복하는데, 0이 되면 결과 String이 클로저에 의해서 반환되며 map(_:) 메서드에 의해서 결과 Array에 더해진다. 위의 예제에서의 후행 클로저 문법의 사용은 클로저가 지원하는 함수 바로 뒤에서 클로저의 기능을 깔끔하게 캡슐화한다. 전체 클로저를 map(_:) 메서드의 외부 괄호 내에서 래핑할 필요가 없다.
만약 함수/메서드가 여러 클로저를 취하고 있다면, 첫번째 후행 클로저에 대해서 인자 레이블을 생략하고 나머지 후행 클로저에 인자를 단다. 예를 들어서, 아래의 함수는 사진 갤러리에 사진을 로드한다:
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
if let picture = download("photo.jpg", from: server) {
completion(picture)
} else {
onFailure()
}
}
사진을 로드하기 위해 이 함수를 호출할 때, 두개의 클로저를 제공한다. 첫 번재 클로저는 성공적인 다운로드 이후에 사진을 표시하는 completion handler이다. 두 번째 클로저는 사용자에게 오류를 표시하는 error handler이다.
loadPicture(from: someServer) { picture in
someView.currentPicture = picture
} onFailure: {
print("Couldn't download the next picture.")
}
이 예제에서, loadPicture(from:completion:onFailure:) 함수는 네트워크 작업을 백그라운드로 디스패치하고, 네트워크 작업이 완료되면 두 completion handlers 중 하나를 호출한다. 이 방법으로 함수를 작성하는 것은 두가지 상황을 다루는 클로저를 하나만 사용하는 대신, 성공적인 다운로드 이후 UI를 업데이트하는 코드로부터 네트워크 실패를 다룰 수 있는 코드를 명확하게 분리할 수 있게 한다.
NOTE
Completion handler는 중첩되는 경우 읽기 어려워질 수 있다. 다른 접근방식은 동시성에서 설명된 대로 비동기 코드를 사용하는 것이다.
값 캡쳐 Capturing Values
클로저는 클로저가 정의되는 맥락으로부터 상수와 변수의 값을 캡쳐할 수 있다. 그런 다음 클로저는 그 바디 안에서 상수와 변수의 값을 참조하거나 수정할 수 있다. 상수와 변수를 정의한 원래의 범위가 더이상 존재하지 않더라도 말이다.
Swift에서 값을 캡쳐할 수 있는 가장 간단한 형태의 클로저는 다른 함수의 바디 내에 작성된 중첩 함수이다. 중첩 함수는 외부 함수의 인자를 캡쳐할 수 있고, 외부 함수 내에 정의 된 상수나 변수도 캡쳐할 수 있다.
다음은 incrementer라는 중첩 함수를 포함하는 makeIncrementer라고 불리는 함수의 예제이다. 중첩된 incrementer() 함수는 runningTotal과 amount라는 두개의 값을 캡쳐한다. 이 값들을 캡쳐하고 나면, incrementer는 makeIncrementer에 의해서 runningTotal을 amount가 호출될 때마다 증가시키는 클로저로 반환된다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
makeIncrementer의 반환 타입은 () -> Int이다. 단순한 값이아닌, 함수를 반환한다는 것을 의미한다. 이 반환된 함수는 매개변수가 없고, 이 함수/메서드가 호출될 때마다 Int 값을 반환한다. 어떻게 함수가 다른 함수를 반환하는지에 알아보려면, Function Types as Return Types를 참고하자.
makeIncrementer(forIncrement:) 함수는 현재의 증가의 누계를 저장하기 위해서, runningTotal이라는 정수 변수를 정의한다. 이 변수는 일단 0으로 초기화 된다.
makeIncrementer(forIncrement:) 함수는 forIncrement라는 단일한 인자 이름과 amount라는 매개변수를 가진다. 이 매개변수에 전달되는 인자 값은 반환되는 incrementer함수가 호출될 때마다 얼마나 많은 runningTotal이 증가 되어야하는지를 명시해준다. makeIncrementer 함수는 실제로 증가를 수행하는 Incrementer라는 중첩 함수를 정의한다. 이 함수는 단순히 runningTotal에 덧셈을 해주고, 결과를 반환해준다.
이를 따로따로 고려하면, 중첩된 incrementer()함수가 비정상적으로 보일 수 있다:
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer()함수는 매개변수가 없지만, 함수의 바디 안에서 runningTotal과 amount를 참조한다. 이것은 주변함수로부터 runningTotal과 amount를 참조하고 자신의 함수 바디에서 사용함으로서 작용한다. 참조를 통한 캡쳐는 runningTotal과 amount가 makeIncrementer로의 호출이 끝날 때도 사라지지 않는다는 걸 보장한다. 그리고 runningTotal이 다음 incrementer 함수가 호출 될 때에도 사용가능하다는 것을 보장한다.
NOTE
최적화로서, 만약 값이 클로저에 의해 변하지 않고 클로저가 생성된 후 값이 변경되지 않는다면, Swift는 값의 사본을 캡쳐하고 저장할 수도 있다.
또한 Swift는 더이상 필요하지 않은 변수 폐기와 관련된 모든 메모리 관리를 처리한다.
makeIncrementer의 동작 예시이다:
let incrementByTen = makeIncrementer(forIncrement: 10)
이 예제는 호출될 때마다 runningTotal 변수에 10을 더하는 incrementer 함수를 참조하기 위해서 incrementByTen이라는 상수를 설정한다. 그 함수를 여러번 호출하는 것은 다음과 같은 동작을 한다:
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
만약 두 번째 incrementer를 생성한다면, 새로운 runningTotal 변수에 참조를 저장하게 될 것이다:
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
원래의 incrementer(incrementByTen)을 다시 호출하는 것은 runningTotal 변수를 계속해서 증가시키고, incrementBySeven에 의해 캡쳐된 변수에는 영향을 미치지 않는다:incrementByTen()
// returns a value of 40
NOTE
클래스 인스턴스의 프로퍼티에 클로저를 할당하고 클로저가 그 인스턴스나 그것의 요소를 참조하여서 캡쳐를 하는 경우, 클로저와 인스턴스 사이에 강한 참조 순환이 생성된다. Swift는 이런 강한 참조 사이클을 끊기 위해서 캡쳐 목록을 사용한다.
참조
https://docs.swift.org/swift-book/LanguageGuide/Closures.html
Closures — The Swift Programming Language (Swift 5.7)
Closures Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages. Closures can capture and store referen
docs.swift.org
'YAGOM CAREER STARTER' 카테고리의 다른 글
[TIL] 20230223: TableView 코드로 구현하기 (0) | 2023.02.24 |
---|---|
[TIL] 20230220: JSON (0) | 2023.02.21 |
[TIL] 20230206: 메모리, ARC (3) | 2023.02.07 |
[토요스터디A반] 20230204: 고차함수 (0) | 2023.02.06 |
[TIL] 20230131: - (0) | 2023.02.06 |