집합
집합(Set)
스위프트에서 집합은 순서가 없는 중복되지 않는 값들의 컬렉션입니다. 딕셔너리처럼 집합에 포함된 값들에는 특정한 순서가 없으며 딕셔너리의 키(Key)처럼 집합은 중복된 값을 포함할 수 없습니다.
스위프트의 다른 컬렉션 타입들과 마찬가지로 집합도 강한 타입 제약을 받습니다. 집합에는 지정된 한 가지 특정 타입의 값들만 담을 수 있다는 뜻입니다. 어떻게 보면 활용면에서의 제약으로 볼 수 있지만 실제로는 의도하지 않은 타입의 값을 넣어서 발생할 수 있는 버그와 문제들을 방지해 줍니다.
해쉬 값과 집합
집합에는 동일한 타입의 값들만 담을 수 있다는 것보다 더 넓은 범위의 요구 사항이 있습니다. 집합에 저장될 타입은 반드시 스위프트의Hashable
프로토콜을 따라야한다는 것입니다. 즉, 집합을 구성하려면 그 자료형에Int
값의hashValue
프로퍼티가 있어야한 다는 뜻입니다.
추가적으로Hashable
프로토콜은Equatable
프로토콜을 따르기 때문에 등호 비교 연산자 (‘==’)도 구현해야합니다. 스위프트 집합에서는hashValue
와 등호 비교 연산을 통해서 집합에 포함된 각각의 값들을 비교하여 중복되지 않은지 확인합니다.
그렇다면 어떤 타입들이Hashable
프로토콜을 따르는지 궁금해집니다. 다행히 스위프트의 거의 모든 기본 자료형들은Hashable
을 디폴트로 따르고 있습니다. 여기에는String
,Int
,Double
,Bool
타입들과 연관값(associated value)이 없는 모든 열거형이 포함됩니다.
집합의 선언과 초기화
가변 집합 vs. 불변 집합
스위프트의 다른 컬렉션 타입과 마찬가지로 집합의 가변성은 집합을var
또는let
키워드 중에 어떤 것으로 선언하는지에 따라 결정됩니다.
var
키워드로 선언하면 집합에 들어가는 내용들은 변경이 가능합니다. 따라서 집합에 포함된 아이템을 추가하고, 제거하는 변경이 가능하다는 뜻입니다.
let
키워드로 집합을 선언하면 불변 집합이 되어 집합에 포함된 아이템을 변경할 수 없게 됩니다.
빈 집합의 생성
스위프트에서 집합을 생성하는 데에는 여러 가지가 방법이 있습니다. 그 중에 한 가지는 생성자 문법을 사용하는 것입니다.
var firstSet = Set<String>()
모든 변수를 정의하는 것처럼var
키워드를 사용하고 변수명firstSet
으로 가변(mutable)의 집합을 만들었습니다. 가변의 집합 변수를 정의하고 대입연산자(‘=’) 다음에Set
타입명을 지정했습니다.Set
은 스위프트의 제네릭 자료형중의 한 가지입니다. 여기서는String
자료형을 지정해서 이 집합이 문자열String
타입의 엘리먼트들을 저장할 것이라는 것을 지정했습니다.
배열 표현식으로 빈 집합 생성
빈 배열 표현식을 파라미터로 전달하여 집합을 생성할 수 있습니다.
let secondSet = Set<Double>([])
이전의 예와 거의 비슷합니다. 이번에는let
키워드를 사용하여 불변(immutable) 집합으로 만들었으며String
대신에Double
형의 값을 담을 수 있는 집합을 정의하였습니다.
여기서 빈 배열 표현 ([]
)을 생성자의 파라미터로 전달하였는데 이 빈 배열을 사용하여 빈 집합Set<Double>
을 생성하라는 뜻입니다.
빈 집합을 만드는 또 다른 방법은 집합을 선언하는 것과 초기화하는 과정을 분리하는 방법입니다. 문맥상 집합의 타입을 유추할 수 있다면 빈 배열 표현식을 사용하여 초기화할 수 있습니다.
let thirdSet: Set<Double>
thirdSet = []
값이 들어 있는 집합의 생성
값이 들어 있는 집합으로 초기화하면서 정의하는 방법에는 두 가지가 있습니다. 위에서 본 것처럼 집합 표현식을 사용하는 방법과 범위 시퀀스를 사용하는 방법입니다.
먼저 집합 표현식으로 집합을 초기화하거나 대입하는 방법입니다.
let fourthSet = Set<Int>([10, 30])
let fifthSet: Set<Double>
fifthSet = [10.1, 37.2, 33.5]
다음으로 시퀀스로 집합을 초기화 하는 방법입니다.
let sixthSet = Set<Int>(1...5)
빈 집합인지 검사하는 방법
집합에 몇 개의 엘리먼트가 들어 있는지 검사하려면Set
의 프로퍼티를 사용해야합니다.
첫 번째 프로퍼티는isEmpty
로 집합에 엘리먼트가 없는 빈 집합인 경우 참(true)을 반환하고 그 반대의 경우에는 거짓(false)을 반환합니다.
firstSet.isEmpty
집합의count
프로퍼티는 집합에 들어 있는 엘리먼트의 수를Int
값으로 반환합니다.
firstSet.count // 0
fifthSet.count // 3
집합에 엘리먼트를 추가하는 방법
이제 집합을 생성할 수 있게 되었습니다. 다음으로 집합에 새로운 엘리먼트를 추가하는 방법을 살펴보겠습니다. 엘리먼트의 추가는insert(_:)
함수를 사용하면 됩니다.
insert(_:)
함수는 집합에 저장된 엘리먼트와 동일한 타입의 단일 파라미터를 입력 받습니다. (호환되지 않는 타입의 값을 사용하면 에러가 발생합니다.)
예를 들어 보겠습니다.
먼저 스타워즈 영화의 등장인물을 저장하기위한 목적의 빈 집합으로forceAwakens
을 생성하고 엘리먼트 하나를 추가하도록 하겠습니다.
var forceAwakens = Set<String>()
forceAwakens.count // 0
forceAwakens.isEmpty // true
forceAwakens.insert("Rey") // 이제 forceAwakens에 하나의 아이템으로 "Rey"가 담겨집니다.
forceAwakens.count // 1
위의 코드에서forceAwakens
이라는 새로운 집합을 선언하고 이 집합에 영화에 등장하는 인물들의 이름을 저장합니다.
최초에는 이 집합은 비어있습니다. (따라서count
는 0입니다.)insert(_:)
함수를 호출하여 “Rey”를 추가하였습니다. 최종적으로는 이 집합은 하나의 엘리먼트를 담고 있고count
는 이 결과를 반영하게됩니다.
여러 개의 엘리먼트를 집합에 추가하려면 반복문을 사용해서insert(_:)
함수를 여러번 호출하면 됩니다.
for character in ["Finn", "BB-8", "Yoda", "C-3PO", "Han Solo", "Princes Leia"] {
forceAwakens.insert(character)
}
// {"Rey", "Han Solo", "Princess Leia", "BB-8", "Yoda", "C-3PO", "Chewbacca", "Finn"}
// 주의: 집합의 엘리먼트에서 순서는 보장 되지 않으므로 매 번 순서가 다를 수 있습니다.
다음에 살펴보겠지만 집합을 다른 집합에 추가하는 방법도 있습니다. 구체적으로unionInPlace(_:)
함수를 사용하는 것입니다. 이 함수를 자세히 알아보기 전에, 두 가지 추가적인 사항들을 먼저 짚고 넘어가겠습니다. 먼저 집합에 들어 있는 엘리먼트에 접근하는 방법입니다.
집합에 속한 엘리먼트를 찾는 방법
앞서 생성한 집합forceAwakens
에 어떤 값이 이미 들어 있는지 여부를 확인하려면 어떻게 해야할까요?
엘리먼트가 존재하는지 확인하는 방법
어떤 항목이 집합에 들어있는지 여부를 확인하려면contains(_:)
함수를 사용하면 됩니다.
contains(_:)
함수는 집합이 수용하는 엘리먼트와 동일한 타입의 값을 파라미터로 전달받아 해당 엘리먼트가 집합에 포함되어 있는지 여부를 불리언 값으로 반환합니다.
forceAwakens.contains("Yoda") // true
단순히 어떤 엘리먼트가 집합에 포함되어 있는지 여부를 확인하는 것 이상으로 그 엘리먼트의 인덱스(index)를 확인할 수도 있습니다.
집합에 속한 엘리먼트의 인덱스를 확인하는 방법
앞서의 정의에서 집합에는 엘리먼트를 보관하는 순서가 없다고 하였습니다. 하지만 일단 엘리먼트가 저장되고 나면, 엘리먼트는 집합의 특정 인덱스로 지정되는 위치에 저장됩니다. 다른 컬렉션 타입에서 처럼 집합에서의 엘리먼트의 인덱스는 0부터 시작하며 전체 집합에 속한 엘리먼트의 수보다 1 작은 수까지 증가합니다.
예를 들어 “Yoda”가 저정되어 있는 인덱스는 아래처럼 구할 수 있습니다.
forceAwakens.indexOf("Yoda")
indexOf(_:)
함수는SetIndex<T>
의 값을 리턴하는데 여기서T
는 집합이 저장하고 있는 값의 타입입니다.
집합을 순차 순회하는 방법
다른 컬렉션 타입과 마찬가지로 집합도 순차적으로 순회할 수 있습니다. 다만 주의해야할 사항은 엘리먼트의 순서를 보장하지 못한다는 점입니다.
for character in forceAwakens {
print(character)
}
엘리먼트에 순서를 지정하려면sort()
함수를 통해서 값들을 정렬하여 그 결과를 순회하면 됩니다.
for character in forceAwakens.sort() {
print(character)
}
집합의 엘리먼트를 제거하는 방법
집합의 첫 번째 엘리먼트를 제거하는 방법
다른 프로그래밍 언어를 배운적이 있다면 스택(stack)에 어떤 값을 푸쉬(push)하거나 팝(pop)하는 개념에 대해서 알고 있을 것입니다. 스택은 마치 접시들을 쌓아 둔 것에 비유할 수 있습니다. 새 접시는 항상 스택의 상단에 푸쉬(push)하고 필요할 때 가장 위에있는 접시를 팝(pop)해서 사용합니다.
popFirst()
는 (추상적으로) 첫 번째 엘리먼트를 꺼내(팝:pop)오면서 집합에서 이 엘리먼트를 제거하는 과정을 구현한 함수입니다.
let firstVal = forceAwakens.popFirst()
print(firstVal)
popFirst()
가 리턴하는 값은 옵셔널입니다. 만약 집합이 비어있어서 반환할 값이 없는 경우에nil
을 반환합니다.
Set
에는popFirst()
뿐만 아니라removeFirst()
함수도 있습니다.
removeFirst()
는popFist()
와 완전히 동일합니다. 집합의 (추상적으로) 가장 첫 번째 엘리먼트를 반환하고 집합에서 제거합니다. 하지만removeFirst()
함수를 호출하려면 반드시 집합에 값이 들어 있어야합니다. 빈 집합에 대해서removeFirst()
를 호출하면 에러를 던지게 됩니다. 따라서removeFirst()
함수를 호출하기 전에 집합에 최소한 하나 이상의 엘리먼트가 들어 있는지를 항상 검사해야합니다.
if forceAwakens.count > 0 {
let nextVal = forceAwakens.removeFirst()
print(nextVal)
}
집합의 엘리먼트를 제거하는 방법
remove(_:)
함수는 하나의 엘리먼트를 제거하는 함수입니다. 제거하기 원하는 엘리먼트를 파라미터로 전달합니다.
let item = forceAwakens.remove("Yoda")
print(item)
remove(_:)
의 리턴 타입은 옵셔널이며 값이 존재하여 정상적으로 제거된 경우 해당 값을 반환하고 집합 내에 존재하지 않아 제거할 수 없는 경우에는nil
을 반환합니다.
집합의 엘리먼트를 인덱스로 제거하는 방법
엘리먼트를 직접 지정해서 제거하는 방법외에도, 엘리먼트의 인덱스를 사용해서 제거할 수 있습니다.
위에서 엘리먼트의 인덱스를 획득하기 위해서indexOf(_:)
함수를 사용했었습니다. 이 함수는 해당 엘리먼트를 지정할 수 있는 인덱스 값을 옵셔널로 반환합니다. 일단 인덱스를 알게되면removeAtIndex(_:)
에 이 인덱스를 전달하여 해당하는 엘리먼트를 집합에서 제거할 수 있습니다.
주의할 점은removeAtIndex(_:)
함수는 반드시 유효한 인덱스(인덱스에 해당하는 값이 집합내에 존재하는)를 통해서 호출해야합니다. 이 함수는 옵셔널을 반환하는 함수가 아니므로 유효하지 않는 인덱스를 전달하면 에러가 발생합니다.
if let idx = forceAwakens.indexOf("BB-8") {
var otherItem = forceAwakens.removeAtIndex(idx)
print(otherItem)
}
다수의 엘리먼트를 제거하는 방법
집합에서 여러 개의 아이템들을 한 번에 제거하는 방법에 대해서 살펴보겠습니다. 여러가지 방법이 가능하지만 여기서는 간단한 방법부터 알아보도록 하겠습니다.
우선 가장 쉬운 방법은remove(_:)
함수를 여러번 호출하는 방법일 것입니다.
for character in ["Finn", "C-3PO"] {
let character = forceAwakens.remove(character)
print(character)
}
집합에 포함된 모든 엘리먼트를 제거하려면removeAll(keepCapacity:)
함수를 호출합니다. 여기서keepCapacity
는 디폴트 파라미터는 불리언 값으로 성능상의 이유로 집합의 용량을 그대로 유지하면서 엘리먼트를 제거하기 원할 때 명시적으로 지정하며 대부분의 경우 디폴트 설정을 사용합니다.
var returnOfTheJedi = Set<String>(["Chewbacca", "C-3PO", "R2-D2", "Yoda", "Darth Vader"])
여기까지 집합의 기본 사항에 대해서 알아보는 동안 집합과 배열이 매우 비슷하다는 점과 배열에 대비하여 집합이 가지는 장점에 대해서 궁금해졌을 것입니다. 왜 집합을 사용하는 것일까요?
사실 스위프트의 집합은 “집합 연산” 과 “집합 등식”을 수행할 때 매우 강력한 능력을 발휘합니다. 집합의 연산과 등식의 개념에 대해서 먼저 살펴보도록 하겠습니다.
집합의 포함관계와 등식
학교에서 배운 집합 이론을 떠올려 보겠습니다. 아래의 그림에는 영화 스타워즈의 등장인믈들을 나타내는 세 개의 집합 A, B, C가 있습니다.
첫번째 집합 A는 영화 ‘제다이의 귀환’의 등장 인물로 다음과 같이 정의됩니다.var
var returnOfTheJedi = Set<String>(["Chewbacca", "C-3PO", "R2-D2", "Yoda", "Darth Vader"])
집합 B는깨어난 포스
의 등장인물로 구성됩니다.
var forceAwakens = Set<String>(["Finn", "BB-8", "Yoda", "Chewbacca", "C-3PO", "R2-D2"])
여기서Yoda
,Chewbacca
,C-3PO
,R2-D2
는returnOfTheJedi
과forceAwakens
양쪽에 모두 속하는 등장인물입니다. 이 교집합은 집합 A와 B 모두에 속합니다.
마지막 집합 C는BB-8
하나만을 원소로 포함하는 집합으로 정의하였습니다.
var robots = Set<String>(["BB-8"])
집합 B는 집합 C를 포함하고 있으므로 집합 C는 집합 B의 부분집합이라고 합니다.
이제 예로 사용할 집합의 정의를 끝냈으니 실제 집합의 등식에 대해서 살펴보겠습니다.
집합의 등식
두 개의 집합이 동일한지 검사하는 방법은 비교 연산자 (‘==’)를 사용하여 결과를 불리언 값으로 받으면 됩니다.
예를 들어 새로운 집합robots2
를 만들어 앞서의robots
와 비교하겠습니다.
var robots2 = Set<String>(["BB-8"])
robots2 == robots // true
robots == forceAwakens // false
마지막 행의 코드를 보면robots
의 모든 엘리먼트가forceAwakens
에 포함되어 있음에도 동일한 집합이 아니므로 비교 결과는false
가 됩니다.
부분집합 (Subset)
isSubsetOf(_:)
함수를 통해서 어떤 집합이 다른 집합의 부분집합인지 확인할 수 있습니다. 위의 집합 관계도를 보면 집합 C는 집합 B의 부분집합입니다.
robots.isSubsetOf(forceAwakens) // true
robots2.isSubsetOf(forceAwakens) // true
두 번째 집합robots2
도 동일한 엘리먼트 값들을 담고 있으므로true
를 리턴했습니다.
엄격한 부분집합 (Strict Subset)
스위프트에는isSubsetOf(_:)
함수에 추가해서isStrictSubsetOf(_:)
함수가 있습니다. 이 함수는isSubsetOf
와 완전히 똑같은 방법으로 동작하지만 만약 비교하는 두 집합이 동일한 경우에false
를 반환합니다.
robots.isStrictSubsetOf(forceAwakens) // true
robots.isStrictSubsetOf(robots2) // false
robots
와robots2
가 동일한 집합이므로false
를 리턴했습니다.
상위집합 (Superset)
isSupersetOf(_:)
함수는 어떤 집합이 다른 집합을 포함하고 있는지를 검사합니다.
forceAwakens.isSupersetOf(robots) // true
isSubset(_:)
함수와 비슷한 용법으로 동일한 집합에 대해서true
를 반환합니다.
robots.isSupersetOf(robots2) // true
엄격한 상위집합 (Strict Superset)
마지막으로isStrictSupersetOf(:_)
함수입니다. 예상할 수 있는 것처럼 완전히 동일한 집합에 대해서false
를 반환합니다.
forceAwakens.isStrictSupersetOf(robots) // true
robots.isStrictSupersetOf(robots2) // false
Disjoint
이제까지 부분집합(subset)과 상위집합(superset)에 대해 살펴보았습니다.isDisjointWith(_:)
함수는 두 집합 사이에 공통된 엘리먼트가 없을 경우에true
를 반환합니다. (엄밀히 말해서 어떤 집합의 엘리먼트가 어떤SequenceType
프로토콜을 따르는 집합 또는 배열의 엘리먼트와 공통된 값이 있는지 검사합니다.)
returnOfTheJedi.isDisjointWith(robots) // true
집합 A (returnOfTheJedi
)와 집합 C (robots
) 사이에는 공통된 엘리먼트가 없으므로true
를 리턴합니다. 하지만 집합 B (forceAwakens
)과는 겹치는 부분이 있으므로false
가 리턴됩니다.
forceAwakens.isDisjointWith(robots) // false
집합 연산
이제 집합 연산을 처리하는 함수에 대해서 알아보겠습니다.
교집합 (OR)
집합 다이어그램을 다시 살펴보면 집합 A와 집합 B간의 겹치는 부분이 있습니다. 이 부분만 떼어내어 표현하면 아래 그림과 같습니다.
스위프트의Set
타입에는 교집합 부분을 처리하는 함수로intersect(_:)
가 있습니다.
intersect(_:)
함수는 한 개의 파라미터를 받습니다. 이 파라미터에는 보통 다른 집합(Set
)이나 배열(Array
)가 사용되는데 물론 꼭 이 두 가지 타입이 아니라도SequenceType
프로토콜을 따른다면 어떤 값이든 가능합니다.
intersect(_:)
함수는 양쪽에 공통으로 포함된 엘리먼트로 구성된 새로운Set
을 반환합니다.
let commonCharacters = returnOfTheJedi.intersect(forceAwakens)
이미 설명한 것 처럼 파라미터로 반드시 집합을 사용할 필요는 없습니다.
let intersectSet = forceAwakens.intersect(["Yoda", "C-3PO", "Han Solo"])
intersect(_:)
는 결과를 새로운 집합을 만들어 반환하므로 원본 집합의 값을 변경할 수 없습니다. 원본 집합을 변경하려면intersectInPlace(_:)
함수를 사용해야합니다.
원본 집합을 변경할 수 있다는 점을 고려해 보면intersectInPlace(_:)
함수는 가변(mutable) 집합에 대해서만 적용할 수 있다는 것을 이해할 수 있을 것입니다. 즉,var
키워드로 선언되어야합니다.
var forceAwakens2 = forceAwakens
forceAwakens2.intersectInPlace(returnOfTheJedi)
이제forceAwakens2
는 두 집합에 공통으로 들어 있는 엘리먼트로만 구성된 집합이 됩니다.
합집합 (AND)
다음으로 설명할Set
의 함수는union(_:)
과unionInPlace(_:)
입니다. 그림으로 표현하면 다음과 같습니다.
union(_:)
함수는 두 집합의 합집합을 새로운Set
로 생성하여 반환합니다. 파마리터는SequenceType
입니다.
let combinedSet = forceAwakens.union(returnOfTheJedi)
앞서intersect(_:)
와intersectInPlace(_:)
와 마찬가지로union(_:)
에는unionInPlace(_:)
가 있습니다. 새로운 집합을 생성하는 대신에 이 함수를 호출한 집합을 변경합니다.
var forceAwakens4 = forceAwakens
forceAwakens4.unionInPlace(returnOfTheJedi)
파라미터가SequenceType
으로 정의되어 있으므로 파라미터에 집합을 반드시 사용해야하는 것은 아닙니다.
forceAwakens4.unionInPlace(["Kylo Ren", "Poe Daeron"])
배타적 교집합 (Exclusive OR)
다음은exclusiveOr(_:)
와exclusiveOrInPlace(_:)
함수 입니다. 이 두 함수의 관계는 이전에 설명한 다른 함수들과 동일하며 배타적 교집합을 그림으로 표현하면 다음과 같습니다.
예제 집합에서 배타적 교집합은 집합 A와 집합 B에 속하면서 양쪽에 공통으로 속하지 않는 요소들을 뜻합니다.
let uniqueCharacters = forceAwakens.exclusiveOr(returnOfTheJedi)
같은 방식으로exclusiveOrInPlace(_:)
함수의 역할은 동일하며 다만 원본 집합을 변경합니다.
var forceAwakens5 = forceAwakens
forceAwakens5.exclusiveOrInPlace(returnOfTheJedi)
집합의 차 (Substract)
마지막으로substract(_:)
와substractInPlace(_:)
함수입니다. 다음 그림처럼 두 집합의 차를 처리합니다.
let uniqueToForceAwakens = forceAwakens2.subtract(returnOfTheJedi)
마찬가지 원리로substractInPlace(_:)
함수의 예는 다음과 같습니다.
var forceAwakens6 = forceAwakens
forceAwakens6.subtractInPlace(returnOfTheJedi)
이상으로 스위프트의Set
에 대해서 정리를 마치겠습니다.