🐦 Swift

[Swift] 데이터 타입 고급 (튜플, 컬렉션, 열거형)

dev_zoe 2023. 5. 31. 22:17
반응형

본 포스팅은 '스위프트 프로그래밍 (3판) - 야곰 저' 도서와 앨런 Swift 문법 마스터스쿨 강의를 통해 공부하며 정리하는 포스팅입니다.

혹시 틀린 부분이 있거나 질문이 있으시다면 언제든지 댓글 달아주시면 정말 감사하겠습니다 :)


1. 튜플

지정된 데이터의 묶음으로, 연관된 데이터를 같이 묶음으로써 표현할 때 유용함 (ex. 홍길동, 20세, 서울)

- 튜플.인덱스, 튜플.요소이름 으로 튜플 안에서 각 데이터를 꺼내오고, 할당도 가능함

//1. 인덱스로 가져오는 방법
var person: (String, Int, Double) = ("yuri", 10, 170.0)

print("\(person.0), \(person.1), \(person.2)") // yuri, 10, 170

//2. 요소의 이름을 정해주는 방법 (가독성이 좋아짐)

var person: (name: String, age: Int, height: Double) = ("yuri", 10, 170.0)
var person = (name: "yuri", age: 10, height: 170.0) // 타입 추론

print("\(person.name), \(person.age), \(person.height)") //yuri, 10, 170.0
print("\(person.0), \(person.1), \(person.2)") // yuri, 10, 170.0 (마찬가지로 인덱스로 접근 가능)

person.name = "zoe"
person.height = 160.0

- 튜플 분해 (튜플의 각 요소를 상수/변수화 -> 값 바인딩)

let (name, age, address) = ("조이", 20, "서울")

print(name) //조이

- 튜플 별칭

typealias Person = (name: String, age: Int, height: Double)
let yuri:Person = ("yuri", 20, 170.0)

