-
Chapter 8: Collection Iteration With ClosuresRaywenderlich/Swift Apprentice 2022. 8. 29. 10:48
Version
Swift 5.5, iOS 15, Xcode 13
이전에(earlier), 함수(functions)에 대해 배웠다(learned). 그러나(but) Swift에는 코드를 재사용 가능한(reusable) 덩어리(chunks)로 분할(break up)하는 데 사용할 수 있는 또 다른 객체(object)인 클로저(closure)가 있다. 이는 컬렉션(collections)을 다룰 때(dealing with) 도움(instrumental)이 된다.
클로저(closure)는 단순히(simply) 이름 없는 함수(function)로, 변수(variable)에 할당(assign)하여 다른 값(value)처럼 전달(pass)할 수 있다. 이 장(chapter)은 클로저(closures)가 얼마나 편리(convenient)하고 가치가 있는지(valuable) 알려줄 것이다.
Closure basics
클로저(closures)는 클로저(closure)의 범위(scope) 내에서(within) 변수(variables)와 상수(constants)를 "닫을(close over)" 수 있기 때문에 그렇게 명명되었다. 이는 단순히(simply) 클로저(closure)가 주변(surrounding) 맥락(context)에서, 모든 변수(variable) 또는 상수(constant)의 값(values)에 접근(access)할 수 있음을 의미한다. 클로저(closure) 내에서 사용되는 변수(variables)와 상수(constants)는 클로저(closure)에 의해 캡처(captured)되었다고 한다.
클로저(closures)가 이름이 없는(without names) 함수(functions)라면 어떻게 사용해야 하는지 물을 수 있다. 클로저(closure)를 사용하려면 먼저 변수(variable)나 상수(constant)에 클로저(closure)를 할당(assign)해야 한다.
클로저(closure)를 담을(hold) 수 있는 변수(variable) 선언(declaration)은 다음과 같다:
var multiplyClosure: (Int, Int) -> Int
multiClosure
는 두 개의Int
값(values)을 사용하고,Int
를 반환(returns)한다. 이는 함수(function)에 대한 변수(variable) 선언(declaration)과 동일(same as)하다. 클로저(closure)는 단순히 이름 없는 함수(function)이고 클로저(closure)의 유형(type)은 함수(function) 유형(type)이기 때문이다.플레이그라운드(playground)에서 선언(declaration)을 컴파일(compile)하려면, 다음과 같은 초기(initial) 정의(definition)를 제공(provide)해야 한다:
var multiplyClosure = { (a: Int, b: Int) -> Int in return a * b }
이것은 함수(function) 선언(declaration)과 유사해(similar to) 보이지만, 미묘한(subtle) 차이(difference)가 있다. 동일한 매개 변수(parameter) 목록(list),
->
기호(symbol)와 반환(return) 유형(type)이 있지만, 클로저(closures)를 사용하면 이러한 요소(elements)가 중괄호(braces) 안(inside)에 표시(appear)되고 반환(return) 유형(type) 뒤에in
키워드(keyword)를 사용한다.클로저(closure) 변수(variable)를 정의(defined)하면, 다음과 같이 함수(function)처럼 사용할 수 있다:
let result = multiplyClosure(4, 2)
예상대로(as you’d expect),
result
는 8이다. 하지만, 다시 말하지만 미묘한(subtle) 차이(difference)가 있다.클로저(closure)에 매개변수(parameters)에 대한 외부 이름(external names)이 없다. 함수(functions)처럼 설정할 수 없다.
Shorthand syntax
클로저(closure) 구문(syntax)을 줄이는 방법은 여러 가지가 있다. 첫째(first), 일반(normal) 함수(functions)와 마찬가지로 클로저(closure)가 단일(single) 반환(return) 문(statement)으로 구성된(consists of) 경우 다음과 같이
return
키워드(keyword)를 생략(leave out)할 수 있다:multiplyClosure = { (a: Int, b: Int) -> Int in a * b }
다음으로(next), Swift의 유형 추론(type inference)을 사용해 유형(type) 정보(information)를 제거(removing)하여 구문(syntax)을 더 단축(shorten)할 수 있다:
multiplyClosure = { (a, b) in a * b }
이미(already)
multiClosure
를 두 개의Int
를 사용하고Int
를 반환(returning)하는 클로저(closure)로를 선언(declared)했으므로, Swift가 이러한 유형(types)을 추론(infer)하도록 할 수 있다.마지막으로(finally) 원하는 경우 매개변수(parameter) 목록(list)을 생략(omit)할 수도 있다. Swift를 사용하면 다음과 같이 0부터 시작하는 숫자로 각(each) 매개변수(parameter)를 참조(refer to)할 수 있다:
multiplyClosure = { $0 * $1 }
매개변수 목록(parameter list), 반환 유형(return type),
in
키워드(keyword)가 모두 사라졌고(gone), 새 클로저(closure) 선언(declaration)이 원본(original)보다 훨씬 짧다. 번호가 매겨진(numbered) 매개변수(parameters)는 위와 같이 클로저(closure)가 짧고(short) 간결할(sweet) 때만 사용해야 한다.매개변수 목록(parameter list)이 더 길어지면(longer) 번호가 매겨진(numbered) 각 매개변수(parameter)가 무엇을 참조(refers to)하는지 기억하기 어려울(confusing) 수 있다. 이러한 경우(cases)에는 명명된(named) 구문(syntax)을 사용해야 한다.
다음 코드를 고려해(consider) 본다:
func operateOnNumbers(_ a: Int, _ b: Int, operation: (Int, Int) -> Int) -> Int { let result = operation(a, b) print(result) return result }
Int
값(values)을 처음 두 매개변수(parameters)로 사용하는operatorOnNumbers
라는 함수(function)를 선언(declares)한다. 세 번째 매개변수(parameter)는operation
이라는 이름의 함수(function) 유형(type)이다.operationOnNumbers
자체(itself)는Int
를 반환(returns)한다.그런 다음, 다음과 같이 클로저(closure)와 함께
operaOnNumbers
를 사용할 수 있다:let addClosure = { (a: Int, b: Int) in a + b } operateOnNumbers(4, 2, operation: addClosure)
클로저(closures)는 단순히(simply) 이름 없는(without names) 함수(functions)라는 것을 기억(remember)해야 한다. 따라서 다음과 같이
operatorOnNumbers
의 세 번째 매개변수(parameter)로 함수(function)를 전달(pass)할 수도 있다는 사실에 놀라지(be surprised to) 않아야 한다.func addFunction(_ a: Int, _ b: Int) -> Int { a + b } operateOnNumbers(4, 2, operation: addFunction)
operationOnNumbers
는operation
이 함수(function)든 클로저(closure)이든(whether), 동일한 방식으로 호출(called)된다.클로저(closure) 구문(syntax)은 여기서 또 다시 유용(handy)하다. 다음과 같이
operaOnNumbers
함수(function) 호출(call)에 클로저(closure)를 인라인(inline)으로 정의(define)해 사용할 수 있다:operateOnNumbers(4, 2, operation: { (a: Int, b: Int) -> Int in return a + b })
클로저(closure)를 정의(define)하고 지역(local) 변수(variable)나 상수(constant)에 할당(assign)할 필요가 없다. 함수(function) 매개변수(parameter)로 전달(pass)하는 바로 그 위치에서 클로저(closure)를 선언(declare)하기만 하면 된다.
그러나 클로저(closure) 구문(syntax)을 단순화(simplify)하여, 많은 상용구 코드(boilerplate code)를 제거(remove)할 수 있다. 따라서(therefore) 위의 내용을 다음과 같이 줄일(reduce) 수 있다:
operateOnNumbers(4, 2, operation: { $0 + $1 })
한 단계 더 나아갈 수도 있다(you can even go a step further). + 연산자(operator)는 두 개의 인수(arguments)를 받아 하나의 결과(result)를 반환(returns)하는 함수(function)일 뿐이므로 아래와 같이 쓸(write) 수 있다:
operateOnNumbers(4, 2, operation: +)
구문(syntax)을 단순화(simplify)할 수 있는 방법이 하나 더 있지만, 클로저(closure)가 함수(function)에 전달되는 최종(final) 매개변수(parameter)일 때만 가능하다. 이 경우(case), 클로저를 함수(function) 호출(call) 외부로(outside of) 이동(move)할 수 있다:
operateOnNumbers(4, 2) { $0 + $1 }
이상하게(strange) 보일 수 있지만,
operation
레이블(label)을 제거(removed)하고 함수(function) 호출(call) 매개변수 목록(parameter list) 외부(outside)에서 중괄호(braces)를 가져온(pulled) 것을 제외(except)하고는 이전 코드 조각(code snippet)과 동일하다. 이를 후행 클로저 구문(trailing closure syntax) 이라고 한다.Multiple trailing closures syntax
함수(function) 입력(inputs)에 대한 다중(multiple) 클로저(closure)가 있는 경우, 특별한(special) 단축(shorthand) 방식으로 이를 호출(call)할 수 있다. 다음과 같은 함수(function)가 있다고 가정(suppose)한다:
func sequenced(first: ()->Void, second: ()->Void) { first() second() }
Swift를 사용하면 다음과 같이 호출(call)할 수 있다:
sequenced { print("Hello, ", terminator: "") } second: { print("world.") }
그러면, “Hello, world.” 가 출력(print out)될 것이다.
Note: 클로저(closure)로 함수(function)를 호출(call)하는 방법을 잊어버린 경우, Xcode가 도움이 될 수 있다. 메서드(method) 이름을 입력하고(또는 코드 완성(code complete)) 리턴(return) 키를 두 번(twice) 누른다(type). 코드 완성(code completion) 함수(function)은 후행 클로저(trailing closure) 구문(syntax)을 채워준다(fill out).
Closures with no return value
지금까지 본 모든 클로저는(closures) 하나 이상의 매개변수(parameters)를 가지고 값(values)을 반환(returned)했다. 그러나 함수(functions)와 마찬가지로 클로저(closures)도 이러한 것이 필수(required to)가 아니다. 매개변수(parameters)를 사용(takes)하지 않고 아무것(nothing)도 반환(returns)하지 않는 클로저(closure)를 선언(declare)하는 방법은 다음과 같다:
let voidClosure: () -> Void = { print("Swift Apprentice is awesome!") } voidClosure()
클로저(closure)의 유형(type)은
() -> Void
이다. 빈(empty) 괄호(parentheses)는 매개변수(parameters)가 없음을 나타낸다(denote). 반환(return) 유형(type)을 반드시 선언(declare)해야 하므로, Swift는 클로저(closure)를 선언(declaring)하고 있음을 알 수 있다. 여기서Void
가 유용(handy)하며, 그 이름에서 암시(suggests)하는 바와 정확히(exactly) 일치한다. 클로저(closure)는 아무 것도 반환(returns)하지 않는다.Note:
Void
는 사실(actually)()
의 별칭(typealias)일 뿐이다. 이는() -> Void
를() -> ()
로 작성(written)할 수 있음을 의미한다. 그러나 함수(function)의 매개변수(parameter) 목록(list)은 항상(always) 괄호(parentheses)로 묶어야(surrounded) 하므로,Void -> ()
또는Void -> Void
는 유효하지 않다(invalid).Capturing from the enclosing scope
마지막으로(finally), 클로저(closure)의 정의(defining) 특성(characteristic)으로 돌아단다. 클로저(closure)는 해당 범위(scope) 내의 변수(variables)와 상수(constants)에 접근(access)할 수 있다.
Note: 범위(scope)는 엔터티(entity, 변수(variable), 상수(constant) 등)에 접근(accessible)할 수 있는 범위(range)를 정의(defines)한다. 이전에 if 문(statements)과 함께 도입(introduced)된 새로운 범위(scope)를 학습했다. 클로저(closures) 또한 새로운 범위(scope)를 도입(introduce)하고, 그것이 정의(defined)된 범위(scope)에서 확인(visible)할 수 있는 모든 엔터티(entities)를 상속(inherit)한다.
예를 들어(for example), 다음 클로저(closure)를 확인해 본다:
var counter = 0 let incrementCounter = { counter += 1 }
incrementCounter
는 비교적(relatively) 간단(simple)하다.counter
변수를 증가(variable)시킨다.counter
변수(variable)는 클로저(closure) 외부(outside)에서 정의(defined)된다. 클로저(closure)가 변수(variable)와 동일한 범위(scope)에서 정의(defined)되어 있으므로, 클로저(closure)가 변수(variable)에 접근(access)할 수 있다. 클로저(closure)는counter
변수(variable)를 캡처(capture)한다고 한다. 변수(variable)에 대한 모든 변경 사항(changes)은 클로저(closure) 내부(inside)와 외부(outside) 모두에서 볼 수 있다.다음과 같이 클로저(closure)를 5번 호출(call)한다고 가정해 본다:
incrementCounter() incrementCounter() incrementCounter() incrementCounter() incrementCounter()
5번의 호출 후
counter
는 5가 된다.클로저(closures)가 둘러싸인(enclosings) 범위(cope)에서 변수(variables)를 캡처(capture)할 수 있다는 사실(fact)은 매우(extremely) 유용(useful)할 수 있다. 예를 들어(for example), 다음과 같은 함수(function)를 작성(write)할 수 있다:
func countingClosure() -> () -> Int { var counter = 0 let incrementCounter: () -> Int = { counter += 1 return counter } return incrementCounter }
이 함수(function)는 매개변수(parameters)를 사용하지 않고 클로저(closure)를 반환(returns)한다. 반환(returns)하는 클로저(closure)는 매개변수(parameters)를 사용하지 않고
Int
를 반환(returns)한다.이 함수(function)에서 반환(returned)된 클로저(closure)는 호출(called)될 때마다 내부(internal)
counter
를 증가(increment)시킨다. 이 함수(function)를 호출(call)할 때마다 다른(different)counter
를 얻게된다.예를 들어(for example), 다음과 같이 사용할 수 있다:
let counter1 = countingClosure() let counter2 = countingClosure() counter1() // 1 counter2() // 1 counter1() // 2 counter1() // 3 counter2() // 2
함수(function)에 의해 생성된(created) 두 카운터(counters)는 상호(mutually) 배타적(exclusive)이며 독립적(independently)으로 계산된다. 깔끔하다(neat).
Custom sorting with closures
컬렉션(collections)을 자세히 살펴보기 시작하면, 클로저(closures)가 유용(handy)하다. 7장(chapter), "Arrays, Dictionaries & Sets"에서는 배열(array)의
sort
메서드(method)를 사용하여 배열(array)을 정렬(sort)했다. 클로저(closure)를 지정(specifying)하여 사용자 정의(customize) 정렬 방식(sorted)을 정의(customize)할 수 있다. 정렬된(sorted) 배열(array)을 가져오려면, 다음과 같이sorted()
를 호출한다:let names = ["ZZZZZZ", "BB", "A", "CCCC", "EEEEE"] names.sorted() // ["A", "BB", "CCCC", "EEEEE", "ZZZZZZ"]
사용자 정의(custom) 클로저(closure)를 지정(specifying)하여, 배열(array)이 정렬(sorted)되는 방식에 대한 세부 정보(details)를 변경(change)할 수 있다. 다음과 같이 후행 클로저(trailing closure)를 지정(specify)한다:
names.sorted { $0.count > $1.count } // ["ZZZZZZ", "EEEEE", "CCCC", "BB", "A"]
이제 배열(array)은 문자열(string)의 길이에 따라 정렬(sorted)되며, 긴(longer) 문자열(strings)이 먼저 나오게 된다.
Iterating over collections with closures
Swift에서 컬렉션(collections)은 종종(often) 함수형 프로그래밍(functional programming)과 관련된(associated) 몇 가지 편리한(convenient) 기능(features)을 구현(implement)한다. 이러한 기능(features)은 컬렉션(collection)에 적용(apply to)할 수 있는 함수(functions) 형태(shape)로 제공된다.
여기에는 각(each) 요소(element)를 변환(transforming)하거나 특정 요소(certain elements)를 필터링(filtering)하는 등의 작업(operations)이 포함(include)된다.
이제 보게 되겠지만, 이 모든 함수(functions)는 클로저(closures)를 사용한다.
이 함수(functions) 중 첫 번째(first)를 사용하면, 컬렉션(collection)의 요소(elements)를 반복(loop over)하고 다음과 같은 작업(operation)을 수행(perform)할 수 있다:
let values = [1, 2, 3, 4, 5, 6] values.forEach { print("\($0): \($0*$0)") }
이는 컬렉션(collection)의 각 항목을 반복(loops)하여, 해당 값(value)과 그 제곱(square)을 출력(printing)한다.
또 다른 함수(function)를 사용하면, 다음과 같이 특정(certain) 요소(elements)를 필터링(filter out)할 수 있다:
var prices = [1.5, 10, 4.99, 2.30, 8.19] let largePrices = prices.filter { $0 > 5 }
여기에서는 상점 품목(items)의 가격(prices)을 나타내는(represent)
Double
배열(array)을 생성한다(create). $5보다 큰 가격을 필터링(filter out)하려면filter
함수(function)를 사용한다. 해당 함수(function)는 다음과 같다:func filter(_ isIncluded: (Element) -> Bool) -> [Element]
filter
는 단일(single) 매개변수(parameter), 즉Element
를 받아(takes)Bool
을 반환(returns)하는 클로저(closure, 또는 함수(function))를 사용한다. 그리고,filter
함수(function)는Element
배열(array)을 반환(returns)한다. 이러한 맥락에서(in this context),Element
는 배열(array)의 항목(items) 유형(type)을 나타낸다(refers to). 위의 예(example)에서는Double
이 된다.값(value)을 유지해야 하는지 여부에 따라,
true
또는false
를 반환(return)하는 것이 클로저(closure)의 역할(job)이다.filter
에서 반환(returned)된 배열(array)에는 클로저(closure)가true
로 반환(returned)한 모든 요소(elements)가 포함(contain)된다.예제(example)에서,
largePrices
에는 다음이 포함된다(contain):(10, 8.19)
Note:
filter
(및 이러한 모든 함수(functions))에서 반환(returned)된 배열(array)은 새로운(new) 배열(array)이다. 원본(original)은 전혀 수정(modified)되지 않는다.특정 조건(certain condition)을 만족(satisfies)하는 첫 번째(first) 요소(element)에만 관심이 있다면(interested in),
first(where:)
를 사용할 수 있다. 예를 들어(for example), 후행 클로저(trailing closure)를 사용하면 다음과 같다:let largePrice = prices.first { $0 > 5 }
이 경우(case),
largePrice
는 10이 된다.이외에도, 더 다양한 것들이 있다.
모든 품목(items)을 원래 가격(original price)의 90%로 할인(discount)한다고 상상해 본다. 이를 위한
map
이라는 편리한(handy) 함수(function)가 있다:let salePrices = prices.map { $0 * 0.9 }
map
함수(function)는 클로저(closure)로 배열(array)의 각(each) 항목(item)에 대해 실행(execute)하고, 순서(order)가 유지된(maintained) 각 결과(result)를 포함(containing)하는 새 배열(array)을 반환(return)한다. 이 경우(in this case),salePrices
에는 다음이 포함(contain)된다:[1.35, 9, 4.491, 2.07, 7.371]
map
함수(function)를 사용하여, 다음과 같이 유형(type)을 변경(change)할 수도 있다:let userInput = ["0", "11", "haha", "42"] let numbers1 = userInput.map { Int($0) }
이는 사용자가 입력(input)한 일부 문자열(strings)을
Int?
배열(array)로 바꾼다(turns).String
에서Int
로의 변환(conversion)이 실패(fail)할 수 있으므로 옵셔널(optional)이어야 한다.유효하지 않은(invalid, 누락된(missing)) 값(values)을 필터링(filter)하려면 다음과 같이
compactMap
을 사용할 수 있다:let numbers2 = userInput.compactMap { Int($0) }
Int
배열(array)을 생성(creates)하고 누락된(missing) 값(values)을 버린다(tosses out)는 점을 제외(except)하면,map
과 거의(almost) 동일하다.map
및compactMap
과 이름이 비슷(similar)하지만, 약간 다른(different) 작업을 하는flatMap
연산(operation)도 있다. 직접 살펴보면 다음과 같다:let userInputNested = [["0", "1"], ["a", "b", "c"], ["🐕"]] let allUserInput = userInputNested.flatMap { $0 }
allUserInput
이["0", "1", "a", "b", "c", "🐕"]
임을 알 수(notice) 있다.Swift는
flatMap
에 주어진 클로저(closure)의 반환(return) 값(value)이 컬렉션(collection) 자체(itself)가 될 것으로 예상(expects)한다. 그런 다음 이 모든 컬렉션(collections)을 함께 연결(concatenates)한다. 따라서 이 경우(in this case)에서는 내부(inner) 컬렉션(collections)을 해제(unwrapping)하는 작업(trick)이 수행됐다. 첫 번째 내부(inner) 컬렉션(collection)의 모든 항목(items)을 포함(containing)하는 컬렉션(collection) 뒤에, 두 번째 내부(inner) 컬렉션(collection)의 모든 항목(items)을 포함하는 식으로 계속 진행된다(and so on).또 다른 편리한(handy) 함수(function)는
reduce
이다. 이 함수(function)는 초기 값(initial value)과 배열(array)의 각(each) 요소(element)에 대해 호출(called)되는 클로저(closure)를 취한다(takes). 클로저(closure)가 호출(called)될 때마다, 현재 값(current value, 초기 값(initial value)으로 시작)과 배열(array) 요소(element) 두 가지 입력(inputs)을 받는다(gets). 클로저(closure)는 다음(next) 현재 값(current value)이 될 값을 반환(returns)한다. 이 과정(process)이 복잡하게(convoluted) 들릴 수 있지만, 예제(example)를 확인해 보면 명확(clear)하게 알 수 있다.예를 들어(for example), 다음과 같이
prices
배열(array)의 총계(total)를 계산(calculate)하는 데 사용할 수 있다:let sum = prices.reduce(0) { $0 + $1 }
총계(total)를 나타내는(representing) 초기 값(initial value)은 0이다. 클로저(closure)는 각(each) 요소(element)에 대해 호출(called)되고, 계산 중인 총계(running total)에 현재(current) 요소(element)를 더한(plus) 값을 반환(returns)한다. 반환(returned)된 값(value)은 총계(running total)이다. 최종(final) 결과(result)는 배열(array)에 있는 모든 값(values)의 총계(total)이다. 이 경우(in this case),
sum
은 다음과 같다:26.98
filter
,map
,reduce
를 확인했으므로, 클로저(closures) 구문(syntax) 덕분에 이러한 함수(functions)가 얼마나 강력(powerful)한지 알(realize) 수 있다. 몇 줄(lines)의 코드(code)만으로 컬렉션(collection)에서 매우(quite) 복잡한(complex) 값(values)을 계산(calculated)했다.이러한 함수(functions)는 딕셔너리(dictionaries)와 함께 사용(be used with)할 수도 있다. 가격(price)을 해당 항목(items)의 개수에 매핑(mapping)하는 딕셔너리(dictionary)로 상점(shop)의 재고(stock)를 나타낸다(represent)고 상상(imagine)해 본다. 이를 사용하여 재고(stock)의 총(total) 가치(value)를 다음과 같이 계산(calculate)할 수 있다:
let stock = [1.5: 5, 10: 2, 4.99: 20, 2.30: 5, 8.19: 30] let stockSum = stock.reduce(0) { $0 + $1.key * Double($1.value) }
reduce
함수(function)의 두 번째 매개변수(parameter)는 딕셔너리(dictionary) 요소(elements)의key
와value
이 명명되어 있는 튜플(tuple)이다. 계산(calculation)을 수행(perform)하려면, 해당 값(value)의 유형 변환(type conversion)이 필요하다.결과(result)는 다음과 같다:
384.5
reduce(into:_:)
라는 또 다른(another) 형태(form)의reduce
가 있다. 컬렉션(collection)을 축소(reducing)하는 결과(result)가 다음과 같이 배열(array)이나 사전(dictionary)인 경우 사용한다:let farmAnimals = ["🐎": 5, "🐄": 10, "🐑": 50, "🐶": 1] let allAnimals = farmAnimals.reduce(into: []) { (result, this: (key: String, value: Int)) in for _ in 0 ..< this.value { result.append(this.key) } }
클로저(closure)에서 무언가(something)를 반환(return)하지 않는다는 점을 제외(except)하면, 다른 버전(version)과 동일한 방식으로 작동(works)한다. 대신(instead), 각(each) 반복(iteration)은 변경할 수 있는(mutable) 값(value)을 제공(gives)한다. 이런 식으로(in this way), 이 예제(example)에서는 하나의 배열(array)만 생성(created)되고 추가(appended)되므로 경우(cases)에 따라
reduce(into:_:)
가 더 효율적(efficient)이다.배열(array)을 잘라야(chop up) 하는 경우, 도움(helpful)이 될 수 있는 몇 가지 함수(functions)가 더 있다. 첫 번째 함수(function)는 다음과 같이 작동하는
dropFirst
이다:let removeFirst = prices.dropFirst() let removeFirstTwo = prices.dropFirst(2)
dropFirst
함수(function)는 기본값(defaults)이 1인 단일(single) 매개변수(parameter)를 사용(takes)하고, 해당 숫자만큼의 요소(elements)가 앞쪽(front)에서 제거(removed)된 배열(array)을 반환(returns)한다. 결과(results)는 다음과 같다:removeFirst = [10, 4.99, 2.30, 8.19] removeFirstTwo = [4.99, 2.30, 8.19]
dropFirst
와 마찬가지로, 배열(array)의 끝(end)에서 요소(elements)를 제거(removes)하는dropLast
도 있다. 다음과 같이 사용한다:let removeLast = prices.dropLast() let removeLastTwo = prices.dropLast(2)
결과(results)는 예상(expect)한 대로이다:
removeLast = [1.5, 10, 4.99, 2.30] removeLastTwo = [1.5, 10, 4.99]
아래(below)와 같이 배열(array)의 첫 번째(first) 또는 마지막(last) 요소들(elements)만 선택(select)할 수도 있다:
let firstTwo = prices.prefix(2) let lastTwo = prices.suffix(2)
여기서(here)
prefix
는 배열(array) 앞쪽(front)에서 해당 개수(required number)의 요소(elements)를 반환(returns)하고,suffix
는 배열(array)의 뒤쪽(back)에서 해당 개수(required number)의 요소를 반환(returns)한다. 이 함수(function)의 결과(results)는 다음과 같다:firstTwo = [1.5, 10] lastTwo = [2.30, 8.19]
그리고 마지막으로(finally), 클로저(closure)로 해당 조건(qualified)을 주거나 무조건적으로(unconditionally)
removeAll()
을 사용하여 컬렉션(collection)의 모든 요소를 제거(remove)할 수 있다:prices.removeAll() { $0 > 2 } // prices is now [1.5] prices.removeAll() // prices is now an empty array
Lazy collections
때때로(sometimes) 거대한(huge) 컬렉션(collection)을 가질 수 있고 심지어(perhaps) 무한(infinite)할 수도 있지만, 어떻게든(somehow) 접근(access)할 수 있기(be able to)를 원한다(want to). 이것의 구체적인 예(concrete example)는 모든 소수(prime numbers)가 될 것이다. 그것은 무한한(infinite) 숫자의 집합(set)이다. 그런 집합(set)을 사용하려면 lazy collection을 입력(enter)한다. 처음 10개의 소수(prime numbers)를 계산(calculate)하려 한다. 명령형(imperative) 방식으로 이 작업을 수행하려면 다음과 같이 할 수 있다:
func isPrime(_ number: Int) -> Bool { if number == 1 { return false } if number == 2 || number == 3 { return true } for i in 2...Int(Double(number).squareRoot()) { if number % i == 0 { return false } } return true } var primes: [Int] = [] var i = 1 while primes.count < 10 { if isPrime(i) { primes.append(i) } i += 1 } primes.forEach { print($0) }
숫자(number)의 소수(prime) 여부를 확인(checks)하는 함수(function)를 만든다(creates)(). 그런 다음(then), 이를 사용하여 처음 10개의 소수(prime)로 구성된 배열(array)을 생성(generate)한다.
Note: 위의 소수(prime) 여부를 계산(calculate)하는 함수(function)는 그다지 성능이 좋지 않다. 이것은 심오한(deep) 주제(topic)이며 이 장(chapter)의 범위(scope)를 훨씬 벗어난다(far beyond). 궁금하다(curious)면, 먼저 에라토스테네스의 체(the Sieve of Eratosthenes)에 대해 읽는 것이 좋다.
이것은 잘 작동(works)하지만, 이 장(chapter)의 앞부분(earlier)에서 본 것처럼 함수적인 구현(functional)이 더 나은 방법이다. 처음 10개의 소수(prime number)를 가져오는 함수적인(functional) 방법은 모든 소수(prime numbers)의 시퀀스(sequence)를 가져온 다음
prefix()
를 사용하여 처음 10개를 얻는(get) 것이다. 그러나(however) 어떻게 무한한(infinite) 시퀀스(sequence)를 가져와prefix()
를 얻을 수 있을까? 여기에서lazy
연산(operation)을 사용하여 Swift에게 필요할 때(on-demand) 컬렉션(collection)을 생성(create)하도록 지시할 수 있다.실제로(action) 해 본다. 위(above)의 코드(code)를 다음과 같이 다시 작성(rewrite)할 수 있다:
let primes = (1...).lazy .filter { isPrime($0) } .prefix(10) primes.forEach { print($0) }
완전히 개방된(completely open-ended) 컬렉션(collection)인
1...
부터 시작한다(start with). 즉, 무한대(infinity, 또는Int
유형이 가질 수 있는 최대(maximum) 정수(integer))까지를 의미(means)한다. 그리고 나서(then),lazy
를 사용(use)하여 이것이 lazy 컬렉션(collection)이 되길 원한다고 Swift에 알린다(tell). 그런 다음(then),filter()
와prefix()
를 사용하여 소수(primes)를 걸러 내고(filter out) 처음 10개를 선택(choose)한다.그 시점에서(at that point), 소수(primes)를 확인(checked)하지 않았기 때문에 시퀀스(sequence)는 전혀 생성(generated)되지 않는다. 두 번째 구문(statement)에서만
primes.forEach
로 시퀀스(sequence)를 확인(evaluated)하고 처음 10개의 소수(prime)가 출력된다(printed out).lazy 컬렉션(collections)은 컬렉션(collection)이 크거나(huge, 심지어 무한대(even infinite)) 이를 생성(generate)하는 데 비용이 많이 드는(expensive) 경우 매우(extremely) 유용(useful)하다. 정확히(precisely) 필요한 시점까지 계산(computation)을 유예(saves)한다.
이것으로 클로저(closures)를 사용한 컬렉션 반복(collection iterations)을 마무리(wraps up)한다.
Mini-exercises
- 일부 이름을 문자열(strings)로 포함(contains)하는
names
이라는 상수(constant) 배열(array)을 생성(create)한다. 어떤 이름(names)이든 가능하지만, 세 개 이상이어야 한다. 이제reduce
를 사용(use)하여 배열(array)의 각(each) 이름(name)을 연결(concatenation)한 문자열(string)을 만든다. - 동일한
names
배열(array)을 사용하여 4자(characters)보다 긴 이름(names)만 포함(contain)하도록 배열(array)을 필터링(filter)한 다음, 위의 예제(exercise)와 동일한 연결(concatenation)된 이름을 생성한다(Hint: 이러한 연산(operations)을 함께 연결(chain)할 수 있다.) - 문자열(strings) 이름(names)에 정수(integers) 나이(ages)를 매핑(mapped)해 포함(containing)하는
namesAndAges
라는 상수(constant) 딕셔너리(dictionary)를 생성(create)한다. 이제filter
를 사용하여 18세 미만의 사람들만 포함(containing)하는 딕셔너리(dictionary)를 만든다(create). - 동일한
namesAndAges
딕셔너리(dictionary)를 사용하여 성인(18세 이상)을 필터링(filter out)한 다음,map
을 사용하여 이름(names)만 포함(containing)하고 있는 배열로 변환(convert to)한다(예(i.e.): 연령 제거(drop the ages)).
Challenges
다음 단계로 넘어가기 전에(before moving on) 클로저(closures)를 사용한 컬렉션 반복(collection iterations)에 대한 지식(knowledge)을 확인(test)하기 위한 몇 가지 챌린지(challenges)가 있다. 스스로 해결(solve)해 보려고 하는 것이 가장 좋지만, 막힌다면(get stuck) 다운로드(download)나 책의 소스 코드 링크(source code link)에서 해답(solutions)을 참고할 수 있다.
Challenge 1: Repeating yourself
첫 번째 챌린지(challenge)는 주어진(given) 클로저(closure)를 주어진(given) 횟수만큼 실행(run)하는 함수(function)를 작성(write)하는 것이다.
다음과 같이 함수(function)를 선언(declare)한다:
func repeatTask(times: Int, task: () -> Void)
함수(function)는
task
클로저(closure)를times
번 실행(run)해야 한다. 이 함수(function)를 사용하여"Swift Apprentice is a great book!"
을 10번 출력(print)해 본다.Challenge 2: Closure sums
이 챌린지(challenge)에서는 다른 수학적(mathematical) 합계(sums)를 생성(create)하기 위해 재사용(reuse)할 수 있는 함수(function)를 작성(write)한다.
다음과 같이 함수(function)를 선언(declare)한다:
func mathSum(length: Int, series: (Int) -> Int) -> Int
첫 번째 매개변수(parameter)인
length
는 합계(sum)할 값(values)의 수(number)를 정의(defines)한다. 두 번째 매개변수(parameter)인series
는 일련의 값(series of values)을 생성(generate)하는 데 사용할 수 있는 클로저(closure)이다.series
에는 시리즈(series) 내 값(value)의 위치(position)인 매개변수(parameter)가 있어야 하며, 해당 위치(at that position)의 값(value)을 반환(return)해야 한다.mathSum
은 위치(position) 1에서 부터 시작(starting)하여,length
값의 개수(number of values)를 계산(calculate)하고 합(sum)을 반환(return)해야 한다.함수(function)를 사용하여 385와 같은 처음 10개 제곱수(square numbers)의 합(sum)을 구한다(find). 그런 다음(then), 이 함수(function)를 사용(use)하여 처음 10개 피보나치 수(Fibonacci numbers)의 합(sum)을 구하면(find) 143이 된다. 피보나치 수(Fibonacci numbers)는 5장(chapter), "Functions"에서 작성한 함수를 사용할 수 있다. 해당 장에서 자신이 작성한 해답(solution)이 올바른지(correct) 확신이 서지 않는다면, 따로 해답(solutions)을 가져와도(grab) 된다.
Challenge 3: Functional ratings
이 마지막(final) 챌린지(challenge)에는 관련 등급(associated ratings)이 부여(given)된 앱 이름 목록(list)이 있다. Note — 이는 모두 가상(fictional)의 앱(apps)이다. 다음과 같이 데이터(data) 딕셔너리(dictionary)를 생성(create)한다:
let appRatings = [ "Calendar Pro": [1, 5, 5, 4, 2, 1, 5, 4], "The Messenger": [5, 4, 2, 5, 4, 1, 1, 2], "Socialise": [2, 1, 2, 2, 1, 2, 4, 2] ]
먼저(first), 앱 이름(app names)에 평균 평점(average ratings)을 매핑(mapping)한
AverageRatings
라는 딕셔너리(dictionary)를 만든(create)다.forEach
를 사용(use)하여appRatings
딕셔너리(dictionary)를 반복(iterate)한 다음,reduce
를 사용(use)하여 평균 평점(average rating)을 계산(calculate)한다. 이 등급(rating)을averageRatings
딕셔너리(dictionary)에 저장(store)한다. 마지막으로(finally)filter
와map
을 함께 연결하여(chained together to) 평균 평점(average rating)이 3보다 큰 앱 이름(app names) 목록(list)을 가져온다(get).Key points
- 클로저(closures)는 이름이 없는 함수(functions)이다. 변수(variables)에 할당(assigned to)하고 함수(functions)에 매개변수(parameters)로 전달(passed)할 수 있다.
- 클로저(closures)는 다른 함수(functions)보다 훨씬 더 쉽게(a lot easier) 사용할 수 있는 약식 구문(shorthand syntax)을 가지고 있다.
- 클로저(closure)는 주변(surrounding) 맥락(context)에서 변수(variables)와 상수(constants)를 캡처(capture)할 수 있다.
- 클로저(closure)를 사용하여 컬렉션(collection)을 정렬(sorted)하는 방법을 정의(direct)할 수 있다.
- 컬렉션(collection)에는 반복(iterate over)하고 변환(transform)하는 데 사용할 수 있는 편리한(handy) 함수(functions) 집합(set)이 있다. 변환(transforms)에는 각(each) 요소(element)를 새 값(value)에 매핑(mapping), 특정(certain) 값(values)을 필터링(filtering out), 컬렉션(collection)을 단일 값(single value)으로 축소(reducing)하는 등의 작업이 있다.
- lazy 컬렉션(collections)을 사용하면 꼭 필요한 경우(strictly needed)에만 컬렉션(collection)을 평가(evaluate)하도록 할 수 있다. 즉, 크거나(large) 비싸거나(expensive) 잠재적으로(potentially) 무한한(infinite) 컬렉션(collections)을 쉽게 작업할 수 있다.
'Raywenderlich > Swift Apprentice' 카테고리의 다른 글
Chapter 10: Structures (0) 2022.10.22 Chapter 9: Strings (0) 2022.09.29 Chapter 7: Arrays, Dictionaries & Sets (1) 2022.08.27 Chapter 6: Optionals (0) 2021.10.08 Chapter 5: Functions (0) 2021.09.02 - 일부 이름을 문자열(strings)로 포함(contains)하는