호댕의 iOS 개발

[Swift 기본 문법] Collection(Array / Dictionary / Set / KeyValuePairs) 본문

Software Engineering/Swift

[Swift 기본 문법] Collection(Array / Dictionary / Set / KeyValuePairs)

호르댕댕댕 2022. 4. 27. 01:07

이는 앨런 Swift 문법 마스터 스쿨을 듣고 정리한 글입니다. 

 

앨런 Swift문법 마스터 스쿨 (온라인 BootCamp - 2개월과정) - 인프런 | 강의

Swift문법을 제대로 이해, 활용해보고자 하는 철학을 바탕으로 과정이 설계되었습니다. 코딩에 대해 1도 모르는 비전공자를 시작으로 네카라쿠배에 입사할 수 있는 초고급 수준까지 올리는 것을

www.inflearn.com

 

🧺 Collection은 뭘까?

그렇다면 Collection은 뭐라고 정의할 수 있을까? 

이는 여러 개의 데이터를 효율적으로 관리하기 위한 자료형이라고 볼 수 있다. 

 

즉, 다양한 데이터를 담아 용도에 따라 사용할 수 있도록 하는 바구니 역할인 것이다. 

 

Swift에선 Collection으로 크게 Array, Dictionary, Set을 제공한다. 

 

  • Array: 데이터를 순서대로 저장 (각각 요소가 Index를 가지고 있어 해당 인덱스를 통해 접근하면 시간 복잡도가 O(1)이다)
  • Dictionary: 데이터를 키와 값, 하나의 쌍으로 만들어 관리하며 순서가 없음
  • Set: 순서가 없으며 중학교 때 배우는 집합과 비슷한 연산 가능

 

🔶 Array

배열의 index는 항상 0부터 자동으로 순서가 지정되며, 순서가 정해져 있기 때문에 데이터의 중복이 가능하다. 

 

선언은 아래와 같은 형식으로 이뤄진다. 

// 타입은 생략 가능
var numbers: Array<Int> = [1, 2, 3, 4, 5]
var words: [String] = ["Apple", "Swift", "iOS", "Hello"]

// 빈 배열 생성 
let emptyArray1: Array<Int> = []
let emptyArray2: [Int] = []
let emptyArray3 = [Int]()

🔍 요소에 접근

Array는 기본적으로 서브스크립트 문법을 지원하기 때문에 대괄호에 검색할 index를 작성하여 해당 요소에 접근할 수 있다. 

words = ["Apple", "Swift", "iOS", "Hello"]

words[0] // 첫번째 인덱스에 위치한 "Apple"이 나옴

index의 범위를 통해 검색을 할 수도 있다. 

words = ["Apple", "Swift", "iOS", "Hello"]

words[0...2] // ["Apple", "Swift", "iOS"]

만약 단순히 첫 번째 요소나 마지막 요소에 접근하고 싶다면 다음과 같이 first와 last를 사용할 수 있다. 

words.first // Optional("Apple")
words.last // Optional("Hello")

이는 반환 타입이 옵셔널이기 때문에 만약 아예 빈 배열이라면 first와 last의 값은 nil이 나오게 된다. 

 

let numbers = [1, 2, 3, 4, 5]
numbers.startIndex // 0
numbers.endIndex // 5

배열의 startIndex를 보면 항상 0이 나오게 된다. 하지만 endIndex는 요소가 5개인데도 4가 아닌 5가 나오게 된다. 

 

따라서 만약 endIndex를 바로 사용하게 되면 Fatal error: Index out of range가 발생하게 된다. 따라서 마지막 요소에 접근하려면 numbers[endIndex - 1]로 접근해야 한다. 

 

(0) 1 (1) 2 (2) 3 (3) 4 (4) 5 (5)

 

괄호에 있는 것이 index라 생각한다면 이는 1byte 간격으로 데이터가 존재하게 된다. 여기서 endIndex는 마지막 요소가 끝나는 지점의 index까지 반환하게 되는 것이다. 

 

🔨 삽입 / 교체 / 추가 / 삭제

배열의 경우 삽입 / 교체 / 추가 / 삭제가 전부 가능하다. 

 

<삽입>

이는 insert 메서드를 사용해서 배열의 원하는 위치에 추가할 수 있다. 

다만 추가할 수 있는 Index를 벗어나는 경우 Array index is out of range 에러가 발생하게 된다. 

var alphabet = ["A", "B", "C", "D", "E", "F", "G"]

alphabet.insert("c", at: 4) // ["A", "B", "C", "D", "c", "E", "F", "G"]

index 4의 위치에 값이 들어가는 것을 볼 수 있다. 

alphabet.insert(contentsOf: Collection, at: Int)

이렇게 배열을 특정 index에 집어넣을 수도 있다. 

 

<교체>

이미 해당 요소에 값이 있는 경우 아래 방법들을 통해 배열의 요소를 바꿔줄 수 있다. 

alphabet = ["A", "B", "C", "D", "E", "F", "G"]

