클로저
클로저에 대해서 간단하게 예제를 들어보면서 살펴보자.
calculator
이라는 함수가 있고, 값을 더하거나 빼거나 곱하는 코드를 작성해보자!
func calculator(n1: Int, n2: Int) -> Int {
return n1 + n2
}
var makeCalculation = calculator(n1: 2, n2: 5) //7
만약 calculator
함수에 대한 입력을 순서대로 전달하려면 어떻게 해야 할까?
func calculator(n1: Int, n2: Int) -> Int {
return n1 + n2
}
func add(n1: Int, n2: Int) -> Int {
return n1 + n2
}
add
함수를 만들었고 이 함수를 Calculator
함수의 input
매개변수로 넣어주면 된다. input
으로 넣기 위해선 add
함수를 요약해야 한다. add
함수를 보면 (Int, Int) -> Int
로 요약 가능하다. 이것을 calculator
인풋 매개변수로 넣어주면 끝
func calculator(n1: Int, n2: Int, operation: (Int, Int) -> Int) -> Int {
return operation(n1, n2)
}
func add(no1: Int, no2: Int) -> Int {
return no1 + no2
}
func multiply(no1: Int, no2: Int) -> Int {
return no1 * no2
}
func divide(no1: Int, no2: Int) -> Int {
return no1 / no2
}
calculator(n1: 4, n2: 2, operation: add) //6
calculator(n1: 10, n2: 2, operation: multiply) //20
calculator(n1: 20, n2: 2, operation: divide) //10
클로저를 만드는 법을 쉽게 보자면
그렇다면 위 예제처럼 add
/ multiply
/ divide
함수를 사용하지 않고 클로저를 사용하면 편하겠네..!
func add(no1: Int, no2: Int) -> Int {
return no1 + no2
}
// 이 함수를
{ (no1: Int, no2: Int) -> Int in
return no1 + no2
}
// 이렇게 만들고 아래와 같이 호출함
calculator(n1: 4, n2: 2, operation: { (no1: Int, no2: Int) -> Int in
return no1 + no2
}) //6
두 줄의 코드로 줄일 수 있지만 여전히 복잡하다. 하지만 이 클로저를 또 한 번 줄일 수 있다. 바로 유형추론을 사용하는 것이다.
요약하는 단계
유형 추론을 사용하여 입력값을 지우거나, $
기호를 사용하여 극단적으로 코드를 줄일 수 있다. 또 클로저 이름을 삭제하고 후행 클로저를 사용하여 줄일 수 도 있다.
클로저의 장단점
장점 - 코드를 극적으로 단순화하는 것.
단점 - 기독 성이 떨어짐
고차 함수
맵(map)
map
은 자신을 호출할 때 매개변수로 전달된 함수를 실행하여 그 결괏값을 다시 반환해주는 함수이다.map
을 사용하기 위해서는 Swift의Collection
,Sequence
프로토콜을 따르면 가능하다. 따라서Array
,Dictionary
,Set
,optioanl
등에서 사용이 가능map
을 사용하여도 기존의 컨테이너의 값은 변경되지 않고 새로운 컨테이너가 생성되어map
은 기존 데이터를 변형하는데 많이 사용된다.map
은 다른 함수의 형태로 입력을 받는다.
map
메서드와 for-in
구문의 차이점은 코드의 재사용이나 컴파일러 최적화 성능 차이이다.
또, 다중 스레드 환경일 때 대상 컨테이너의 값이 스레드에서 변경되는 시점에 다른 스레드에서도 동시에 값이 변경되려고 할 때 예측하지 못한 결과가 발생하는 부작용을 방지한다.
let item = ["가방", "책", "블로그", "지갑"]
func addName(name: String) -> String {
return "서근의 " + name
}
item.map(addName)
이것을 클로저로 바꾸면 아래와 같다.
let item = ["가방", "책", "블로그", "지갑"]
let first = item.map{ (name) in "서근의 " + name }
print(first)
let second = item.map({(name) in "서근의 " + name})
print(second)
let third = item.map {"서근의 " + $0}
print(third)
정수를 문자열로 바꿀 수 있다.
let number = [1, 2, 3, 4, 5]
print(number.map{$0 + 1}) //[2, 3, 4, 5, 6]
let newArray = number.map{"\($0)"} //Int타입을 String 타입으로 변환 가능
print(newArray) //["1", "2", "3", "4", "5"]
map 메서드와 for-in 구문
for-in
구문과 map
메서드 사용을 비교해보자면 아래와 같다.
let numbers: [Int] = [0, 1, 2, 3, 4]
var doubledNumbers: [Int] = [Int]()
var string: [String] = [String]()
for number in numbers {
doubledNumbers.append(number * 2)
string.append("\(number)")
}
print(doubledNumbers) //[0, 2, 4, 6, 8]
print(string) //[0, 2, 4, 6, 8]
//map 메서드
doubledNumbers = numbers.map({ (number: Int) -> Int in
return number * 2 //[0, 2, 4, 6, 8]
})
string = numbers.map({ (number: Int) -> String in
return "\(number)" //["0", "1", "2", "3", "4"]
})
map
메서드를 사용하면 for-in
구문을 사용한 것보다 간단하고 편하게 연산을 실행할 수 있다. 또, map
메서드를 사용하면 for-in
구문을 사용하기 위해 빈 배열을 생성할 필요도, append
연산을 실행할 시간도 필요가 없어진다.
위 코드에서 사용된 map
메서드를 클로저 표현으로 요약할 수 있다.
let numbers: [Int] = [0, 1, 2, 3, 4]
//map 메서드
doubledNumbers = numbers.map({ (number: Int) -> Int in
return number * 2 //[0, 2, 4, 6, 8]
})
string = numbers.map({ (number: Int) -> String in
return "\(number)" //["0", "1", "2", "3", "4"]
})
//유형 추론으로 요약 가능
doubledNumbers = numbers.map({ (number) in
return number * 2 //[0, 2, 4, 6, 8]
})
string = numbers.map({ (number) in
return "\(number)" //["0", "1", "2", "3", "4"]
})
//매개변수 및 반환 타입 생략
doubledNumbers = numbers.map({return $0 * 2})
string = numbers.map({return "\($0)"})
//반환 키워드 생략
doubledNumbers = numbers.map({$0 * 2})
string = numbers.map({"\($0)"})
//후행 클로저로 요약 가능
doubledNumbers = numbers.map { $0 * 2 }
string = numbers.map { "\($0)"}
위에서 'map 메서드와 for-in 구문의 차이점은 코드의 재사용이나 컴파일러 최적화 성능 차이이다.'라고 언급했었는데, 코드의 재사용 측면에 대해 알아보자면 만약 같은 기능을 여러 번 사용해야 한다면 하나의 클로저를 여러 map
메서드에서 사용하는 것이 좋다.
let evenNumbers: [Int] = [0, 2, 4, 6, 8, 10]
let oddNumbers: [Int] = [0, 1, 3, 5, 7, 9]
let multiplyTwo: (Int) -> Int = { $0 * 2 }
let doubledEvenNumbers = evenNumbers.map(multiplyTwo)
//[0, 4, 8, 12, 16, 20]
let doubledOddNumbers = oddNumbers.map(multiplyTwo)
//[0, 2, 6, 10, 14, 18]
다양한 컨테이너 타입에서의 map 활용
let alphabetDictionary: [String: String] = ["a":"A", "b":"B"]
var keys: [String] = alphabetDictionary.map { (tuple: (String, String)) -> String in
return tuple.0
}
print(keys) //["b", "a"]
keys = alphabetDictionary.map { $0.0 }
print(keys) //["a", "b"]
keys = alphabetDictionary.map({$0.1})
print(keys) //["A", "B"]
let value: [String] = alphabetDictionary.map{ $0.1 }
print(value) //["A", "B"]
var numberSet: Set<Int> = [1, 2, 3, 4]
let resultSet = numberSet.map { $0 * 2 }
print(resultSet) //[2, 8, 4, 6]
let optionalInt: Int? = 2
let resultInt = optionalInt.map { $0 * 3 }
print(resultInt) //Optional(6) - error : 타입캐스팅에서 다루도록 함
let range: CountableClosedRange = (0...5)
let resultRange: [Int] = range.map { $0 * 5 }
print(resultRange) //[0, 5, 10, 15, 20, 25]
위 코드에서 optional
쪽이 오류가 났는데 이 부분은 타입 캐스팅 부분에서 자세히 다뤄보겠습니다.
필터(filter)
filter
는 내부 값을 걸러서 추출하는 역할을 한다.map
과 동일하게 새로운 컨테이너에 걸러진 값을 담아 반환한다.map
은 기존의 요소를 변경한 값을 반환했다면,filter
는 기준을 가지고 기준에 맞는 값들을 반환해준다.filter
함수의 매개변수로 전달되는 함수 반환 타입은Bool
이다.- 새로운 컨테이너에 포함될 항목이라고 판단되면
true
, 그게 아니라면false
를 반환
let number = [1, 2, 3, 4, 5]
print(number.filter {$0 > 3}) //4, 5
//필터 조건이 맞다면 map조건을 실행
let filterAndMap = [1, 2, 3, 4, 5].filter{$0 > 3}.map{$0 * 10}
print(filterAndMap) //40, 50
이런 식으로 filter
를 사용하여 필요 없는 요소들을 삭제하고 필요한 요소들만 가지고 연산을 하는 것이 가능하다.
let numbers: [Int] = [0, 1, 2, 3, 4, 5]
var evenNumber: [Int] = numbers.filter { (number: Int) -> Bool in
return number % 2 == 0
}
print(evenNumber) // [0, 2, 4]
let oddNumbers: [Int] = numbers.filter { $0 % 2 == 1 }
print(oddNumbers) // [1, 3, 5]
콘텐츠의 변형 후 필터링이 필요하다면 아래 코드와 같이 map
을 사용 후 필터 메서드를 호출할 수 있다.
let numbers: [Int] = [0, 1, 2, 3, 4, 5]
let mappedNumber: [Int] = numbers.map{ $0 + 3 }
let evenNumber: [Int] = mappedNumber.filter { (number: Int) -> Bool in
return number % 2 == 0
}
print(evenNumber) //[4, 6, 8]
//mappedNumber 프로퍼티가 굳이 필요하지 않다면 메서드를 체인처럼 연결해 사용 가능
let oddNumbers: [Int] = numbers.map{$0 + 3}.filter { $0 % 2 == 1 }
print(oddNumbers) //[3, 5, 7]
↪︎ map과 filter 메서드 연계 사용
map
과 filter
를 연계하여 사용하면 복잡한 연산을 쉽게 해결할 수 있다.
리듀스(reduce)
reduce
는 줄이다 라는 뜻이지만,결합 기능
을 하는 메서드이다.- 컨테이너의 내부의 요소들을 하나로 합치는 기능을 하는 고차 함수이다.
- 배열의 모든 값을 전달 인자로 전달받아 클로저의 연산 결과로 합해주게 된다.
Swift에서의 reduce 형태 (두 가지)
- 첫 번째, 클로저가 각 요소를 전달받아 연산한 후 값을 다음 클로저 실행을 위해 반환하며 컨테이너를 순환하는 형태.
initialResult
라는 이름의 매개변수로 전달되는 값을 통해 초깃값을 지정하고,nextPartialResult
매개변수로 클로저를 전달받음. - 두 번째, 컨테이너를 순환하며 클로저가 실행되지만 클로저가 따로 결괏값을 반환하지 않는 형태. 대신
inout
매개변수를 사용하여 초깃값에 직접 연산을 실행함
let number = [1, 2, 3, 4, 5]
let sum1 = number.reduce(0) { (result:Int, element: Int) -> Int in return result + element }
print(sum1) //15
//추론으로 생략 가능
let sum2 = number.reduce(0) { (result, element) in result + element }
print(sum2) //15
let sum3 = number.reduce(0) {$0 + $1}
print(sum3) //15
let sum4 = number.reduce(1, +)
print(sum4) //16
/*
reduce 초기값이 0이기 때문에 0 + 1 부터 시작하여 마지막 값을 결괏값으로 보여준다.
0 + 1
1 + 2
3 + 3
6 + 4
10 + 5
결괏값 = 15
*/
만일 initial
(초기값) 값이 1이라면 초기 항목은 {1 + 1}
이다. 클로저는 이전 결과와 다음 항목을 계속 호출하여 다음과 같은 과정을 거쳐 하나의 값을 얻게 된다. {1 + 1}
, {2 + 2}
, {4 + 3}
, {7 + 4}
, {11 + 5}
이며, 결과는 16
이 된다.
map, filter, reduce 활용
map
, filter
, reduce
를 활용해서 특정 조건을 출력하는 코드를 만들어 보려고 한다. 먼저 Friend
구조체에 친구의 정보를 담을 저장 프로퍼티를 생성하고 Gender
배열을 정의해 성별을 담아둔다. 그리고 배열 friends
인스턴스를 생성한다.
enum Gender: String {
case male = "남자"
case female = "여자"
case unknow = "미상"
}
struct Friend {
let name: String
let gender: Gender
let location: String
var age: UInt
}
var friends: [Friend] = [Friend]()
친구들의 정보를 아래 코드처럼 정의해 주는데 현재 나이 + 1 을 해주고, 조건은 "미국에 거주하지 않는 20세보다 작거나 같은 남성을 찾는다."인 사람을 찾으려고 한다.
friends.append(Friend(name: "서근", gender: .male, location: "러시아", age: 20))
friends.append(Friend(name: "철수", gender: .male, location: "한국", age: 15))
friends.append(Friend(name: "민지", gender: .female, location: "미국", age: 22))
friends.append(Friend(name: "훈이", gender: .male, location: "대전", age: 13))
friends.append(Friend(name: "영미", gender: .female, location: "서울", age: 31))
friends.append(Friend(name: "찰스", gender: .male, location: "미국", age: 11))
friends.append(Friend(name: "후산", gender: .male, location: "우즈베키스탄", age: 17))
friends.append(Friend(name: "하산", gender: .female, location: "카자흐스탄", age: 20))
이제 map
으로 현재 나이에 + 1을 더해 Friend
배열을 생성한다.
var result: [Friend] = friends.map {
Friend(name: $0.name, gender: $0.gender, location: $0.location, age: $0.age + 1)
}
그리고 filter
메서드로 미국에 거주하지 않고 20세보다 작거나 같은 남성을 걸러주고, reduce
메서드로 필터링해준다.
result = result.filter { $0.location != "미국" && $0.gender == .male && $0.age <= 20 }
let string: String = result.reduce("미국에 거주하지 않는 20세보다 작거나 같은 남성을 찾는다.") {
$0 + "\n" + "이름:\($1.name) | 거주지:\($1.location) | 성별:\($1.gender.rawValue) | 나이:\($1.age)세"
}
print(string)
//미국에 거주하지 않는 20세보다 작거나 같은 남성을 찾는다.
//이름:철수 | 거주지:한국 | 성별:남자 | 나이:16세
//이름:훈이 | 거주지:대전 | 성별:남자 | 나이:14세
//이름:후산 | 거주지:우즈베키스탄 | 성별:남자 | 나이:18세
전체 코드로 보면 다음과 같다.
enum Gender: String {
case male = "남자"
case female = "여자"
case unknow = "미상"
}
struct Friend {
let name: String
let gender: Gender
let location: String
var age: UInt
}
var friends: [Friend] = [Friend]()
friends.append(Friend(name: "서근", gender: .male, location: "러시아", age: 20))
friends.append(Friend(name: "철수", gender: .male, location: "한국", age: 15))
friends.append(Friend(name: "민지", gender: .female, location: "미국", age: 22))
friends.append(Friend(name: "훈이", gender: .male, location: "대전", age: 13))
friends.append(Friend(name: "영미", gender: .female, location: "서울", age: 31))
friends.append(Friend(name: "찰스", gender: .male, location: "미국", age: 11))
friends.append(Friend(name: "후산", gender: .male, location: "우즈베키스탄", age: 17))
friends.append(Friend(name: "하산", gender: .female, location: "카자흐스탄", age: 20))
var result: [Friend] = friends.map { Friend(name: $0.name, gender: $0.gender, location: $0.location, age: $0.age + 1)}
result = result.filter { $0.location != "미국" && $0.gender == .male && $0.age <= 20}
let string: String = result.reduce("미국에 거주하지 않는 20세보다 작거나 같은 남성을 찾는다.") { $0 + "\n" + "이름:\($1.name) | 거주지:\($1.location) | 성별:\($1.gender.rawValue) | 나이:\($1.age)세"}
print(string)
//미국에 거주하지 않는 20세보다 작거나 같은 남성을 찾는다.
//이름:철수 | 거주지:한국 | 성별:남자 | 나이:16세
//이름:훈이 | 거주지:대전 | 성별:남자 | 나이:14세
//이름:후산 | 거주지:우즈베키스탄 | 성별:남자 | 나이:18세
읽어주셔서 감사합니다🤟
'SWIFT > Grammar' 카테고리의 다른 글
Swift : 기초문법 [서브스크립트(subscript)] (0) | 2022.01.21 |
---|---|
Swift: 기초문법 [모나드 - 컨텍스트, 함수객체, 모나드] (0) | 2022.01.20 |
Swift : 기초문법 [접근 제어 - open, public, internal, fileprivate, private] (0) | 2022.01.18 |
Swift : 기초문법 [인스턴스 #4 클로저 사용 프로퍼티 기본값, 디이니셜라이저] (2) | 2022.01.15 |
Swift : 기초문법 [인스턴스 #3 초기화 위임, 실패가능한 이니셜라이저] (0) | 2022.01.15 |