print("이름: \(yuri.name) 나이: \(yuri.age))

- 튜플과 if/switch 문의 활용

let iOS = (language: "Swift", type: "Mobile")

if iOS == ("Swift, "Mobile") { //튜플 채로 비교 가능
}
switch iOS {
case ("Swift", "Mobile"):
    print("iOS는 모바일 분야입니다.")
case ("Swift", "Server"):
    print("iOS는 서버 분야입니다.")
default:
    break
}

//iOS는 모바일 분야입니다.

- switch문을 활용한 튜플 케이스 처리

var coord = (0, 5)   // 좌표

switch coord {
case (let distance, 0), (0, let distance):   // x축 or y축에 있으면 출력하라는 코드
    print("X 또는 Y축 위에 위치하며, \(distance)만큼의 거리가 떨어져 있음")
default:
    print("축 위에 있지 않음")
}


switch coord {
case (let x, let y) where x == y:      //상수 x, y에 coord 값을 바인딩한 후 where 절로 비교
    print("(\(x), \(y))의 좌표는 y = x 1차함수의 그래프 위에 있다.")
    
case let (x, y) where x == -y:
    print("(\(x), \(y))의 좌표는 y = -x 1차함수의 그래프 위에 있다.")
    
case let (x, y):
    print("(\(x), \(y))의 좌표는 일차 함수가 아닌 임의의 지점에 있다.")
}

switch coord {
case (0, 0):
    print("(0, 0)은 원점 위에 있다.")
case (-2...2, -2...2): //범위 연산자로 튜플이 해당 범위에 속하는지 확인 가능
    print("(\(coord.0), \(coord.1))은 원점의 주위에 있다.")
default:
    print("점은 (\(coord.0), \(coord.1))에 위치한다.")
}

 

2. 콜렉션

배열, 딕셔너리, 세트가 있음

- 데이터를 묶어서 효율적으로 관리하기 위한 자료형

 

배열 (Array)

- 데이터를 순서대로 저장하는 컬렉션 타입 (중복 O)

let array: [Int] = []
let array: Array<Int> = []
let array = Array<Int>()
let array = [Int]()

 

배열 주요 메소드

원래 외울 필요까지는 없지만

코딩테스트에서 배열/문자열 모르면 절대 안되기 때문에! 계속 새로운걸 알 때마다 갱신해두는 포스팅이 있다.

해당 포스팅 참고!

 

딕셔너리 (Dictionary)

- 순서 없이 키와 값의 쌍으로 관리하는 컬렉션 타입

// 단축문법
var words: [String: String] = [:]

// 정식문법
var words: Dictionary<String, String>

// 빈 딕셔너리 생성
let emptyDic1: Dictionary<Int, String> = [:]
let emptyDic2 = Dictionary<Int, String>()
let emptyDic3 = [Int: String]()
let emptyDic4:[Int:String] = [:]

- 키 값은 반드시 유일해야하며, Hashable 해야한다. Hashable 한 키값으로 인해 검색속도가 빠르다. (시간복잡도 O(1))

 

❓Hashable이란?

- 해시함수는 input을 특정 해싱 알고리즘을 적용하여 유일한 값으로 만들어주는 함수인데, 이 때 해시함수의 input으로 쓰일 수 있는 타입은 Hashable 프로토콜을 따르게 된다.

- Swift의 모든 데이터 타입은 Hashable 프로토콜을 채택하고 있어, 해시함수의 input 값 즉 딕셔너리의 key로 사용될 수 있다.

 

딕셔너리 주요 메소드

원래 외울 필요까지는 없지만

코딩테스트에서 배열/문자열 모르면 절대 안되기 때문에! 계속 새로운걸 알 때마다 따로 갱신해두는 포스팅이 있다.

해당 포스팅 참고!

 

세트

- 순서와 중복 없는 컬렉션 타입

- 세트 생성시, 배열과 동일하게 대괄호 ([])를 사용함

- 배열과 대부분의 메소드가 유사하고, 다른점은 추가는 insert(_ element) 함수라는 점, 제거는 인덱스가 아닌 remove("삭제하려는 값")이라는 점이 있다.

- Set의 요소로 Hashable 한 (Hashable 프로토콜을 따르는) 데이터 타입의 값이 와야함 (딕셔너리와 동일하게 Hashing 알고리즘이 탐색에 적용되기 때문에 시간복잡도가 O(1) 이다.)

var names: Set<String> = [] // 빈 세트 생성
var names: Set<String> = ["yuri", "yuri", "chulsoo"] // ["yuri", "chulsoo"]

- 집합 연산 (합집합, 교집합, 차집합)

let englishClassStudents: Set<String> = ["john", "chulsoo", "yagom"]
let koreanClassStudents: Set<String> = ["jenny", "yagom", "chulsoo", "hana", "minsoo"]

let intersectSet: Set<String> = englishClassStudents.intersection(koreanClassStudents)
// 교집합 {"yagom", "chulsoo"}

let unionSet: Set<String> = englishClassStudents.union(koreanClassStudents)
// 합집합 {"minsoo", "jenny", "john", "yagom", "chulsoo", "hana"}

let subtractSet: Set<String> = englishClassStudents.subtracting(koreanClassStudents)
// 차집합 {"john"}

print(unionSet.sorted())  // ["chulsoo", "hana", "jenny", "john", "minsoo", "yagom"]
// 코드 4-13 세트의 활용 – 포함관계 연산
let 새: Set<String> = ["비둘기", "닭", "기러기"]
let 포유류: Set<String> = ["사자", "호랑이", "곰"]
let 동물: Set<String> = 새.union(포유류)  // 새와 포유류의 합집합

print(새.isSubset(of: 동물))           // 새가 동물의 부분집합인가요? - true
print(동물.isSuperset(of: 포유류))      // 동물은 포유류의 전체집합인가요? - true
print(동물.isSuperset(of: 새))         // 동물은 새의 전체집합인가요? - true

 

3. 열거형

- 연관된 항목들을 묶어서 표현할 수 있는 타입

- 1) 타입에 제한된 선택지를 주고싶을 때 2) 예상된 입력 값이 한정되어 있을 때