// 요소 교체하기
alphabet[0] = "a"

// 범위를 교체하기
alphabet[0...2] = ["x", "y", "z"]

// 교체하기 함수 문법
alphabet.replaceSubrange(0...2, with: ["a", "b", "c"])

특정 범위에 대해서도 교체를 해줄 수도 있고 특정 index에 접근하여 변경을 해줄 수도 있다. 

replaceSubrange 메서드를 통해 변경을 해줄 수도 있다. 

 

<추가>

추가는 +나 append를 통해 해줄 수 있다. 

추가의 경우 항상 배열의 마지막 끝에 추가를 해주게 된다. 

alphabet = ["A", "B", "C", "D", "E", "F", "G"]

alphabet += ["H"]

// 추가하기 함수
alphabet.append("H")
alphabet.append(contentsOf: ["H", "I"]) // 배열도 추가 가능

 

<삭제>

특정 범위의 요소에 빈배열을 주거나 remove(at:) 메서드 등을 통해 삭제를 할 수 있다. 

alphabet = ["A", "B", "C", "D", "E", "F", "G"]

// 서브스크립트 문법으로 삭제
alphabet[0...2] = []

// 요소 한개 삭제
alphabet.remove(at: 2)  // 삭제하고, 삭제된 요소 리턴
alphabet.remove(at: alphabet.endIndex - 1) // 마지막 요소를 지움.

// 요소 범위 삭제
alphabet.removeSubrange(0...2)

alphabet = ["A", "B", "C", "D", "E", "F", "G"]

alphabet.removeFirst()   // 맨 앞에 요소 삭제하고 삭제된 요소 리턴
alphabet.removeFirst(2)   // 앞의 두개의 요소 삭제 ===> 리턴은 안함

alphabet.removeLast()   // 맨 뒤에 요소 삭제하고 삭제된 요소 리턴
alphabet.removeLast(2)

// 배열의 요소 모두 삭제(제거)
alphabet.removeAll() // 모든 요소를 삭제 - 메모리 공간을 지워버림.
alphabet.removeAll(keepingCapacity: true)  // 저장공간을 일단은 보관해 둠. (안의 데이터만 일단 날림)

여기서 처음 접한 것은 removeAll(keepingCapacity:) 였다. 

이는 모든 요소를 삭제하는 것은 removeAll()과 똑같지만 이를 통해 지우게 되면 메모리 상의 저장 공간은 일단 유지를 해둔다. 이를 통해 다시 배열을 할당할 경우 좀 더 빠르게 할당을 할 수 있도록 하는 것이다. 

 

이외에도 sorted(), reversed(), shuffled() 등과 같은 메서드들이 존재한다. 

이런 메서드들은 ed를 붙인 경우도 있고 안 붙인 경우도 존재한다. 이는 어떤 차이가 있을까?

 

이는 SwiftAPI Design Guidelines에 나와 있다. 

When the operation is naturally described by a verb, use the verb’s imperative for the mutating method and apply the “ed” or “ing” suffix to name its nonmutating counterpart.

즉 ed나 ing를 붙이지 않는다면 배열을 직접 변경하게 되며, ed나 ing를 붙이게 되면 배열을 직접 변경하진 않고 새로운 배열을 반환하게 된다. 

 

⚖️ 배열의 비교

배열의 경우 순서가 존재하기 때문에 순서까지 일치해야 동일한 배열이라고 판단한다. 

let a = ["A", "B", "C"]
let b = ["a", "b", "c"]
let c = ["a", "b", "c"]
let d = ["c", "b", "a"]

a == b   // false
b == c   // true
c == d   // false 순서만 달라도 다른 배열로 인식

 

만약 for문이나 forEach 문을 돌면서 요소만 아는 것이 아니라 해당 요소의 index까지 알고 싶다면 어떻게 해야할까?

이는 알고리즘을 풀다보면 종종 필요하게 된다. 

nums = [10, 11, 12, 13, 14]

for tuple in nums.enumerated() {
    print("\(tuple.0) - \(tuple.1)") 
}

for tuple in nums.enumerated() {
    print("\(tuple.offset) - \(tuple.element)") 
}

