개발🧑‍💻/Swift

[Swift] 데이터 타입 (2) - 데이터 타입 안심, 컬렉션 타입

* 이 글은 "스위프트 프로그래밍 (3판)" 및 야곰닷넷의 "스위프트 기초 강의"를 들으며 정리한 내용입니다.

 

1.  스위프트의 데이터 타입 안심

 스위프트는 타입에 굉장히 민감하고 엄격한 언어입니다. 서로 다른 타입간의 데이터 교환은 반드시 타입캐스팅(Type-Casting) 을 거쳐야 합니다. 스위프트에서 값 타입의 데이터 교환은 엄밀히 말해서 타입캐스팅이 아닌 새로운 인스턴스를 생성하여 할당하는 것입니다.

 

 스위프트는 데이터 타입을 안심하고 사용할 수 있는(Type-Safe) 언어입니다. 예를 들면 String 타입 변수에 Int 타입의 값을 할당하려고 하면 컴파일 오류가 발생합니다. 스위프트가 컴파일 시 타입을 확인하는 것을 타입 확인이라고 합니다. 이러한 타입 확인을 통해 런타임 오류를 피할 수 있습니다.

 

 스위프트에서는 변수나 상수를 선언할 때 특정 타입을 명시하지 않아도 컴파일러가 할당된 값을 기준으로 변수 혹은 상수의 타입을 결정합니다. 이를 타입 추론이라고 합니다.

 

var name = "Peter"

print(type(of: name))
name = 100

/*

String 
error: cannot assign value of type 'Int' to type 'String'
name = 100
       ^~~
*/

// 타입을 지정하지 않음. 그러나 타입 추론을 통해 name은 String 타입으로 선언됨을 알 수 있다.
// String 타입으로 지정된 name에 100을 할당하려고 시도하였으나 오류가 발생함

 

 스위프트에서는 기본적으로 제공하는 데이터 타입이든, 사용자가 임의로 만든 데이터 타입이든 이미 존재하는 데이터 타입에 임의로 다른 이름(별칭)을 부여할 수 있습니다. 이를 타입 별칭이라고 합니다.

 

typealias NewInt = Int
typealias OldInt = Int
typealias Birth = String
typealias NewString = String

let newName : NewString = "Peter Parker"
let birthDate : Birth = "2002.05.03"

let myAge : OldInt = 25 // OldInt는 Int의 또다른 이름
var age : NewInt = 26

// NewInt와 OldInt 모두 Int임으로 같은 타입으로 취급된다.
age = myAge

// 기존의 Int도 사용 가능하다.
let month : Int = 3

 

2. 튜플(Tuple)

 튜플은 타입의 이름이 따로 지정되어 있지 않은, 프로그래머 마음대로 만드는 타입입니다. '지정된 데이터의 묶음' 이라고 표현할 수 있습니다. 스위프트의 튜플은 파이썬의 튜플과 비슷합니다. 튜플은 타입 이름이 따로 없으므로 일정 타입의 나열만으로 튜플 타입을 생성해줄 수 있습니다.

// String, Int, Double 타입을 갖는 튜플 선언
var student : (String,Int,Double) = ("Peter",4,4.2)

// 인덱스를 통해 값을 나타낼 수 있다.
print("이름 : \(student.0), 학년 : \(student.1), 점수 : \(student.2)")

// 이름 : Peter, 학년 : 4, 점수 : 4.2

// 인덱스를 통해 값을 할당
student.1 = 3
student.2 = 4.3

print("이름 : \(student.0), 학년 : \(student.1), 점수 : \(student.2)")
// 이름 : Peter, 학년 : 3, 점수 : 4.3

 그러나 튜플의 각 요소를 인덱스를 통해 숫자로 표현하게 되면 다른 동료와의 협업시에 의미를 유추하기 어렵기 때문에, 튜플의 요소마다 이름을 붙여줄 수도 있습니다.

// String, Int, Double 타입을 갖는 튜플 선언
var student2 : (name : String, year: Int, grade : Double) = ("Peter",4,4.2)

// 각 요소의 이름을 통해 값을 나타낼 수 있다.
print("이름 : \(student2.name), 학년 : \(student2.year), 점수 : \(student2.grade)")
// 이름 : Peter, 학년 : 4, 점수 : 4.2

// 인덱스를 통한 값의 할당, 이름을 통한 값의 할당 모두 가능
student2.grade = 4.0
student2.1 = 1

// 기존의 인덱스를 이용한 값 확인 가능
print("이름 : \(student2.0), 학년 : \(student2.1), 점수 : \(student2.2)")
// 이름 : Peter, 학년 : 1, 점수 : 4.0

 

3. 컬렉션 타입

