Apple 제공 Swift 프로그래밍 가이드(4.2)의 Collection Types 부분을 공부하며 정리한 글입니다. 개인적인 생각, 이해를 돕기 위한 예제도 조금 들어가있습니다.


들어가며

Swift 에서는 값을 저장하기 위한 세 가지 Primary Collection type 을 제공합니다. Array, Set, Dictionary 입니다.

  • Array : 넣은 순서대로 저장되는 콜렉션

  • Set : 값이 중복되지 않는 순서없는 콜렉션

  • Dictionary : key-value 관계를 가지는 순서없는 콜렉션


이 콜렉션 타입들 역시 다른 상수 변수와 마찬가지로 type 체크에 엄격합니다. 즉 한번 저장할 값의 type 을 정하면 다른 type 의 값은 저장할 수 없습니다. (따라서 String, Int 같은 각기 다른 타입의 값들을 하나의 콜렉션에 저장하고 싶은 경우에는 Any 타입을 사용합니다. Any 타입에 대해서는 나중에 자세히 다룹니다.)

NOTE

 Array, Set, Dictionary 모두 generic collections 로 구현되었습니다. Generic type 과 콜렉션에 대해서는 Generics 에서 자세히 다룹니다.



Mutability of Collections

Array, set, dictionary 를 변수에 할당하면 mutable 이 되고 상수에 할당하면 immutable 이 됩니다.

  • mutable: 생성한 후에도 자유롭게 아이템을 추가/삭제할 수 있음

  • immutable: 생성한 후 콜렉션의 사이즈와 내용을 변경할 수 없음 (아이템 추가/삭제 불가)

NOTE

변경될 일이 없는 콜렉션은 immutable 로 만드는 것이 좋습니다. 코드가 좀 더 좋아질 뿐만 아니라 Swift 컴파일러의 최적화를 가능하게 해줍니다.




Arrays

Array 는 같은 타입의 값을 들어온 순서대로 저장합니다. 따라서 인덱스는 달라도 값은 같은 경우가 발생할 수 있습니다.

NOTE

Swift’s Array type is bridged to Foundation’s NSArray class.

For more information about using Array with Foundation and Cocoa, see Bridging Between Array and NSArray.



Array Type Shorthand Syntax

Array<Element> 타입을 [Element] 로 축약해서 나타낼 수 있습니다. (이때, Element 는 Array에 저장할 값의 타입)


Creating an Array