- 각 열거형이 고유의 타입으로 인정되므로, 코드의 가독성과 안정성이 높아짐

 

1) 기본 열거형

enum School { // 타입이름 : 대문자로 시작
    case primary // 각 케이스 : 소문자로 시작
    case elementary
    case middle
    case high
    case college
    case university
    case graduate
}

//한줄로도 표현 가능
enum School { case primary, elementary, middle, high, college, university, graduate }

var highestEducationLevel:School = School.graudate
//위와 같은 표현
var highestEducationLevel:School = .graduate
highestEducationLevel = .primary //위에서 타입 지정을 해주었기 때문에 타입 생략 가능

 

* switch를 활용한 기본 열거형의 사용

let 최종학력: School = School.university

switch 최종학력 {
    case .primary:
    	print("유치원")
    case .elementary:
    	print("초등학교")
    case .middle:
    	print("중학교")
    case .high:
    	print("고등학교")
    case .university, .college: // switch에서 콤마는 or
    	print("대학교")
    case .graduate:
    	print("대학원")
}

// "대학교"

 

2) 원시값(rawValue)을 가지는 열거형

- 열거형의 각 항목은 값을 가질 수 있다. 이를 원시값이라 하고, rawValue 프로퍼티를 통해 값을 꺼내올 수 있다.

enum School: String {
    case primary = "유치원"
    case elementary = "초등학교"
    case middle = "중학교"
    case high = "고등학교"
    case college
    case university
    case graduate
}

var highestEducationLevel:School = School.university
print("저의 최종 학력은 \(highestEducationLevel.rawValue) 입니다") //저의 최종학력은 university 입니다.
enum Numbers: Int {
	case zero //0
    case one //1
    case two //2
    case ten = 10
}

- 보통 원시값의 타입으로 Int String을 많이 활용하며, 원시값을 따로 지정하지 않는다면

Int: 0, 1, 2, ... 차례대로 자동 매칭

String: case 이름 문자열로 자동 매칭

 

- 만약 원시값을 가지는 열거형이라면,  원시값을 알 경우 원시값을 통해 인스턴스를 생성할 수 있음

enum RpsGame: Int {
    case rock
    case paper
    case scissors
}

RpsGame(rawValue: 0)!
RpsGame(rawValue: 1)
RpsGame(rawValue: 2) // 자동매칭 되므로 따로 지정하지 않아도 자동 매칭된 원시값을 통해 인스턴스 생성 가능


// 옵셔널값을 안전하게 벗겨서 사용

if let r = RpsGame(rawValue: 0) {
    print(r)   // rock
}

 

 

3) 연관 값을 가지는 열거형

- 보다 구체적인 하위 정보를 저장하고 사용하고싶을 때 유용함

enum Computer {
    case cpu(core: Int, ghz: Double)
    case ram(Int, String)
    case hardDisk(gb: Int)
}

let myChip1 = Computer.cpu(core: 8, ghz: 3.5)
let myChip3 = Computer.ram(16, "DRAM")
let myChip6 = Computer.hardDisk(gb: 128)

 

* switch문을 활용한 열거형 case 패턴

var chip = Computer.cpu(core: 8, ghz: 2.0)

switch chip {
case .cpu(core: 8, ghz: 3.1):
    print("CPU 8코어 3.1GHz입니다.")
case .ram(32, _):
    print("32기가램 입니다.")
case .ram(_, _):
	print("램 입니다.")
default:
    print("그 이외의 칩에는 관심이 없습니다.")
}

// 그 외의 칩에는 관심이 없습니다

// 연관값을 가진 케이스를 패턴 매칭시키기

switch chip {
case let .cpu(a, b):    // let a = 연관값, let b = 연관값
    print("CPU \(a)코어 \(b)GHz입니다.")
case let .ram(a, _):
    print("램 \(a)기가램 입니다.")
case let .hardDisk(a) :
    print("하드디스크 \(a)기가 용량입니다.")
}

// cpu 8코어 2.GHz 입니다.

 

*if/for 문 활용