3.1 배열 (Array)

 배열은 같은 타입의 데이터를 일렬로 나열한 후 순서대로 저장하는 형태의 컬렉션 타입입니다. 배열의 선언에는 다양한 방법이 있습니다. let 키워드를 사용하여 배열을 선언하면 변경할 수 없는 배열이 되고, var 키워드를 사용하여 배열을 선언하면 변경이 가능한 배열이 됩니다.

 

 배열의 isEmpty 프로퍼티는 비어있는 배열인지 확인할 수 있고, 배열에 몇개의 요소가 있는지 확인하고 싶으면 count 프로퍼티를 사용합니다. 배열은 인덱스를 통해 각 요소에 접근할 수 있습니다. 잘못된 인덱스로 접근하려고 하면 Exception Error가 발생합니다. 배열의 맨 처음과 마지막 요소는 first,last 프로퍼티를 통해 가져올 수 있습니다. firstIndex(of:) 메소드를 사용하면 배열 내의 제일 먼저 발견된 해당 요소의 인덱스를 반환합니다. 맨 뒤에 요소를 추가할때는 append(_:) 메소드를 사용합니다. 배열의 중간에 요소를 삽입할때는 insert(_:at:) 메소드를 사용합니다. 요소를 삭제할때는 remove(_:) 메소드를 사용합니다.

 

// String 타입의 데이터를 요소로 하는 배열을 선언한다. 아래의 두 방법 모두 선언이 가능하다.
var avengers : Array<String> = ["Spider-Man","Iron Man","Captain America","Hulk"]
var sameAvengers : [String] = ["Spider-Man","Iron Man","Captain America","Hulk"]

// Any 데이터를 요소로 하는 빈 배열을 생성한다. 두 코드는 같은 동작을 한다.
var emptyArray1 : [Any] = [Any]()
var emptyArray2 : [Any] = Array<Any>()

// 배열의 타입을 명시했다면 [] 만으로 빈 배열을 생성할 수 있다.
var emptyArray3 : [String] = []

emptyArray3.isEmpty // true
avengers.count // 4

print(avengers[0]) // Spider-Man
avengers[2] = "Thor"
print(avengers[2]) // Thor

// 아래의 두 케이스는 인덱스의 범위를 벗어났기 때문에 에러 발생
// print(avengers[4])
// avengers[4] = "Black Widow"

// 맨 마지막에 추가
avengers.append("Vision") 
avengers.append(contentsOf: ["QuickSilver","Scarlet Witch"])

// 중간에 삽입
avengers.insert("Hawkeye", at: 3) // 인덱스 3에 삽입
avengers.insert(contentsOf:["Ant-Man","Wasp"], at: 2) // 인덱스 2에 삽입

print(avengers.first) // Spider-Man
print(avengers.last) // Scarlet Witch
print(avengers.firstIndex(of:"Vision")) // 7
print(avengers.firstIndex(of:"Black Panther")) // nil

let firstHero : String = avengers.removeFirst() 
let lastHero : String = avengers.removeLast() 
let indexZeroHero : String = avengers.remove(at: 0) 

print(firstHero) // Spider-Man
print(lastHero) // Scarlet Witch
print(indexZeroHero) // Iron Man
print(avengers[0 ... 5]) // ["Ant-Man", "Wasp", "Thor", "Hawkeye", "Hulk", "Vision"]

3.2 딕셔너리 (Dictionary)

 딕셔너리는 요소들이 순서 없이 키와 값의 쌍으로 구성되는 컬렉션 타입입니다. 딕셔너리에 저장되는 값은 항상 키와 쌍을 이루게 되는데, 하나의 딕셔너리 안에는 같은 이름을 가진 키를 중복하여 사용할 수 없습니다. 딕셔너리에서 키는 값을 대변하는 유일한 식별자가 됩니다.

 

 배열과 마찬가지로 let 키워드와 var 키워드를 이용하여 불변,가변 딕셔너리를 생성할 수 있고 Dictionary 키워드와 키의 타입, 값의 타입 조합으로 선언합니다. 또한 isEmpty, count 프로퍼티도 사용 가능합니다.

 

// typealias를 사용하여 표현
typealias StringIntDictionary = [String:Int]

// Dictionary를 선언하는 방법들
var nameAndNumber1 : Dictionary<String,Int> = Dictionary<String,Int>()
var nameAndNumber2 : [String:Int] = [String:Int]()
var nameAndNumber3 : StringIntDictionary = StringIntDictionary()
var nameAndNumber4 : [String:Int] = [:]

// 초기값을 할당한 뒤 선언
var nameAndNumber5 : [String:Int] = ["Ronaldo":7,"Sancho":25,"GreenWood":11]

nameAndNumber5.isEmpty // false
nameAndNumber5.count // 3

 

 딕셔너리는 키를 통해 각 값에 접근할 수 있습니다. 딕셔너리 내부에서 키는 유일해야 하며, 값은 유일하지 않습니다. 딕셔너리는 배열과는 다르게 딕셔너리 내부에 존재하지 않는 키로 접근해도 오류가 발생하지 않고, nil을 반환합니다. 특정 키에 해당하는 값을 제거하려면 removeValue(forKey:) 메소드를 사용합니다.

 