// 1. Creating an Empty Array
 
 // initializer syntax를 사용하여 Empty Array 생성하기
 
 var someInts = [Int]()
 
 print("someInts is of type [Int] with \(someInts.count) items.")
 
 // 출력결과: "someInts is of type [Int] with 0 items.”
 
 
 
 someInts.append(3)
 
 // 이제 someInts 에는 Int 타입의 값 1개가 들어있음
 
 someInts = []
 
 // 이제 someInts 는 empty Array
 
 // 그러나 someInts 는 여전히 [Int] 타입
 
 
 
 
 
 // 2. Creating Array with a Default Value
 
 // 특정 값이 N개 들어있는 Array 생성하기
 
 var threeDoubles = Array(repeating: 0.0, count: 3)
 
 // threeDoubles 는 [Double] 타입이며, equals [0.0, 0.0, 0.0]
 
 
 
 
 
 // 3. Creating an Array by Adding Two Arrays Together
 
 // 더하기 연산자(+)를 이용하여 두 Array 를 더해 새로운 Array 생성하기
 
 // 이때 두 Array 의 타입에 따라 새로운 Array 의 타입이 유추됨
 
 var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
 
 // anotherThreeDoubles 는 [Double] 타입이며, equals [2.5, 2.5, 2.5]
 
 
 
 var sixDoubles = threeDoubles + anotherThreeDoubles
 
 // sixDoubles 의 타입은 [Double] 로 유추되며, equals [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]
 
 
 
 // 참고로 저장하는 타입이 다른 Array 끼리는 + 로 더하는 것이 불가능 (컴파일 에러)
 
 // 예를들어 [Double] 타입과 [Float] 타입의 배열은 + 를 통해 더할 수 없음
 
 // * 두 배열을 명시적으로 [Any] 타입으로 지정한다면 + 를 통해 더할 수 있음
 
 
 
 
 
 // 4. Creating an Array with an Array Literal
 
 // Array Literal 을 통해 Array 생성하기
 
 // * Array Literal: [값1, 값2, 값3] 형태로 값들을 콤마로 구분해 리스팅하고 대괄호로 묶기
 
 var shoppingList: [String= ["Eggs""Milk"]
 
 // shoppingList 은 두 개의 아이템들로 초기화 되었음
 
 
 
 // var shoppingList = ["Eggs", "Milk"]
 
 // 위와 같은 코드만 작성해도 shoppingList는 알아서 [String] 타입으로 유추됨
 
 // 두 아이템 "Eggs" 와 "Milk" 가 모두 String 타입이기 때문
 
 
 
 // var shoppingList = ["Eggs", 3]
 
 // 위 코드에서는 두 아이템 "Eggs" 와 3 의 타입이 다르기 때문에
 
 // shoppingList 의 타입 유추가 불가능 (컴파일 에러)
cs


Accessing and Modifying an Array

// 1. Array 안에 몇 개의 아이템이 있는지 알아보려면: count 프로퍼티 (read-only 속성임)
 
 print("The shopping list contains \(shoppingList.count) items.")
 
 // 출력: "The shopping list contains 2 items."
 
 
 
 
 
 // 2. Array 가 비어있는지 알아보려면: isEmpty 프로퍼티 (count == 0 일때 true)
 
 if shoppingList.isEmpty {
 
    print("The shopping list is empty.")
 
 } else {
 
    print("The shopping list is not empty.")
 
 }
 
 // 출력: "The shopping list is not empty.”
 
 
 
 
 
 // 3. Array 에 새로운 아이템을 맨 뒤에 추가하려면: append(_:) 메서드 호출
 
 shoppingList.append("Flour")
 
 // 이제 shoppingList 에는 3개의 아이템이 들어있으며, 팬케이크를 만들 수 있게 되었다!
 
 
 
 
 
 // 4. 연산자 += 를 이용해서 Array 에 다른 Array 를 더할 수도 있다
 
 // (물론 저장되는 값의 타입은 같아야 함)
 
 shoppingList += ["Baking Powder"]
 
 // 이제 shoppingList 에는 4개의 아이템이 들어있음
 
 shoppingList += ["Chocolate Spread""Cheese""Butter"]
 
 // 이제 shoppingList 에는 7개의 아이템이 들어있음
 
 
 
 
 
 // 5. Subscript 문법을 이용해서 Array 의 특정 값에 접근하려면:
 
 // Array 의 이름 뒤에 대괄호를 붙이고 그 안에 원하는 값의 인덱스를 넘기기
 
 var firstItem = shoppingList[0]
 
 // firstItem == "Eggs”
 
 // 참고로 Swift 의 Array 는 인덱스 0부터 시작한다. (1이 아님!)
 
 
 
 
 
 // 6. Subscript 문법을 이용해서 Array 의 특정 인덱스에 들어있는 값을 변경할 수 있다
 
 shoppingList[0= "Six eggs"
 
 // 이제 shoppingList 의 첫 번째 아이템은 "Eggs" 가 아닌 "Six eggs"
 
 
 
 // 인덱스를 통해 Array 에 접근할 때는 그 인덱스가 반드시 유효해야 한다!
 
 // 예를들어 shoppingList[shoppingList.count] = "Salt" 같은 코드는 런타임 에러를 발생시킨다
 
 // (* Array 의 맨 마지막 아이템에 해당하는 인덱스는 count-1)
 
 
 
 
 
 // 7. Subscript 문법을 이용해서 Array 의 특정 Range 를 특정 Array 로 교체할 수 있다
 
 // 개수는 일치하지 않아도 된다 (3개의 아이템이 들어있는 Range 를 2개의 아이템이 들어있는 Array 로 교체 가능)
 
 // * 당연히 타입은 일치해야 한다
 
 shoppingList[4...6= ["Bananas""Apples"]
 
 // 이제 shoppingList 는 6개의 아이템이 들어있음 (아이템 3개가 새로운 아이템 2개로 교체됨)
 
 
 
 // 보충설명
 
 var array:[Int= [0123456]
 
 array[2...5= [9// array는 [0, 1, 9, 6] 이 된다.
 
 array[0...4= [01234// 이런 식으로 범위를 넘은 Range 를 지정하면 런타임 에러 발생!
 
 // 값이 들어있는 인덱스에 대한 접근만 array[N] = value 또는 array[n...m] = [x, x, x] 문법 허용
 
 
 
 
 
 // 8. Array의 특정 위치에 새로운 아이템을 삽입하려면: insert(_:at:) 메서드
 
 shoppingList.insert("Maple Syrup", at: 0)
 
 // 이제 shoppingList 에는 7개의 아이템이 들어있음
 
 // "Maple Syrup" 은 shoppingList 의 맨 첫 번째 아이템
 
 
 
 // 보충설명
 
 array = [013]
 
 array.insert(2, at: 2// 2번 인덱스 자리에 2를 끼워넣겠다
 
 // [0, 1, 2, 3]
 
 
 
 array = [012]
 
 array.insert(3, at: 3// 3번 인덱스 자리에 3을 끼워넣겠다
 
 // [0, 1, 2, 3]
 
 
 
 array = [012]
 
 array.insert(4, at: 4// 4번 인덱스 자리에 4를 끼워넣겠다
 
 // 그러나 3번 인덱스에 값이 없으므로, 런타임 에러
 
 
 
 
 
 // 9. Array의 특정 위치에 있는 값을 삭제하려면: remove(at:) 메서드
 
 let mapleSyrup = shoppingList.remove(at: 0)
 
 // remove 메서드는 지정한 인덱스의 아이템을 remove하고 그 아이템의 값을 return 한다
 
 // mapleSyrup 상수에 들어있는 값은 shoppingList Array의 첫 번째 아이템이었던 "Maple Syrup"
 
 
 
 // 이제 shoppingList 에는 6개의 아이템이 들어있음
 
 
 
 firstItem = shoppingList[0]
 
 // "Six eggs”
 
 
 
 
 
 // 10. Array 의 맨 마지막 값을 삭제하려면: removeLast() 메서드
 
 let apples = shoppingList.removeLast()
 
 // apples 상수에 들어있는 값은 shoppingList 의 맨 마지막 아이템이었던 "Apples"
 
 // 가장 마지막 값을 삭제할 때는 remove(at:) 메서드보다 removeLast() 가 편리하다 (인덱스를 넘길 필요가 없으니)
cs


NOTE

Array 에 실제로 들어있는 아이템의 범위를 넘어선 인덱스를 통해 Array 의 아이템에 접근하려고 하면 런타임 에러가 발생하게 됩니다. 따라서 미리 인덱스의 유효성을 체크하는 것이 좋습니다. Array 의 count 프로퍼티를 확인해보세요. 유효한 인덱스 중 가장 큰 값은 count - 1 인 값입니다. Swift 에서는 Array 의 인덱스가 1이 아닌 0부터 시작하기 때문입니다. 만약 count 가 0 이라면 그 Array 에 유효한 인덱스는 없습니다. Empty Array 라는 뜻이기 때문입니다.



Iterating Over an Array

for-in 루프를 사용하여 Array 안의 모든 값을 순회할 수 있습니다:

for item in shoppingList {
    print(item)
}
// Six eggs
// Milk
// Flour
// Baking Powder
// Bananas
cs


배열의 값만이 아니라 인덱스도 순회가 필요한 경우에는 enumerated() 메서드를 사용하세요. (인덱스, 값) 쌍의 튜플을 순회할 수 있습니다.

for (index, value) in shoppingList.enumerated() {
    print("Item \(index + 1): \(value)")
}
// Item 1: Six eggs
// Item 2: Milk
// Item 3: Flour
// Item 4: Baking Powder
// Item 5: Bananas
cs


for-in 루프에 대한 더 자세한 정보는 For-In Loops 문서를 참조하세요.



Sets

Set 은 같은 타입의 값을, 중복없이, 순서없이 저장합니다. 저장하는 순서는 안 지켜져도 되지만 콜렉션 안의 값들이 유니크해야 할 때 Set 을 사용하세요.

NOTE

Swift’s Set type is bridged to Foundation’s NSSet class.


Hash Values for Set Types

Set 에 저장할 아이템의 타입은 반드시 hashable 해야 합니다.

  • Hashable 한 타입이란: 값의 hash value 를 계산해 제공할 수 있는 타입을 말함

  • Hash Value 란: a == b 일 때, a.hashValue == b.hashValue 를 만족하는 Int 값으로, 동일한 값은 항상 동일한 hash value 를 가짐

Swift의 모든 기본 타입(String, Int, Double, Bool 등)은 default로 hashable 합니다. 즉, 이런 타입들은 Set 에 저장될 수 있고 Dictionary 의 key 값도 될 수 있습니다. Int나 Bool 타입이 Dictionary Key로 쓰일 수 있다는 점이 Objective-C와 비교가 되네요. 참고로 Enum Case 의 값도 기본적으로 hashable 합니다(단, associated value 가 없는 경우).

NOTE

Custom Type을 Set 에 저장하거나 Dictionary 의 Key 타입으로 사용하고 싶다면 Hashable 프로토콜을 구현해야 합니다. 참고로 Hashable 프로토콜은 Equatable 프로토콜을 따르고 있으므로 equals 연산자 (==) 구현도 필요합니다.

Hashable 프로콜을 따르기 위해 필요한 구현을 정리하면

  • Int 타입의 hashValue 프로퍼티 getter

  • equals 연산자(==)를 구현하며 다음 세 조건을 만족

  1. a == a (Reflexivity)

  2. a == b 를 만족할 때, b == a (Symmetry)

  3. a == b && b == c 를 만족할 때, a == c (Transitivity)

프로토콜을 구현하는 방법에 대해 더 알고 싶다면 Protocols 문서를 참조하세요.



Set Type Syntax

Set<Element> 로 Set 을 만들 수 있습니다. 이때 Element 는 Set 에 저장될 아이템의 타입입니다. Set 은 Array 와 다르게 shorthand form 이 없습니다. 예를들어 var intArray: [Int] 는 Int 타입을 저장하는 Array 를 만들어주지만, 이와 유사하게 var intSet: <Int> 를 시도해보면 컴파일 에러가 발생합니다.


Creating and Initializing a Set

// 1. Empty Set 생성
 
 var letters = Set<Character>()
 
 print("letters is of type Set<character> with \(letters.count) items.")
 
 // 출력 "letters is of type Set<character> with 0 items.”
 
 
 
 letters.insert("a")
 
 // 이제 letters 에는 1개의 값(Character 타입)이 들어있음
 
 
 
 // 이미 Set 타입임을 알고 있는 경우에는 빈 Array 리터럴을 할당해 Empty Set 으로 만들 수도 있다
 
 letters = []
 
 // 이제 letters 는 Eempty Set 이고, 여전히 타입은 Set<Character> 이다
 
 
 
 
 
 // 2. Array 리터럴을 이용하여 Set 생성
 
 var favoriteGenres: Set<String> = ["Rock""Classical""Hip hop"]
 
 // favoriteGenres 는 3개의 아이템들로 초기화되었다
 
 
 
 var favoriteGenres2: Set = ["Rock""Classical""Hip hop"]
 
 // 위와 같이 저장할 아이템의 타입을 명시하지 않아도, Array 리터럴을 통해 String 타입이 저장될 Set 이라는 것을 유추해준다
 
 // 단, Set 이라고 명시하지 않으면 이 변수는 Array 가 되니 조심하자
cs



Accessing and Modifying a Set

// 1. Set 안에 몇 개의 Item이 있는지 알고 싶으면: count 프로퍼티 (read-only)
print("I have \(favoriteGenres.count) favorite music genres.")
// 출력 "I have 3 favorite music genres.”
 
 
// 2. Set 안이 비어있는지 알고 싶으면: isEmpty 프로퍼티 (count == 0 일때 true)
if favoriteGenres.isEmpty {
   print("As far as music goes, I'm not picky.")
else {
   print("I have particular music preferences.")
}
// 출력 "I have particular music preferences."
 
 
// 3. Set 에 Item 을 추가하려면: insert(_:) 메서드
favoriteGenres.insert("Jazz")
// 이제 favoriteGenres 에는 4개의 아이템이 들어있음
 
 
// 4. Set 에서 Item 을 제거하려면: remove(_:) 메서드
// 이 메서드는 Set 에서 Item 을 지우고 그 값을 리턴한다
// 만약 Item 이 Set 안에 없었다면 nil 을 리턴한다
// Set 안의 모든 아이템들을 한번에 지우려면 removeAll() 메서드를 사용할 것
if let removedGenre = favoriteGenres.remove("Rock") {
   print("\(removedGenre)? I'm over it.")
else {
   print("I never much cared for that.")
}
// 출력 "Rock? I'm over it.”
 
 
// 5. 특정 Item 이 Set 안에 들어있는지 알고 싶으면: contains(_:) 메서드
if favoriteGenres.contains("Funk") {
   print("I get up on the good foot.")
else {
   print("It's too funky in here.")
}
// 출력 "It's too funky in here."
cs



Iterating Over a Set

for genre in favoriteGenres {
    print("\(genre)")
}
// Classical
// Jazz
// Hip hop
 
 
// Set 안의 Item을 특정 순서로 순회하고 싶다면 sorted() 메서드를 사용
// Item을 < 연산자로 sorting 한 array가 반환됨
for genre in favoriteGenres.sorted() {
    print("\(genre)")
}
// Classical
// Hip hop
// Jazz
cs



Performing Set Operations

기본적인 Set Operation 을 쉽게 수행하는 방법에 대해 알아보겠습니다. 기본적인 Set Operation 의 예를 들어보면, 두 Set 을 합친다거나, 두 Set 에서 공통으로 가지는 값을 찾는다거나, 등등이 있습니다.

Fundamental Set Operations


위 그림에서의 ab 는 각각 Set 입니다. 각 벤다이어그램 위에 표기된 메서드는 벤다이어그램의 초록색 부분에 해당하는 Item 들로 구성된 새로운 Set 을 생성하여 리턴합니다. 자세한 설명은 생략하고 예제로 대체합니다.

let oddDigits: Set = [13579]
let evenDigits: Set = [02468]
let singleDigitPrimeNumbers: Set = [2357]
 
oddDigits.union(evenDigits).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
oddDigits.intersection(evenDigits).sorted()
// []
oddDigits.subtracting(singleDigitPrimeNumbers).sorted()
// [1, 9]
oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted()
// [1, 2, 9]
cs


Set Membership and Equality

위 그림에서 a, b, c 는 각각 Set 이며 absuperset, basubset, bc 는 서로에게 disjoint 한 관계입니다.

  • "is equal" operator (==): 두 Set 의 내용이 전부 동일한지? (Item의 type, count, value 모두)

  • isSubset(of:): subset 인지?

  • isSuperset(of:): superset 인지?

  • isStrictSubset(of:): subset이면서, subset != superset 인지?

  • isStrictSuperset(of:): superset이면서, subset != superset 인지?

  • isDisjoint(with:): 두 Set 간의 교집합이 없는지?

let houseAnimals: Set = ["🐶""🐱"]
let farmAnimals: Set = ["🐮""🐔""🐑""🐶""🐱"]
let cityAnimals: Set = ["🐦""🐭"]
 
houseAnimals.isSubset(of: farmAnimals)
// true
farmAnimals.isSuperset(of: houseAnimals)
// true
farmAnimals.isDisjoint(with: cityAnimals)
// true
 
 
// 추가설명
let myAnimals: Set = ["🐮""🐔"]
let myFriendAnimals: Set = ["🐮""🐔""🐑""🐶""🐱"]
 
myAnimals.isStrictSubset(of: farmAnimals)
// true
myFriendAnimals.isStrictSubset(of: farmAnimals)
// false
cs




Dictionaries

Dictionary 는 key-value 쌍을 순서없이 저장합니다. 이때 key 가 되는 타입끼리는 서로 같아야 하며 value 가 되는 타입끼리도 서로 같아야 합니다. Key 는 그 각각이 유니크하며 value 를 위한 identifier 역할을 합니다. 따라서 key 와 value 는 1:1 관계를 가집니다. 사전처럼 사용할 콜렉션 타입이 필요할 때 Dictionary 를 사용하면 유용합니다.

NOTE

Swift’s Dictionary type is bridged to Foundation’s NSDictionary class.


Dictionary Type Shorthand Syntax

Swift 에서 Dictionary 를 쓸 때는 Dictionary<Key, Value> 로 씁니다. 이때 Key 는 Dictionary 에서 Key 로 쓰일 타입이며 Value 는 Key 와 1:1 쌍으로 저장될 값의 타입을 뜻합니다. 이 폼은 [Key: Value] 로 축약할 수 있으며 이 축약형이 더 선호됩니다.

NOTE

Dictionary 의 Key 가 될 타입은 Hashable 해야 합니다. Set 에 저장될 타입처럼요. 이 둘은 유니크한 값인지를 따질 수 있어야 한다는 공통점이 있지요.


Creating a Dictionary

// 1. Creating an Empty Dictionary
var namesOfIntegers = [IntString]()
// namesOfIntegers 는 [Int: String] 타입의 Empty Dictionary
 
namesOfIntegers[16= "sixteen"
// namesOfIntegers 에는 이제 하나의 key-value 쌍이 들어있음
namesOfIntegers = [:]
// namesOfIntegers 는 다시 한 번 [Int: String] 타입의 Empty Dictionary 가 됨
 
 
// 2. Creating a Dictionary with a Dictionary Literal
var airports: [StringString= ["YYZ""Toronto Pearson""DUB""Dublin"]
 
// 아래와 같이만 써도 알아서 [String: String] 타입을 유추한다
var airports = ["YYZ""Toronto Pearson""DUB""Dublin"]
 
cs


Accessing and Modifying a Dictionary

// 1. 아이템 개수를 알아보고 싶으면: count 프로퍼티 (read-only)
print("The airports dictionary contains \(airports.count) items.")
// 출력 "The airports dictionary contains 2 items."
 
 
// 2. 비어있는지 알아보고 싶으면: isEmpty 프로퍼티 (count == 0 이면 true)
if airports.isEmpty {
    print("The airports dictionary is empty.")
else {
    print("The airports dictionary is not empty.")
}
// 출력 "The airports dictionary is not empty."
 
 
// 3. subscript syntax 를 사용해서 아이템 삽입 및 갱신하기
airports["LHR"= "London"
// airports 에는 이제 3개의 아이템이 들어있음
 
airports["LHR"= "London Heathrow"
// Key "LHR" 와 쌍을 이루는 Value 는 이제 "London Heathrow"
 
 
// 4. Subscript Syntax 안쓰고 아이템 삽입 및 갱신하기: updateValue(_:forKey:) 메서드
// dictionary[key] = value 와 같은 동작을 하지만 리턴값이 존재한다
// key 와 1:1 쌍을 이루는 value 가 이미 존재한다면 그 value 를 리턴하고
// 없다면 nil 을 리턴한다 (따라서 updateValue 메서드의 리턴 타입은 optional)
if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
    // 리턴값이 있다는 것은 즉 이미 존재하던 값에서 새로운 값으로 대체되었음을 말함
    print("The old value for DUB was \(oldValue).")
}
// 출력 "The old value for DUB was Dublin."
 
 
// 5. Subscript Syntax 를 사용해서 특정 key 와 1:1 쌍을 이루는 value 에 접근하기
if let airportName = airports["DUB"] {
    print("The name of the airport is \(airportName).")
else {
    // key 에 해당하는 value 가 없다면 nil 을 반환한다
    print("That airport is not in the airports dictionary.")
}
// 출력 "The name of the airport is Dublin Airport.”
 
 
// 6. 특정 아이템 지우기 (key-value 쌍 자체를 지우기)
airports["APL"= "Apple International"
// "Apple International" 은 실제 공항이 아니니까 지워버리자!
airports["APL"= nil
// "APL"-"Apple International" 쌍은 이제 Dictionary 에서 지워졌다
 
 
// 7. 특정 아이템을 지우고 싶으면: removeValue(forKey:) 메서드
// 위에서 수행했던 dictionary[key] = nil 과 같은 동작을 하지만 리턴값이 존재한다
// key 와 1:1 쌍을 이루는 value 가 이미 존재한다면 그 value 를 리턴하고
// 없다면 nil 을 리턴한다 (따라서 removeValue 메서드의 리턴 타입은 optional)
if let removedValue = airports.removeValue(forKey: "DUB") {
    print("The removed airport's name is \(removedValue).")
else {
    print("The airports dictionary does not contain a value for DUB.")
}
// 출력 "The removed airport's name is Dublin Airport.”
cs


Iterating Over a Dictionary

for-in 루프를 통해 Dictionary 의 key-value 쌍을 순회할 수 있습니다. Dictionary 의 아이템 각각은 (key, value) 튜플을 반환하며, 이 튜플의 멤버를 분해하여 임시 상수/변수에 할당할 수 있습니다.

for (airportCode, airportName) in airports {
    print("\(airportCode): \(airportName)")
}
// YYZ: Toronto Pearson
// LHR: London Heathrow
cs


for-in 루프에 대해 더 알고 싶다면 For-In Loops 문서를 참조하세요.

Dictionary 의 key 리스트, 혹은 value 리스트만 순회하고 싶다면 keys, values 프로퍼티를 사용하세요.

for airportCode in airports.keys {
    print("Airport code: \(airportCode)")
}
// Airport code: YYZ
// Airport code: LHR
 
for airportName in airports.values {
    print("Airport name: \(airportName)")
}
// Airport name: Toronto Pearson
// Airport name: London Heathrow
cs


Dictionary 의 key 리스트나 value 리스트만을 Array 인스턴스로 사용할 일이 있다면, keys 나 values 프로퍼티를 사용해 Array 를 새로 초기화하도록 하세요.

let airportCodes = [String](airports.keys)
// airportCodes is ["YYZ", "LHR"]
 
let airportNames = [String](airports.values)
// airportNames is ["Toronto Pearson", "London Heathrow"]
cs


Swift 에서 Dictionary 타입은 순서를 가지지 않습니다. 만약 특정한 순서로 key 리스트나 value 리스트를 순회하기 원한다면 keysvalues 프로퍼티에 sorted() 메서드를 사용하세요.

// <참고> 뒷장에 나오는 부분인데 그냥 맛보기로. 클로저 모르시면 넘어가세요!
 
var dict: [StringString= ["b""bValue""a""aValue"]
 
// Dictionary 의 소팅은 이런 식으로 할 수 있다
let sortedDict = dict.sorted { (first, second) -> Bool in
    return first.key < second.key
}
// 위 코드에서의 first, second는 Dictionary 의 아이템(튜플 타입, key-value 쌍)
// 함수의 맨 마지막 파라미터로 클로저가 들어오면 괄호는 생략가능해서 이런 폼이 되었음
// sortedDict 에는 이제 ("a", "aValue") ("b", "bValue") 순서로 들어있다
 
// 또한 위 코드를 아래와 같이 축약할 수 있다
let sortedDict = dict.sorted { $0.0 < $1.0 }
// $0와 $1는 각각 첫 번째와 두 번째로 들어오는 파라미터
// return은 생략가능
cs



'Swift 4.2' 카테고리의 다른 글

Collection Types  (0) 2018.09.05
Strings and Characters  (0) 2018.06.20
Basic Operators  (0) 2018.03.23
The basics  (0) 2018.03.02
A Swift Tour  (0) 2017.10.17
Version Compatibility  (0) 2017.10.04

Apple 제공 Swift 프로그래밍 가이드(4.2)의 Strings and Characters 부분을 공부하며 정리한 글입니다. 개인적인 생각, 이해를 돕기 위한 예제도 조금 들어가있습니다.


들어가며

String 은 문자열을, Character 는 문자를 저장할 수 있는 타입입니다. Swift String 은 단순한 문법으로 다룰 수 있으며, 그럼에도 불구하고 빠릅니다. 특히 String 두 개를 연결할 때 + 연산자만 사용하면 되는데, 이 부분은 Objective-C 에 비하면 참 편해졌습니다.

String 에 특정 상수/변수 등을 포함해야 할 때는 Objective-C 와 다르게 String Literal 안에 \(변수이름)을 넣어야 합니다. 이게 무슨 말인지는 String Interpolation 부분에서 자세히 알려드리겠습니다.

Swift 에서의 모든 String 은 encoding-independent Unicode character 로 구성되어 있으며, 유니코드 호환도 잘 됩니다.

NOTE

Swift’s String type is bridged with Foundation’s NSString class. Foundation 모듈이 String 의 extension 을 구현해놓았기 때문에, 현재는 Foundation 만 import 하면 String 으로도 NSString 의 메서드들을 사용할 수 있습니다.

Foundation 과 Cocoa 사이에서의 String 사용 방법에 대한 더 자세한 정보는 Bridging Between String and NSString 을 참고하세요.




String Literals

String Literal 이란 double quotes( )로 둘러싸인 문자열을 말합니다. 이것을 통해 미리 String 값을 define 해놓고, String 상수/변수에 그 값을 할당할 수 있습니다.

let someString = "Some string literal value" // String Literal로 초기화된 변수는 String 타입 (타입유추됨)
let lenghtOneString = "a" // 이 경우도 변수는 String 타입. Character 타입이라고 명시하면 Character 타입이 된다.
cs


Multiline String Literals

만약 여러 라인으로 나눠서 입력할 String Literal 이 필요하다면 그 시작과 끝을 three double quotation marks( """ )로 감싸세요.

let quotation = """
The White Rabbit put on his spectacles.  "Where shall I begin,
please your Majesty?" he asked.
 
"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
 
 
let singleLineString = "These are the same."
let multilineString = """
These are the same.
"""
 
 
// backslash (\) 를 써주면 String 값에서는 그 라인에서 자동 줄바꿈되지 않습니다
let softWrappedQuotation = """
The White Rabbit put on his spectacles.  "Where shall I begin, \
please your Majesty?" he asked.
 
"Begin at the beginning," the King said gravely, "and go on \
till you come to the end; then stop."
"""
 
 
// 이 경우 앞뒤로 \n(라인 피드)가 들어갑니다
let lineBreaks = """
 
This string starts with a line break.
It also ends with a line break.
 
"""
cs


멀티라인 String 은 들여쓰를 할 수 있습니다. 들여쓰기를 하려면 마침큰따옴표(closing quotation marks, """ )전에 공백(whitespace)을 넣으세요. 그러면 다른 라인들도 그만큼의 공백은 String 값에 포함되지 않고 그냥 무시됩니다.


Special Characters in String Literals

String literal 은 다음의 특별한 문자를 포함할 수 있습니다:

    • escaped special characters: \0 (null character), \\ (backslash), \t (horizontal tab), \n (line feed), \r (carriage return), \" (double quotation mark) and \' (single quotation mark)

    • arbitrary Unicode scalar: \u{n}, where n is a 1–8 digit hexadecimal number with a value equal to a valid Unicode code point

예제를 봅시다. 

let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"
// "Imagination is more important than knowledge" - Einstein
let dollarSign = "\u{24}"        // $,  Unicode scalar U+0024
let blackHeart = "\u{2665}"      // ♥,  Unicode scalar U+2665
let sparklingHeart = "\u{1F496}" // 💖, Unicode scalar U+1F496
 
let threeDoubleQuotationMarks = """
Escaping the first quotation mark \"""
Escaping all three quotation marks \"\"\"
"""
cs




Initializing an Empty String

빈 문자열이 들어있는 String 타입 변수를 만드는 방법을 알아봅시다:

var emptyString = "" // empty string literal을 할당하거나
var anotherEmptyString = String()  // 초기화 문법을 사용하거나
// 두 String 모두 똑같이 비어있다.
cs


String 값이 비어있는지 확인하려면 Boolean 타입인 isEmpty 프로퍼티를 체크해보세요:

if emptyString.isEmpty {
    print("Nothing to see here")
}
// Prints "Nothing to see here"
cs




String Mutability

변수는 값 변경 가능, 상수는 값 변경 불가의 룰이 똑같이 적용됩니다:

var variableString = "Horse"
variableString += " and carriage"
// variableString is now "Horse and carriage"
 
let constantString = "Highlander"
constantString += " and another Highlander"
// this reports a compile-time error - a constant string cannot be modified
cs


Swift 에서는 이처럼 String 이라는 타입이 하나 있고, 그 타입을 상수로 만들지 변수로 만들지에 따라 Mutability가 결정됩니다. 이것은 NSString / NSMutableString 등 변수의 타입 자체로 Mutability를 결정했던 Objective-C & Cocoa 와는 다른 방식입니다.



Strings Are Value Types

Swift 의 StringValue type 입니다. 따라서 함수나 메서드로 전달되거나 새로운 상수나 변수로 할당될 때에 그 String 값은 복사 됩니다. 이 경우 String 의 참조가 아닌 그 안에 들어있는 값만 새로 복사되어 전달되는 것이기 때문에, 복사된 값을 변경해도 원본은 영향받지 않습니다. 이 Value type 이라는 것은 Structures and Enumerations Are Value Types 에서 자세히 다루게 될 텐데, 값 복사본이 전달된다는 특성을 이해하고 있어야 앞으로 원하는 코드를 작성할 수 있을 것입니다. (NSString 의 경우에는 참조가 전달되기 때문에, 기존에 Objective-C 개발을 했던 경우 참조 전달과 착각하여 실수를 하기 쉽습니다.)

<추가설명1 - Value type 의 복사에 대해> Value type 의 할당을 할 때는 값 복사가 일어난다고 했습니다. 그러나 엄밀히 말하면 할당을 하는 시점마다 항상 복사가 일어나는 것은 아닙니다. 일단 복사는 하지않고 같은 곳만 바라보고 있다가, 값이 변경될 때에 복사를 하게 됩니다. 이것은 Swift 의 컴파일러가 최적화를 해주기 때문인데요. 왜 이런 식으로 동작하는 것일까요? 예를들어 Value type 중 하나인 Array 를 생각해보겠습니다. 만약 용량이 상당히 큰 Array 가 있는데, 이것을 함수 파라미터로 넘기거나 할 때마다 항상 Array 전체 복사가 일어난다면 성능이 많이 떨어질 것입니다.

<추가설명2 - Value type 과 Reference type 의 차이점 한 가지> Value type 은 var 로 선언될 경우에만 내부값 변경이 가능하고, Reference type 은 var 와 let 어느 것으로 선언되든지 내부값 변경이 가능합니다. 예를 들어 다음과 같은 Struct 하나를 떠올려봅시다: 이 Struct 에는 Name 이라는 내부 프로퍼티가 하나 있고 Name 은 var 로 선언되어 있습니다. 단순히 생각해보면 Name 을 언제든 바꿀 수 있을 것 같은 생각이 듭니다. 그러나 Struct 는 Value type 입니다. 따라서 이 Struct 가 만약 let 으로 선언되었다면 Name 프로퍼티는 변경이 불가능합니다. (반면 Reference type 의 경우에는 var 와 let 어느 것으로 선언되더라도 내부값을 변경할 수 있습니다. 참고로 NSObject 를 상속받은 객체, 기타 Class로 선언된 객체들이 Reference type 에 속합니다.)



Working with Characters

Stringcharacters 프로퍼티를 사용하면 String 값의 각각의 문자(Character)에 접근할 수 있습니다다. for-in loop 를 이용하여 iterating 해봅시다:

for character in "Dog!🐶" {
    print(character)
}
// D
// o
// g
// !
// 🐶
cs


for-in loop 에 대해서는 For-In Loops 에 자세히 기술되어 있습니다.

상수/변수를 생성할 때 Character 타입임을 명시하고 single-character string literal 을 할당하면 Character 타입을 만들 수 있습니다:

let exclamationMark: Character = "!"
cs


Character 값들로 구성된 array 를 이니셜라이저에 넘겨서 String 타입을 만들 수 있습니다:

let catCharacters: [Character] = ["C""a""t""!""🐱"]
let catString = String(catCharacters)
print(catString)
// Prints "Cat!🐱"
cs




Concatenating Strings and Characters

Swift 의 String 값들은 +, += 연산자를 통해 서로 더할 수 있습니다. Character 값을 String 값에 더하려면 append() 메서드를 사용해야 합니다. 예제로 알아보겠습니다:

let string1 = "hello"
let string2 = " there"
var welcome = string1 + string2
// welcome now equals "hello there"
 
var instruction = "look over"
instruction += string2
// instruction now equals "look over there"
 
let exclamationMark: Character = "!"
welcome.append(exclamationMark)
// welcome now equals "hello there!"
 
// <주의> welcome += exclamationMark 는 서로 타입이 달라서 Compile error
cs


Character 타입은 오직 하나의 문자만 그 안에 담을 수 있습니다. 따라서 Character 타입의 변수에 다른 String 이나 Character 값을 더하는 것은 불가능합니다.

var str: String = "str"
var char: Character = "c"
 
let str_str = str + str
let char_char = char + char // compile error
let str_char = str + char // compile error
let str_append_char = str.append(char)
 
let str_let: String = "str_let"
let str_let_append_char = str_let.append(char) //compile error
cs


여러 줄의 string literal 을 더하는 경우에는 마지막 라인에 line break (\n) 가 포함되어 있느냐 없느냐에 따라 결과가 달라지는 경우가 있습니다. 예제로 알아봅시다:

let badStart = """
one
two
"""
let end = """
three
"""
print(badStart + end)
// Prints two lines:
// one
// twothree
 
let goodStart = """
one
two
 
"""
print(goodStart + end)
// Prints three lines:
// one
// two
// three
cs




String Interpolation

상수, 변수, String literal 을 조합하여 String 을 만들 수 있습니다. 넣고 싶은 아이템을 괄호 + 백슬러시( )로 묶습니다. 예를 들어 변수를 String literal 에 포함시키고 싶다면 \(변수이름) 형태로 String literal 내의 적절한 위치에 넣습니다.

let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
cs


Note

String literal 괄호 안의 수식에는 unescaped backslash( ), carriage return, line feed 등을 포함시킬 수 없습니다. 그러나 다른 String literal 을 포함시킬 수는 있습니다.




Unicode

String 은 사실 유니코드 스칼라(Unicode scalar) 값들로 이루어져 있습니다. 유니코드 스칼라는 Character 하나하나를 위한 유니크한 21-bit Number 입니다. 예를 들어 U+0061 for LATIN SMALL LETTER A ("a"), or U+1F425 for FRONT-FACING BABY CHICK ("🐥").

Swift 의 모든 Character 인스턴스는 하나의 extended grapheme cluster 를 가집니다. Extended grapheme cluster 란, 유니코드 스칼라의 sequence 인데, combine 되면 사람이 읽을 수 있는 하나의 문자가 됩니다. 다음 예제를 봅시다:

let eAcute: Character = "\u{E9}" // é 
let combinedEAcute: Character = "\u{65}\u{301}" // e followed by ́
// eAcute is é (유니코드 스칼라 1개), combinedEAcute is é (유니코드 스칼라 2개)
// 두 개는 같은 문자!
 
let precomposed: Character = "\u{D55C}"                  // 한
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}"   // ᄒ, ᅡ, ᆫ
// precomposed is 한, decomposed is 한
 
let enclosedEAcute: Character = "\u{E9}\u{20DD}"
// enclosedEAcute is é⃝
 
let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"
// regionalIndicatorForUS is 🇺🇸
 
cs




Counting Characters

String 을 구성하는 Character 개수를 알고 싶으면 String 의 count 프로퍼티를 사용합니다:

let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
print("unusualMenagerie has \(unusualMenagerie.count) characters")
// Prints "unusualMenagerie has 40 characters"
cs


참고로 Character 하나에 포함된 유니코드 스칼라가 2개 이상이라도 그와 관계없이 Character 하나 당 count 는 하나로 간주됩니다. count 프로퍼티는 Character 의 개수를 세는 것이지 유니코드 스칼라의 개수를 세는 것이 아닙니다.

var word = "cafe"
print("the number of characters in \(word) is \(word.count)")
// Prints "the number of characters in cafe is 4"
 
word += "\u{301}"    // COMBINING ACUTE ACCENT, U+0301
 
print("the number of characters in \(word) is \(word.count)")
// Prints "the number of characters in café is 4"
cs


위 예제에서 볼 수 있는 것처럼 두 String 이 눈에는 같은 String 처럼 보일지 몰라도 그것을 구성하는 유니코드 스칼라의 개수와 종류는 다를 수도 있습니다. 따라서 똑같은 것처럼 보이는(same string's representation) 두 String 이 서로 다른 양의 메모리를 차지하고 있는 경우도 있습니다.




Accessing and Modifying a String

String 의 값에 접근하거나 변경하는 방법을 알아봅시다. String 의 메서드, 프로퍼티, 서브스크립트 문법 등을 통해 가능합니다.


String Indices

String 값을 구성하는 Character 들 각각이 String 의 어느 위치에 있는지 가리키기 위하여 index typeString.Index 가 사용됩니다.

왜 Int 값으로 Character 의 위치에 접근하지 못하고 굳이 index type 을 써야할까요? 그 이유는 위에서 설명한 유니코스 스칼라에 있습니다. Character 마다 차지하는 메모리가 다를 수 있으며, 유니코드 스칼라를 몇 개씩 건너뛰어야 다음 Character 가 나올지 미리 알 수 없기 때문에 그 역할을 해주는 index type 을 사용하는 것입니다.


String 에는 index 를 위한 다음 프로퍼티가 있습니다.

  • startIndex : 첫 번째 Character 를 가리키는 index
  • endIndex : 마지막 Character의 한 칸 뒤 index. 따라서 endIndex 에서 한 칸 앞으로 와야 마지막 Character 를 가리키게 됩니다.
String 이 비어있을 경우, startIndex 와 endIndex 는 같습니다.

String 에는 Index 를 위한 다음 메서드가 있습니다.

  • index(before:) : 바로 전
  • index(after:) : 바로 뒤
  • index(_:offsetBy:) : 오른쪽 방향으로 N만큼 떨어져있는 곳(음수면 왼쪽으로 N만큼)


String 의 특정 인덱스에 있는 Character 에 접근하기 위하여 서브스크립트 문법을 사용할 수 있습니다.

let greeting = "Guten Tag!"
greeting[greeting.startIndex]
// G
greeting[greeting.index(before: greeting.endIndex)]
// !
greeting[greeting.index(after: greeting.startIndex)]
// u
let index = greeting.index(greeting.startIndex, offsetBy: 7)
greeting[index]
// a
cs


String 의 range 를 벗어난 index 에 접근하려고 하면 런타임 에러가 발생됩니다.
greeting[greeting.endIndex] // Error
greeting.index(after: greeting.endIndex) // Error
cs


String 의 indices 프로퍼티는 String 안의 Character 들에 대한 index 의 Range 를 만들어줍니다. 이 프로퍼티를 for 문과 사용하면 String 내의 각 문자를 순회하는 코드를 간결하게 작성할 수 있습니다.

for index in greeting.indices {
    print("\(greeting[index]) ", terminator: "")
}
// Prints "G u t e n   T a g ! " // 다시 한 번 강조하지만, 위 코드에서의 index 는 0, 1 등의 number type 이 아니라 index type!
cs

NOTE
프로퍼티 startIndexendIndex, 메서드 index(before:), index(after:), 그리고 index(_:offsetBy:)Collection 프로토콜을 따르기만 한다면 어떤 타입이든 사용가능합니다. String, Array, Dictionary, Set 등의 타입이 해당됩니다.


Inserting and Removing

String 의 특정 인덱스에 Character 하나를 삽입하려면 insert(_:at:) 메서드를 사용하세요. 그리고 특정 인덱스에 다른 String 을 삽입하려면 insert(contentsOf:at:) 메서드를 사용하세요.

// String 특정 위치에 Character 삽입하기
var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)
// welcome now equals "hello!"
 
// String 특정 위치에 다른 String 삽입하기
welcome.insert(contentsOf: " there", at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there!"
cs


반대로 특정 Character, String 을 제거하는 경우에는 remove(at:) 와 removeSubrange(_:) 메서드를 사용하세요.

// String에서 특정 위치의 Character 지우기
welcome.remove(at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there"
 
// String에서 sub string 지우기
let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
welcome.removeSubrange(range)
// welcome now equals "hello"
cs


NOTE

RangeReplaceableCollection 프로토콜을 따르는 어떤 타입이건 insert(_:at:), insert(contentsOf:at:), remove(at:)removeSubrange(_:) 메서드를 사용할 수 있습니다. String, Array, Dictionary, Set 등 모두 포함됩니다.





Substrings

String 의 서브 스트링을 가져올 때—예를 들어, 서브스크립트를 사용하거나 prefix(_:) 같은 메서드를 사용해서—그 결과는 Substring 인스턴스입니다. 또 다른 String 이 아니라요. Swift 의 Substring 은 String 의 메서드와 똑같은 메서드들을 많이 가지고 있습니다. 즉 String 을 다룰 때와 같은 방식으로 Substring 을 다룰 수 있습니다. 그러나 Substring 은 String 을 가지고 특정 액션을 수행하는 짧은 시간에서만 사용하도록 하세요. 만약 긴 시간동안 Substring 을 저장하고 사용해야 한다면 Substring 을 String 의 인스턴스로 변환한 다음 그렇게 하세요. 예제를 보겠습니다:


let greeting = "Hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning = greeting[..<index]
// beginning is "Hello"
 
// Convert the result to a String for long-term storage.
let newString = String(beginning)
cs


Substring 도 String 과 마찬가지로 자신을 구성하는 문자열들이 저장되는 메모리를 참조하고 있습니다. 그러나 String 과 Substring 은 성능 최적화 측면에서 달라지는데요, Substring 은 Original String 을 저장하기 위해 사용되는 메모리의 일부를 재사용할 수 있습니다. 또한 다른 Substring 을 저장하기 위해 사용되는 메모리의 일부를 재사용할 수도 있습니다. (String 역시 비슷한 최적화가 이루어지지만, String 의 경우에는 두 개의 String 이 같은 메모리를 공유한다면 이 두 개의 String 은 서로 동일합니다(equal 관계에 놓여 있습니다).) 이런 성능 최적화의 효과로, String 이나 Substring 의 값을 수정하기 전까지는 메모리 복사가 이루어지지 않습니다. 즉 메모리 복사에 대한 자원을 절약할 수 있습니다. 앞서 언급했던 것처럼 Substring 은 긴 시간 사용하기에는 적절하지 않습니다(not suitable for long-term storage). 왜냐하면 Substring 은 Original String 의 메모리를 재사용하기 때문에 Substring 이 사용되는 동안에는 이 Original String 이 통째로 메모리에 남아 있어야 하기 때문입니다.


위 예제에서 greeting 은 String 입니다. 따라서 greeting 은 자신을 구성하는 문자열들이 저장되는 메모리를 참조하고 있습니다. beginninggreeting 의 Substring 이기 때문에, beginninggreeting 이 사용하는 메모리를 재사용합니다. 그와 다르게 newString 은 String 입니다. 따라서 newString 은 자신만의 메모리를 할당받습니다. 아래 그림은 이런 관계들을 보여주고 있습니다:


NOTE

StringSubstring 둘 다 StringProtocol 을 따르고 있습니다. 그러니 String 값을 조작하는 함수에 이 타입들을 편리하게 사용할 수 있습니다.




Comparing Strings


String 비교에는 세 가지 종류가 있습니다.

  • String and Character Equality : 문자열 전체 비교. 참고로 두 Character의 유니코드 스칼라의 조합에 차이가 나더라도 결과적으로 외관과 의미가 동일한 Character라면 두 Character는 같다고 취급.
  • prefix equality : 앞이 일치하는지
  • suffix equality : 뒤가 일치하는지

let quotation = "We're a lot alike, you and I."
let sameQuotation = "We're a lot alike, you and I."
 
if quotation == sameQuotation {
    print("These two strings are considered equal")
}
 
if quotation.hasPrefix("We") {
    print("quotation start with We")
}
 
if quotation.hasSuffix("I.") {
    print("quotation end with I.")
}
cs




Unicode Representations of Strings

앞서 살펴보았듯이 String은 Character(문자열)로 구성되고, Character는 Unicode Scalar로 구성됩니다.


Unicode String 을 텍스트 파일이나 다른 저장소에 쓰게될 때, String 을 구성하는 유니코드 스칼라들은 다음 encoding forms 중 하나로 인코딩됩니다.

  • UTF-8 encoding form (인코딩을 위한 codeUnit이 8-bit)
  • UTF-16 encoding form (인코딩을 위한 codeUnit이 16-bit)
  • UTF-32 encoding form (인코딩을 위한 codeUnit이 32-bit)


앞에서는 String을 for-in loop 로 돌면서, String 을 구성하는 각각의 Character 값에 접근하는 법을 알아보았습니다. (엄밀히 말하면 Unicode extended grapheme cluster를 하나씩 for문으로 도는 것입니다.)

지금부터는 String의 값을 유니코드 표현법으로 접근하는 법을 살펴봅시다. 종류는 다음 세 가지가 있습니다.

  • UTF-8 code units의 collection (String의 utf8 프로퍼티를 통해 접근)
  • UTF-16 code units의 collection (String의 utf16 프로퍼티를 통해 접근)
  • UTF-32 code units의 collection (String의 unicodeScalars 프로퍼티를 통해 접근)


예제를 통해 하나씩 살펴봅시다.

let dogString = "Dog‼🐶"
cs

위 String 은 다음과 같은 Character 들로 구성되어 있습니다.

  • D
  • o
  • g
  • !! (DOUBLE EXCLAMATION MARK or 유니코드 스칼라 U+203C)
  • 🐶  (DOG FACE or 유니코드 스칼라 U+1F436)


이제 이 String의 값에 유니코드 표현법으로 접근해봅시다.



UTF-8 Representation


Stringutf8 프로퍼티를 사용하세요.


  • Type: String.UTF8View
  • collection of unsigned 8-bit (UInt8) values



for codeUnit in dogString.utf8 {
    print("\(codeUnit) ", terminator: "")
}
print("")
// Prints "68 111 103 226 128 188 240 159 144 182 "
cs


처음 3개 codeUnit 값 (68, 111, 103) 은 각각 D, o, g 를 표현합니다. 1 byte 만으로 표현할 수 있는 이런 문자들은 ASCII 로 표기했을 때도 동일한 코드를 가집니다. 그 다음 3개 codeUnit 값 (226, 128, 188) 은 3 byte UTF-8 이고 DOUBLE EXCLAMATION MARK 문자를 표현합니다. 마지막 4개 codeUnit 값 (240, 159, 144, 182) 는 4 byte UTF-8 이고 DOG FACE 문자를 표현합니다.



UTF-16 Representation


String 의 utf16 프로퍼티를 사용하세요.


  • Type: String.UTF16View
  • collection of unsigned 16-bit (UInt16) values



for codeUnit in dogString.utf16 {
    print("\(codeUnit) ", terminator: "")
}
print("")
// Prints "68 111 103 8252 55357 56374 "
cs


처음 3개 codeUnit 값 (68, 111, 103) 은 각각 D, o, g 를 표현하며 이는 UTF-8 표현과 같은 값입니다. D, o, 는 ASCII character 로 표기할 수 있는 문자들이기 때문입니다.

4번째 codeUnit 값 (8252) 는 16진수 값 203C 를 10진수로 나타낸 것입니다. 유니코드 스칼라 U+203C DOUBLE EXCLAMATION MARK 문자를 표현합니다. 이 문자는 UTF-16 에서 1개의 code unit 으로 나타낼 수 있습니다.

그러나 그 다음 나오는 DOG FACE 문자는 UTF-16 에서 2개의 codeUnit 이 필요합니다. 55357(U+D83D)과 56374(U+DC36)은 DOG FACE를 나타내기 위한 UTF-16 surrogate pair 입니다.

<보충설명> surrogate pair 란? 2 byte 인 UTF-16 으로 모든 문자를 표현하기에는 부족하기 때문에, 2 byte 만으로는 표현할 수 없는 예외 문자들을 Supplementary Characters 라고 정했습니다. 이 문자를 만들기 위한 인코딩 방식을 Surrogate Pair 라고 합니다.




Unicode Scalar Representation


String 의 unicodeScalars 프로퍼티를 사용하세요.


  • Type: UnicodeScalarView
  • collection of values of type UnicodeScalar
  • 각 UnicodeScalar value 라는 프로퍼티를 가진다. (scalar의 21-bit value, 즉 Uint32 값)



for scalar in dogString.unicodeScalars {
    print("\(scalar.value) ", terminator: "")
}
print("")
// Prints "68 111 103 8252 128054 "
cs


처음 세 개의 UnicodeScalar 값 (68, 111, 103) 은 변함없이 D, o, 입니다.

4번째 codeUnit 값 (8252) 도 변함없이 16진수 값 203C 를 10진수로 나타낸 것입니다. 유니코드 스칼라 U+203C DOUBLE EXCLAMATION MARK 문자를 표현합니다.

그 다음 UnicodeScalar 값 128054 DOG FACE 인 유니코드 스칼라 U+1F436 을 10진수로 나타낸 것입니다.

다음과 같이 UnicodeScalar 를 통해 새로운 String 값을 구성할 수도 있습니다:


for scalar in dogString.unicodeScalars {
    print("\(scalar) ")
}
// D
// o
// g
// ‼
// 🐶
cs


'Swift 4.2' 카테고리의 다른 글

Collection Types  (0) 2018.09.05
Strings and Characters  (0) 2018.06.20
Basic Operators  (0) 2018.03.23
The basics  (0) 2018.03.02
A Swift Tour  (0) 2017.10.17
Version Compatibility  (0) 2017.10.04

Apple 제공 Swift 프로그래밍 가이드(4.2)의 Basic Operators 부분을 공부하며 정리한 글입니다. 개인적인 생각, 이해를 돕기 위한 예제도 조금 들어가있습니다.


들어가며

Swift 는 표준 C 연산자를 대부분 지원합니다.

Swift 의 연산자에는 코딩 오류를 미리 잡아주기 위한 몇 가지 특별한 점들이 있습니다. 다음은 그 예시 몇 가지입니다:

  • 할당 연산자( )는 어떤 값도 return 하지 않는다. ( if 조건문 안에서 = 대신 == 를 사용하지 않도록 )

  • 산술 연산자( +, -, *, /, % 등 )는 오버플로우를 미리 감지하여 그런 연산을 허용하지 않는다. ( 연산결과가 저장될 변수에 오버플로우 값이 할당되지 않도록 )

또한 Swift 에는 범위 연산자(range operator)인 a..<b a...b가 새롭게 추가되었습니다. 값의 범위를 표현하기 위한 shortcut으로 활용할 수 있습니다. Range를 지정할 때 유용합니다.

이 챕터에서는 일반적인 연산자만 다룹니다. 고급 연산자, 자신만의 커스텀 연산자를 정의하는 법, 커스텀 타입을 위해 일반 연산자를 적절히 구현하는 법 등은 Advanced Operators 에서 다루고 있습니다.



Teminology

Swift의 연산자에는 unary(단항), binary(이항), ternary(삼항) 세 종류가 있습니다.

  • Unary operator: single target (ex: -a). Unary prefix operator 는 타겟의 바로 앞에 붙습니다(ex: !b). Unary postfix operator 는 타겟의 바로 뒤에 붙습니다(ex: c!).

  • Binary operator: two targets (ex: 2 + 3). 타겟 두 개의 가운데에 있기 때문에 infix 라고 부릅니다.

  • Ternary operator: three targets. 스위프트에서의 삼항 연산자는 삼항 조건 연산자 (? b : c) 가 유일합니다.

Operator(연산자)가 영향을 끼치는 값들을 operand(피연산자)라고 부릅니다. 1 + 2 라는 식에서 + 기호는 binary operator 이고 1 2 는 두 개의 operand 입니다.



Assignment Operator

Assignment operator (a = b) : b의 값으로 a의 값을 초기화 또는 업데이트.

1
2
3
4
5
6
7
8
9
10
let b = 10 // initialize b
var a = 5 // initialize a
= b // update a
 
let (x, y) = (12// x = 1, y =2
 
// 주의 : 할당 연산자는 return value가 없다. 다음과 같은 구문은 compile error
if x = y {
  // not valid
}
cs


C, Objective-C 와 다르게 Swift 의 = 연산자는 어떤 값도 리턴하지 않습니다. 그래서 Swift 에서는 == 연산자(equal to operator)를 사용해야 할 조건문에 실수로 = 연산자를 사용하는 것을 방지할 수 있습니다.



Arithmetic Operators

산술연산자의 종류는 다음과 같으며 모든 Number 타입에 사용할 수 있습니다:

  • Addition (+)

  • Subtraction (-)

  • Multiplication (*)

  • Division (/)

1
2
3
4
1 + 2       // equals 3
5 - 3       // equals 2
2 * 3       // equals 6
10.0 / 2.5  // equals 4.0
cs


스위프트의 산술 연산자는 연산결과가 오버플로우 되는 것을 허용하지 않습니다. 그래서 필히 주의해야 할 경우가 생깁니다. 컴파일 시점에서는 알 수 없지만 런타임 때 오버플로우값이 연산결과로 할당되는 경우입니다. 컴파일 시점에서 알 수 있는 오버플로우 할당은 미리 컴파일러가 체크하여 오류로 잡아줍니다. 그러나 런타임 때만 알 수 있는 오버플로우 할당의 경우에는, 아예 어플리케이션이 죽어버리고 맙니다.

오버플로우 값 저장이 필요한 경우가 있을 수도 있는데, 이럴때는 오버플로우 연산자 (ex: a &+ b)를 사용하면 됩니다. Overflow Operators 에서 다루고 있습니다.

// 덧셈 연산자로 String 을 더할 수 있다.
"hello, " + "world"  // equals "hello, world”
 
// 주의 : Character+Character, String+Character 조합은 불가능
let a:Character = "a"
let b:Character = "b"
let ab = a + b // complie error (Character+Character)
let ab2 = "a" + b // compile error (String+Character)
let ab3 = "a" + "b" // ok. (String+String)
 
let c = "c" // String Type 으로 판단
let ac = "a" + c // ok. (String+String)
cs



Remainder Operator

Swift에서의 a % ba = (b x 배수) + 나머지 에서의 나머지입니다. 즉 a % b 의 결과는로 최대한 채운 후의 나머지입니다. 따라서 가 음수일 경우에도 의 부호는 무시됩니다. (9 % 4) 와 (9 % -4) 의 결과가 동일하다는 것입니다. 왜 이렇게 되는지 살펴봅시다.


1
2
3
4
9 % 4 // 9 = (4 x 2) + 1. 따라서 1
9 % -4 // 9 = (-4 x -2) + 1. 따라서 1
-9 % 4 // -9 = (4 x -2) + -1. 따라서 -1
-9 % -4 // -9 = (-4 x 2) + -1. 따라서 -1
cs







Unary Minus Operator & Unary Plus Operator

Unary Minus Operator (-) 와 Unary Plus Operator (+) 는 숫자 값의 부호에 관여합니다.

1
2
3
4
5
let three = 3
let minusThree = -three // minusThree = -(3) = -3
let plusThree = -minusThree // plusThree = -(-3) = 3
let minusSix = -6
let alsoMinusSix = +minusSix // alsoMinusSix = +(-6) = -6
cs


위 예제에서도 드러나듯이, 사실 Unary Plus Operator(+)는 부호를 바꾸지 못하므로 사용하나 안하나 차이가 없습니다. 그러나 코드 상에서 Unary Minus Operator(-)를 사용할 때 함께 사용하면서 코드 상의 대칭을 맞출 수 있습니다.



Compound Assignment Operators

Assignment operator(=) 와 다른 연산자를 합친 compound assignment operator 를 알아봅시다. 하나의 예로 addition assignment operator (+=) 가 있습니다.

1
2
3
var a = 1
+= 2
// a is now equal to 3
cs


+= 2 는 a = a + 2 와 같습니다. + 와 += 의 수행 시간은 동일합니다.

<Note>

Compound assignment operator 역시 아무 값을 반환하지 않습니다. let b = a += 2 와 같은 코드는 compile error 를 유발합니다.


Swift 스탠다드 라이브러리에서 제공하는 연산자들에 대한 정보를 더 얻고 싶다면 Operator Declarations 를 참조하세요.



Comparison Operators

스위프트는 C의 모든 표준 비교 연산자를 지원합니다.

  • Equal to (a == b)

  • Not equal to (a != b)

  • Greater than (a > b)

  • Less than (a < b)

  • Greater than or equal to (a >= b)

  • Less than or equal to (a <= b)

<Note>

Swift 는 또한 identity operators (A === BA !== B) 를 제공합니다. 이 연산자는 AB의 object reference(객체 참조)가 동일한 object instance(객체 인스턴스)를 가리키고 있는지 검사할 때 사용합니다. 자세한 것은 Classes and Structures 를 참고하세요.


자세한 설명은 생략하고 예제로 알아보겠습니다. 단, 튜플 부분은 문서 외의 추가 설명도 넣었습니다.

1 == 1   // true because 1 is equal to 1
2 != 1   // true because 2 is not equal to 1
2 > 1    // true because 2 is greater than 1
1 < 2    // true because 1 is less than 2
1 >= 1   // true because 1 is greater than or equal to 1
2 <= 1   // false because 2 is not less than or equal to 1
 
 
let name = "world"
if name == "world" {
    print("hello, world")
else {
    print("I'm sorry \(name), but I don't recognize you")
}
// Prints "hello, world", because name is indeed equal to "world".
 
 
 
// tuple 의 comparison
// (1) 같은 index에 같은 타입이 있어야 비교가 가능 (또한 비교가 가능한 타입이어야 한다)
// (2) 비교하는 순서는 left to right, 한번에 한 인덱스만, 두 개의 값이 같을 경우 다음 인덱스를 비교
 
(1"zebra"< (2"apple"// 1 < 2 를 해보고 곧바로 return true
(3"apple"< (3"bird"// 첫 번째 element 값이 같았으므로 "apple" < "bird" 를 해보고 return true
(4"dog"== (4"dog"// 모든 element가 같은 것을 확인한 뒤 return true
 
// 추가설명
 
("blue"false< ("purple"true// Bool 타입으로는 크고 작음을 판단할 수 없다. compile error
("blue"false== ("purple"true// Bool 타입으로 같음을 판단할 수는 있다. return false
 
cs




Ternary Conditional Operator

Ternary Conditional Operator (항 조건 연산자)는 question ? answer1 : answer2 의 폼을 가지며, 세 부분으로 구성되는 특별한 연산자입니다.

if question {
    answer1
else {
    answer2
}
 
// 위 코드를 단축하여 표현한 것이 question ? answer1 : answer2
cs


삼항 조건 연산자의 장점은 코드를 간결하게 보이게 하고, 임시변수를 줄일 수 있다는 것입니다. 하지만 남용하거나 복합 구문 안에 삽입하는 것은 가독성을 떨어트리니 주의해야 합니다. 개인적으로는 한 줄 안에 끝날 때만, 그리고 단독으로 쓰일 때만(컨텍스트가 거기서 끝날 때만?) 사용하는 편입니다.

let contentHeight = 40
let hasHeader = true
let rowHeight: Int
if hasHeader {
    rowHeight = contentHeight + 50
else {
    rowHeight = contentHeight + 20
}
 
// 위 코드를 아래와 같이 단축할 수 있습니다
let contentHeight = 40
let hasHeader = true
let rowHeight = contentHeight + (hasHeader ? 50 : 20)
// rowHeight is equal to 90
cs




Nil Coalescing Operator

a ?? b

optional 변수 에 만약 값이 들어있다면 그 값을 사용하고, nil 이라면 를 사용하겠다는 연산자입니다. 풀어쓰면 a != nil ? a! : 가 됩니다. 따라서 다음 두 개가 반드시 전제됩니다.

  • 는 optional 타입

  • 의 타입은 에 저장될 수 있는 타입

이 연산자를 사용하면 optional 변수를 사용할 때 nil 체크를 하고, 언랩핑하고, nil일때의 default 값을 반환하는 등의 일련의 과정을 코드 상에서 짧고 간결하게 나타낼 수 있습니다. 실제로 코딩 시 optional 변수를 사용할 때 종종 사용하는 연산자입니다.

let defaultColorName = "red"
var userDefinedColorName: String? // defaults to nil
 
var colorNameToUse = userDefinedColorName ?? defaultColorName
print(colorNameToUse) // "red"
 
userDefinedColorName = "green"
colorNameToUse = userDefinedColorName ?? defaultColorName
print(colorNameToUse) //"green"
cs


<Note>

삼항 연산자에는 short-circuit evaluation 이 적용됩니다. 즉, a ?? b 에서가 nil이 아니라면 는 계산되지 않습니다. 예를 들어 a ?? (b + c) 에서 에 이미 값이 들어 있다면 (b + c) 연산은 실행되지 않습니다.




Range Operators

Swift 에는 값의 범위를 표현하는 범위 연산자가 몇 종류 있습니다.

  • Closed Range Operator(a...b) : a 이상 b 이하. (ex: 1...3 은 1,2,3)

  • Half-Open Range Operator(a..<b) : a 이상 b 미만. (ex: 1..<3 은 1,2)

  • One-Sided Ranges([a...], [..<b] 등) : 한 쪽 방향으로 갈 수 있는 만큼 가게 됨. (ex: [..<2] 는 0,1)

범위 연산자는 for 문에서 유용하게 쓰일 수 있을 것 같은데, 첫 인덱스가 1인지 0인지에 따라 골라쓰면 될 것 같습니다. 예를들어 1부터 N까지 숫자를 출력하는 for문에서는 a...b를 쓰는 것이 유용하고, 배열을 도는 for문에서는 인덱스가 0부터 count-1 까지니까 a..<b 를 쓰는 것이 유용합니다. Swift 4 에서 추가된 One-Sided Range Operator 도 경우에 따라 유용하게 사용할 수 있을 것 같네요(total count 를 미리 계산해서 범위지정 하지 않아도 되는..등등? 개발하면서 테스트해보겠습니다!).

for index in 1...5 {
    print("\(index) times 5 is \(index * 5)")
}
// 1 times 5 is 5
// 2 times 5 is 10
// 3 times 5 is 15
// 4 times 5 is 20
// 5 times 5 is 25
// a...b 는 b-a+1 번 돌게된다
 
 
let names = ["Anna""Alex""Brian""Jack"]
let count = names.count
for i in 0..<count { // 0..<4
    print("Person \(i + 1) is called \(names[i])")
}
// Person 1 is called Anna
// Person 2 is called Alex
// Person 3 is called Brian
// Person 4 is called Jack
// a..<b 는 b-a 번 돌게된다
 
for name in names[2...] {
    print(name)
}
// Brian
// Jack
 
for name in names[...2] {
    print(name)
}
// Anna
// Alex
// Brian
 
for name in names[..<2] {
    print(name)
}
// Anna
// Alex
 
// One-sided ranges can be used in other contexts, not just in subscripts.
let range = ...5
range.contains(7)   // false
range.contains(4)   // true
range.contains(-1)  // true
cs



Logical Operators

다음 세 가지의 논리 연산자를 통해 Boolean logic 값인 truefalse 를 다룰 수 있습니다:

  • Logical NOT (!a)

  • Logical AND (a && b)

  • Logical OR (a || b)

자세한 설명은 생략합니다. 참고로 Swift 에서 && 와 || 는 left-associative 입니다(여러 개가 한번에 결합되어 있을 때 왼쪽부터 차례로 연산된다는 의미입니다). 따라서 순서에 상관없이 특정 연산이 먼저 이루어져야 할 때는 괄호를 사용해야 합니다. 의도를 명확히 드러내기 위해 괄호를 쓰는 것도 좋습니다.

let allowedEntry = false
if !allowedEntry {
    print("ACCESS DENIED")
}
// Prints "ACCESS DENIED"
 
 
let enteredDoorCode = true
let passedRetinaScan = false
if enteredDoorCode && passedRetinaScan {
    print("Welcome!")
else {
    print("ACCESS DENIED")
}
// Prints "ACCESS DENIED"
 
 
let hasDoorKey = false
let knowsOverridePassword = true
if hasDoorKey || knowsOverridePassword {
    print("Welcome!")
else {
    print("ACCESS DENIED")
}
// Prints "Welcome!"
 
 
if enteredDoorCode && passedRetinaScan || hasDoorKey || knowsOverridePassword {
    print("Welcome!")
else {
    print("ACCESS DENIED")
}
// Prints "Welcome!"
 
 
if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword {
    print("Welcome!")
else {
    print("ACCESS DENIED")
}
// Prints "Welcome!"
cs




'Swift 4.2' 카테고리의 다른 글

Collection Types  (0) 2018.09.05
Strings and Characters  (0) 2018.06.20
Basic Operators  (0) 2018.03.23
The basics  (0) 2018.03.02
A Swift Tour  (0) 2017.10.17
Version Compatibility  (0) 2017.10.04

+ Recent posts