var chip = Computer.hardDisk(gb: 128)

if case Computer.hardDisk(gb: let gB) = chip { // case let Computer.hardDisk(gb: gB)
    print("\(gB)기가 바이트 하드디스크임") // let gB = 열거형 내부의 연관값
}

if case Computer.hardDisk(gb: let gB) = chip, gB == 256 {    // if에서의 콤마는 and
   
    print("256기가 바이트 하드디스크임")
}

let chiplists: [Computer] = [
    .cpu(core: 4, ghz: 3.0),
    .cpu(core: 8, ghz: 3.5),
    .ram(16, "SRAM"),
    .ram(32, "DRAM"),
    .cpu(core: 8, ghz: 3.5),
    .hardDisk(gb: 500),
    .hardDisk(gb: 256)
]

for case let .cpu(core: c, ghz: h) in chiplists {    // 배열중에서 특정 케이스만 뽑아서 활용 가능 ⭐️
    print("CPU칩: \(c)코어, \(h)헤르츠")
}

 

⭐️ 열거형에서 원시값과 연관값을 같이 쓰는 것은 불가능하다.

 

4) 옵셔널 열거형 (열거형에 연관값이 없고, 옵셔널 열거형인 경우)

- 옵셔널은 .some(자료형)과 .none (nil과 완전히 동일) 으로 이루어진 열거형이다! (해당 포스팅 참고)

enum Direction {
	case left
    case right
}

let x: Direction? = .left

switch x{
	case .left:
    case .right:
    case nil:
}

// 정식 원칙
switch x{
	case .some(let value):
    	switch value {
            case .left:
            case .right:
        }
    case .none:
 }
let arrays: [Int?] = [nil, 2, 3, nil, 5]

for case let .some(number) in arrays { // nil을 건너뛰겠다는 뜻
    print("Found a \(number)")
}

 

* 옵셔널 패턴 -  switch문

let num: Int? = 7
print(num)


// 1) 열거형 케이스 패턴

switch num {
case .some(let x):      // let x = num
    print(x)
case .none:
    break
}

// 2) 옵셔널 패턴 (.some을 ? 물음표로 대체 가능한 패턴)

switch num {
case let x?:           // let x? = Optional.some(num)
    print(x)
case .none:
    break
}

 

* 옵셔널 패턴 - if/for 문

// 1) 열거형 케이스 패턴

if case .some(let x) = num {
    print(x)
}

// 2) 옵셔널 패턴 (.some을 ? 물음표로 대체 가능한 패턴)

if case let x? = num {        // "옵셔널(?) 물음표를 띄어내고, x 상수로 보자"의 의미라고 생각하면 됨
    print(x)
}


let arrays: [Int?] = [nil, 2, 3, nil, 5]

// 1) 열거형 케이스 패턴

for case .some(let number) in arrays {
    print("Found a \(number)")
}


// 2) 옵셔널 패턴

for case let number? in arrays {
    print("Found a \(number)")
}

 

5) @unknown 키워드

- 최초로 선언 후, 케이스가 늘어날 수도 있는 열거형의 경우에 switch 문에서 모든 열거형을 다루지 않는다면, 노란 경고 라인을 띄워줘서 개발자가 실수를 줄일 수 있도록 도와주는 키워드

enum SNSLogin: String {      // 3가지 케이스 ===> 4가지 케이스
    case email
    case facebook
    case google
    case kakaotalk
}


let userLogin = SNSLogin.email

switch userLogin {
case .email:
    print("이메일 로그인")
case .facebook:
    print("페이스북 로그인")
//case .google:
//    print("구글 로그인")
default:                         // default블럭만 추가해두는 것이 안전할까? ⭐️
    print("구글 로그인")
}


switch userLogin {
case .email:
    print("이메일 로그인")
case .facebook:
    print("페이스북 로그인")
case .google:
    print("구글 로그인")
@unknown default: // 모든 케이스를 다루지 않았다고 경고를 띄워줘서 개발자의 실수를 줄여줌
    print("그 외의 모든 경우")
}
반응형