print(nameAndNumber5["Sancho"]) //  25
print(nameAndNumber5["Martial"]) // nil

nameAndNumber5["Pogba"] = 6 
print(nameAndNumber5["Pogba"]) // 6

print(nameAndNumber5.removeValue(forKey: "Sancho")) // 25

// 위에서 이미 Sancho 키에 해당하는 값이 삭제되었으므로 nil 반환
print(nameAndNumber5.removeValue(forKey: "Sancho")) // nil

// Sancho 키에 해당하는 값이 없으면 0을 디폴트로 반환
print(nameAndNumber5["Sancho", default:0]) // 0

 

3.3 세트(Set)

 세트는 같은 타입의 데이터를 순서 없이 하나의 묶음으로 저장하는 형태의 컬렉션 타입입니다. 세트 내의 값은 모두 유일한 값으로, 중복된 값이 존재하지 않습니다. 그래서 세트는 순서가 중요하지 않거나 각 요소가 유일한 값이어야 하는 경우에 사용합니다. 세트의 요소는 해시 가능한 값(스위프트 표준 라이브러리의 Hashable 프로토콜을 따르는 값, 스위프트의 기본 데이터 타입은 모두 해시 가능한 값이다.)이 들어와야 합니다.

 

 위의 배열, 딕셔너리와 마찬가지로 isEmpty, count 프로퍼티를 사용할 수 있으며, 선언 방법 및 사용은 다음과 같습니다.

// 빈 세트 생성
var players : Set<String> = Set<String>()
var footballPlayers : Set<String> = []

// 배열과 마찬가지로 [] 대괄호를 사용
var mufcPlayers : Set<String> = ["Ronaldo","Cavani","Rashford","Sancho","Pogba"]

// 그렇기 때문에 타입 추론을 하게되면 Set이 아닌 Array로 타입을 지정
var numbers = [7,21,10,25,6]

print(type(of: numbers)) // Array<Int>

mufcPlayers.isEmpty // false
mufcPlayers.count // 5
mufcPlayers.insert("Jones")
mufcPlayers.count // 6

print(mufcPlayers.remove("Jones")) // Jones
print(mufcPlayers.remove("Dalot")) // nil

 

세트는 내부의 값들이 모두 유일함을 보장하므로, 집합관계를 표현할때 유용하게 쓰이며, 교집합, 합집합 등을 연산하기 좋습니다. 또한 sorted() 메소드를 통해 세트를 정렬된 배열로 반환할 수 있습니다.

 

var newMUFCPlayers : Set<String> = ["Maguire","GreenWood","Ronaldo","Cavani","Sancho","Pogba"]
var englandPlayers : Set<String> = ["Maguire", "Sterling","Kane","Mount","Sancho","Grealish","Saka","GreenWood"]

// 교집합
let intersectSet : Set<String> = englandPlayers.intersection(newMUFCPlayers)
// {"Sancho", "Maguire", "GreenWood"}

// 배타적 논리합(여집합의 합)
let symmetricDiffSet: Set<String> = englandPlayers.symmetricDifference(newMUFCPlayers)
// {"Sterling", "Cavani", "Grealish", "Saka", "Mount", "Ronaldo", "Kane", "Pogba"}

// 합집합
let unionSet : Set<String> = englandPlayers.union(newMUFCPlayers)
// {"Sterling", "Sancho", "Cavani", "Maguire", "Grealish", "Saka", "Mount", "GreenWood", "Kane", "Ronaldo", "Pogba"}

// 차집합
let subtractSet : Set<String> = englandPlayers.subtracting(newMUFCPlayers)
// {"Sterling", "Grealish", "Saka", "Mount", "Kane"}

print(unionSet.sorted())
// ["Cavani", "Grealish", "GreenWood", "Kane", "Maguire", "Mount", "Pogba", "Ronaldo", "Saka", "Sancho", "Sterling"]


newMUFCPlayers.isSubset(of: englandPlayers) // 부분집합인지? - false
newMUFCPlayers.isSuperset(of: newMUFCPlayers) // 전체집합인지? -  true
newMUFCPlayers.isDisjoint(with: englandPlayers) // 서로 배타적인지 - false

 

'개발🧑‍💻 > Swift' 카테고리의 다른 글

[Swift] 구조체와 클래스, 열거형 - 사용자 정의 타입  (0) 2021.11.17
[Swift] 옵셔널  (0) 2021.11.12
[Swift] 함수  (0) 2021.11.09
[Swift] 데이터 타입 (1) - 기본  (0) 2021.10.20
[Swift] 기본  (0) 2021.09.08