for (index, element) in nums.enumerated() {
	print("\(index) - \(element)"
}

위처럼 tuple로 각각의 index와 element를 받을 수도 있고 튜플을 직접 작성하여 받을 수도 있다. 

 

 

🔶 Dictionary

이는 데이터를 키와 값으로 하나의 쌍으로 만들어 관리하게 된다. 

키의 경우 유일해야 하며 중복이 불가능하다. 즉 키의 경우 Hashable해야 하는 것이다. 특정 Key를 넣어주면 항상 같은 결과가 나와야 하며 Hash를 위한 값은 항상 고정된 길이의 숫자 혹은 글자로 유일한 값이어야 한다. 

 

다만 값의 경우 중복이 가능하다. 값의 경우 배열이나 딕셔너리를 중첩하여 사용할 수도 있다. 

 

Hash 알고리즘을 통해 특정 키에 해당하는 값을 빠르게 찾을 수 있다. 

즉, 요소를 찾을 때 처음부터 전부 탐색을 하는 Array보다 검색이 빠르다고 볼 수 있다. 

 

dic = ["A": "Apple", "B": "Banana", "C": "City"]

dic["A"]

여기서 배열과 유사하게 Subscript에 key를 통해 값을 찾을 수 있는데 해당 key를 가지고 있지 않을 수 있기 때문에 nil의 가능성이 있고 따라서 항상 optional로 값을 반환하게 된다. 

만약 특정 키의 값이 존재하지 않는다면 다음과 같이 기본값을 제공할 수도 있다. 

dic["S", default: "Empty"]

여기서는 S라는 키가 없다면 기본으로 값에 Empty를 넣게 되며 이 경우는 nil이 발생하지 않는다. 

 

다만 딕셔너리에는 값만 따로 검색하는 기능은 존재하지 않는다

 

🔨 삽입 / 교체 / 추가 / 삭제

배열과는 달리 딕셔너리에선 삽입 교체 추가를 updateValue나 키에 직접 접근하여 변경을 한다. 

words = [:] // 빈 배열 생성

words["A"] = "Apple"   // "A"라는 키가 있으면 Apple로 값을 변경하고 없다면 키와 value 추가
words // ["A": "Apple"]

words["B"] = "Banana"
words // ["B": "Banana", "A": "Apple"]

words["B"] = "Blue"    // 이미 동일한 Key가 존재하기 때문에 Banana를 Blue로 변경
words // ["B": "Blue", "A": "Apple"]

words.updateValue("City", forKey: "C") // 단순히 추가하면 변경할 내용이 없기 때문에 Nil을 리턴함. (기존의 값 전달)

words.updateValue("Country", forKey: "C")   // 기존에 C라는 Key가 존재하여 값을 Country로 바꾸며 기존 값을 return
words // ["A": "Apple", "B": "Blue", "C": "Country"]

words = ["A": "A"]   // 전체 교체하기(바꾸기)

여기서 updateValue를 하게 되면 

  • 기존의 Key가 있는 경우 : Value를 바꾸게 되며 기존의 Value를 반환함.
  • 기존 Key가 없는 경우 : Value를 추가하게 되며 기존 값은 없기 때문에 nil 반환

이 된다. 

 

삭제의 경우 다음과 같다. 

dic["B"] = nil    // 해당요소 삭제
dic["E"] = nil   // 존재하지 않는 키/값을 삭제 ======> 아무일이 일어나지 않음(에러아님)

dic.removeValue(forKey: "A")

dic.removeAll()
dic.removeAll(keepingCapacity: true) // 배열과 마찬가지로 메모리 공간을 유지

배열의 경우 존재하지 않는 index에 접근할 경우 fatalError가 발생했지만 딕셔너리의 경우 존재하지 않는 키/값을 추가하거나 제거하려 해도 에러가 발생하지 않는다

 

⚖️  딕셔너리의 비교

딕셔너리의 경우 순서가 존재하지 않기 때문에 순서가 다르더라도 같은 키와 값을 가지고 있다면 동일한 딕셔너리로 본다. 

let a = ["A": "Apple", "B": "Banana", "C": "City"]
let b = ["A": "Apple", "C": "City", "B": "Banana"]

a == b   // true

 

🔶 Set

Set은 중학교 1학년 때 배웠던 집합과 유사하다. 

Set은 배열과 같은 형태로 되어 있기 때문에 항상 타입을 작성해줘야 한다. 

 

Set은 중복된 값을 가지고 있지 않으며 순서를 가지고 있지 않다

let set: Set<Int> = [1, 2, 3]

🔨 삽입 / 교체 / 추가 / 삭제

삽입 교체 추가 삭제 모두 딕셔너리와 거의 유사하다. 

삽입 / 교체 / 추가의 경우 update 메서드를 사용하면 된다. 

set.update(with: 1)     // 업데이트 하면서 기존의 값을 반환함.
set.update(with: 7)     // 새로운 요소가 추가되면 ====> 리턴 nil

삭제도 remove 메서드를 통해 가능하며 마찬가지로 removeAll 메서드를 사용할 수 있다. 

 

여기서 중요한 점은 Set은 Subscript 관련 문법을 지원하지 않는다는 것이다. 

 

🔶 KeyValuePairs

이는 Swift 5.2에 등장한 개념으로 딕셔너리와 매우 유사한 형태이나 이는 순서가 존재한다. 

key가 Hashable 프로토콜을 채택하고 있지 않기 때문에 key의 중복도 가능하다. 

 

딕셔너리와 형태가 똑같기 때문에 타입은 꼭 명시해줘야 한다. 

let introduce: KeyValuePairs = ["first": "Hello", "second": "My Name", "third":"is"]

 

여기서는 append와 remove같은 기능이 존재하지 않는다. 

배열처럼 인덱스를 통해 요소에 접근은 가능하다. 

Comments