No.10
- 스터디 준비 자료
컬렉션
- Array, Dictionary, Set
- array
- order(o)
- dictionary
- key, value
- order(x)
- set
- order(x)
- array
Array
var numArr: Array<Int> = [1, 2, 3, 4, 5]
- 순서가 있다는 것이 왜 중요할까?
- Indexing이 가능하다.
- 인덱스 기반으로 요소 접근 (Accessing Elements), 요소 변경 (Modifying Elements), 부분 배열 생성 (Slicing), 범위로 요소 접근 (Accessing Elements with a Range) 등 여러가지 기능이 가능하다.
- 반면에 딕셔너리, 집합은 인덱싱이 안되므로, 위와 같은 기능들을 할 순 없음.
- 배열 선언
// 가변 배열
var numArr: [Int] = [1, 2, 3, 4, 5]
// 정적 배열
let numArr2: [Int] = [1, 2, 3, 4, 5]
- 여러가지 메소드들
numArr.first
numArr.last
numArr.startIndex
numArr.endIndex // count 반환한다고 생각하기
numArr.endIndex.advanced(by: -1) // 끝에서 얼마나 떨어져있는 지
numArr.index(1, offsetBy: 2) // 3
numArr.firstIndex(of: 6) // 있으면 index, 없으면 nil 반환
numArr.insert(7, at: 4) // 4번째 다음 위치에 7 넣기 -> O(n) 시간복잡도
numArr[0...2] = [-3, -2, -1] // 범위를 교체하기
numArr[0...1] = [] // 삭제
numArr.replaceSubrance(0...2, with: [-10, -100, -1000])
numArr.append(100)
numArr.append(contentsOf: [99, 999])
numArr.remove(at: 2) // 삭제하고, 삭제된 요소 리턴
//numArr.remove(at: 18) // 잘못된 인덱스 전달 ====> 에러발생
- 배열을 다루기
- 컬렉션을 직접적으로 변경할때는 동사원형으로 사용
- 컬렉션을 변경하지 않고, 리턴형으로 다른 컬렉션을 반환할때는 분사형태로 사용(-ing / -ted)
- E.g
- 2차원 배열
var data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
data[0][2]
- 반복문에서의 활용
nums = [10, 11, 12, 13, 14]
// (offset: 0, element: 10)
for tuple in nums.enumerated() { // Named Tuple 형태로 변환
//print(tuple)
print("\(tuple.0) - \(tuple.1)")
}
for (index, word) in nums.enumerated() { // 바로 뽑아내기
print("\(index) - \(word)")
}
- Named Tuple?
- Named tuple(이름 있는 튜플)은 튜플의 각 요소에 이름을 명시적으로 지정할 수 있는 Swift 언어의 특별한 형태
// 일반적인 튜플 사용
let coordinates = (x: 10, y: 20)
let xValue = coordinates.0 // 요소에 접근할 때 순서에 의존
// Named tuple 사용
let namedCoordinates = (x: 10, y: 20)
let namedXValue = namedCoordinates.x // 이름으로 요소에 직접 접근
- 흥미로운 포인트
- C언어나 다른 부분에선 일반적으로 배열은 정적 할당이 된다.
- 가변길이의 배열을 선언하기 위해선 힙에다가 따로 선언을 해줘야 함
- Swift는 값타입이라서 스택에 저장이 된다.
- 일반적으로 스택에 저장하다가, 갑자기 한계치를 넘어가면, 힙에다가 저장하도록 내부 메커니즘이 되어 있다.
- By Allen
- 초기화할때 10개까지는 스택에서 사용한다.
- 하지만, 10개를 넘어가서 1개씩 추가하면 그 이후부터는 배로 공간을 늘려서, 20, 40.. 이렇게 2배씩 힙에서 공간이 늘어남. (물론 이때도, 새로이 주소가 할당된다)
- https://developer.apple.com/documentation/swift/array/append(_:)-1ytnt
- 스택에서 늘어날 땐 O(1)의 시간복잡도를, 힙에서 늘어날 땐 O(n)의 시간복잡도를 가진다.
- 💡 값타입이란? : 데이터를 전달할 때 **값을 복사하여 전달하는 타입**
- removeAll() vs []→ 시간복잡도 차이 X, 같다고 생각하면 된다(my opinion)
- https://stackoverflow.com/questions/54133045/performance-array-removeall-vs
- C언어나 다른 부분에선 일반적으로 배열은 정적 할당이 된다.
딕셔너리
- key(hashable), value
- 선언 → 초기 선언 때는 타입 알려주기
var words: [String: String] = [:]
var words1: Dictionary<Int, String>
let emptyDic1: Dictionary<Int, String> = [:]
let emptyDic2 = Dictionary<Int, String>()
let emptyDic3 = [Int: String]()
// words = [:] // 빈 딕셔너리로 만들기
// words = ["A": "A"] // 전체 교체하기(바꾸기)
dic = ["A": "Apple", "B": "Banana", "C": "City"]
dic["B"] = nil // 해당요소 삭제
dic["E"] = nil // 존재하지 않는 키/값을 삭제 ======> 아무일이 일어나지 않음(에러아님)
dic.removeValue(forKey: "A") // 삭제 후, 삭제된 값 리턴
dic.removeValue(forKey: "A") // 다시보면, nil리턴
// 전체 삭제하기
dic.removeAll()
dic.removeAll(keepingCapacity: true)
- 딕셔너리는 기본적으로 서브스크립트[ ]를 이용한 문법을 주로 사용 → key를 이용해 value 확인
- update
- insert, replace, append 모두 기능
- 어차피 순서가 없으니.. key&value 만 넣으면 된다.
- remove
- 삭제
- 딕셔너리 활용
var dict1 = [String: [String]]() // 딕셔너리 밸류에 "배열"이 들어갈 수도 있다.
var dict2 = [String: [String: Int]]() // 딕셔너리 안에 딕셔너리가 들어갈 수 있다.
// 반복문을 이용한 출력
for (key, value) in dict {
print("\(key): \(value)")
}
for (key, _) in dict {
print("Key :", key)
}
for (_, value) in dict {
print("Value :", value)
}
Hash / HashValue / Hashable
[Swift] Hashable 해야 한다? 해쉬값이란? (간단 요약)
- Hash Value
- 데이터를 간단한 숫자로 변환한 것
- 원본 데이터를 해쉬 함수를 사용하여 64bit의 Int값으로 변환한 것
- 2개의 데이터를 비교할 때, 데이터가 동일하면 각 데이터의 해쉬값도 동일
let hi: String = "안녕하세요" let hello: String = "안녕하세요" let apple: String = "애플" print(hi.hashValue) // print(hello.hashValue) // hi와 동일 print(apple.hashValue) // hi, hello와 다른 해시값 가짐
- 하지만 위 명제의 역은 성립하지 않을 수도 있음
- 컴퓨터는 제한된 용량을 가지기 때문에 서로 다른 데이터가 동일한 해시값을 가질 수도 있음.
- 데이터를 간단한 숫자로 변환한 것
- Hashable
- 해당 타입을 해시함수의 input값으로 사용가능한 타입이라는 것
- Hashable 프로토콜 채택
- 해당 타입을 해시함수의 input값으로 사용가능한 타입이라는 것
// Example : Hashable Protocal
struct Drink: Hashable {
let name: String
let mainIngredient: String
let alchol: Double
}
struct RandomData: Hashable {
let string1: String
let string2: String
let double1: Double
}
let lemonade = Drink(name: "lemonade", mainIngredient: "lemon", alchol: 0.0)
let appleCider = Drink(name: "appleCider", mainIngredient: "apple", alchol: 5.0)
if lemonade == appleCider { // Drink 구조체가 Hashable Protocol을 준수하므로 두 인스턴스를 == 연산자로 비교 가능하다.
print("Drinks are the same, because their hash values \(lemonade.hashValue) and \(appleCider.hashValue) are the same.")
} else {
print("Drinks are different, because their hash values \(lemonade.hashValue) and \(appleCider.hashValue) are different.")
}
// 사용자 정의 타입인 구조체 Drink는 원래 인스턴스끼리 == 연산자로 비교할 수 없다.
// 하지만 구조체가 Hashable Protocol을 준수하도록 변경하면, 비교가 가능하다.
- 즉, 해시함수를 사용해 유일한 갑승로 변환이 가능한 타입인지의 여부를 묻는 것
- 검색 알고리즘에서 매우 빠른 시간복잡도를 가짐
Set
- 집합
- 순서가 없고 중복된 값을 가지지 않으므로, 데이터의 고유성을 유지하고 검색과 필터링에 유용하다.
- 검색에 매우 빠른 속도를 보인다.
- 값을 순서대로 가져올 때, 정렬된 배열로 변환해야함.
- 부분 집합, 상위집합, 서로소, 합집합, 차집합, 교집합... 등 다양한 집합 연산을 사용할 수 있음.
- 반복문과의 결합
let iteratingSet: Set = [1, 2, 3]
for num in iteratingSet { // 정렬되지 않은 컬렉션이기 때문에, 실행할때마다 순서가 달라짐
print(num)
}
KeyValuePairs
- 딕셔너리와 유사한 형태이지만, 배열처럼 순서가 있는 컬렉션
- 딕셔너리이지만, 저장된 순서가 중요할 경우, 또는 데이터가 반복될 경우만 임시적/제한적으로 사용
let introduce: KeyValuePairs = ["first": "Hello", "second": "My Name", "third":"is"]
introduce[0]
for value in introduce {
print("\(value.key)는 \(value.value) 입니다.")
}
Copy-On-Write
- 코드상에서 값을 복사해서 담는다 하더라도, 실제 값이 바뀌기 전까지는 그냥 하나의 메모리 값을 공유해서 사용
- 메모리를 적게 차지하기 위해서 스위프트 언어가 알아서 내부에서 처리하는 매커니즘
- 즉, 실제로 값이 복사되지 않고, 동일한 메모리의 주소를 참조하다가, 데이터가 변경되면 메모리를 복사해서 변경할 값을 변경하는 기법
func addcheck(_ object: UnsafeRawPointer) -> String {
let address = Int(bitPattern: object)
return NSString(format: "%p", address) as String
}
let arr1: [Int] = [1, 2, 3]
var arr2 = arr1
// arr1, arr2 메모리 주소 출력
print(addcheck(arr1)) // 1004
print(addcheck(arr2)) // 1004
arr2[0] = 99
print(addcheck(arr1)) // 1004
print(addcheck(arr2)) // 2024
열거형(Enumeration)
- 왜 쓸까?
- 열거형을 사용하면 코드의 가독성과 안정성이 높아짐
- 즉, 명확한 분기 처리가 가능함.
- 대부분 switch문으로 분기 처리함.
- 열거형도 하나의 타입이다.
- 원시값
var today: Weekday? = Weekday(rawValue: 0) // 원시값으로 쉽게 인스턴스 생성 가능
// var today: Weekday? = Weekday.monday
// 인스턴스 생성시 - 옵셔널타입으로 리턴 (실패가능)
- 연관값
- 보다 구체적인 (하위) 정보를 저장하기 위해, 정의 가능
- 옵셔널 타입의 깊은 이해
enum Optional<Wrapped> {
case some(Wrapped) // 구체적인 정보가 Int값에 들어 있는 것
case none // 구체적인 정보에 값이 없음
}
- 옵셔널 열거형의 경우
- 옵셔널 자체가 열거형으로 선언되어 있고, 옵셔널 열거형으로 선언하는 경우
- 열거형을 감싸는 열거형의 형태가 됨
enum SomeEnum {
case left
case right
}
// 타입을 다시 옵셔널 열거형으로 선언 ⭐️
let x: SomeEnum? = .left
/*
[SomeEnum?의 의미] 옵셔널 열거형
- 값이 있는 경우 .some ===> 내부에 다시 열거형 .left /.right (이중 보완)
- 값이 없는 경우 .none ===> nil
*/
// 원칙
switch x {
case .some(let value): // Optional.some(let value) = Optional.some(SomeEnum.left) ⭐️
switch value {
case .left:
print("왼쪽으로 돌기")
case .right:
print("오른쪽으로 돌기")
}
case .none:
print("계속 전진")
}
// 편의적 기능 제공 ⭐️
switch x {
case .some(.left): // == Optional.some(SomeEnum.left):
print("왼쪽으로 돌기")
case .some(.right):
print("오른쪽으로 돌기")
case nil:
print("계속 전진")
}
// 스위치문은 옵셔널 열거형 타입의 사용시, 편의성을 제공
// - switch문에서 옵셔널 열거형 타입을 사용할때, 벗기지 않아도 내부값 접근가능
switch x { // 예전에는 x! 라고 써줘야 했음 (스위치문에서 옵셔널 타입도 ok)
case .left:
print("왼쪽으로 돌기")
case .right:
print("오른쪽으로 돌기")
case nil:
print("계속 전진")
}
- 열거형에 연관값이 있는 경우
- 연관값(Associated Values)이 있는 경우와 switch문
enum Computer { // 3가지로 정의
case cpu(core: Int, ghz: Double)
case ram(Int, String)
case hardDisk(gb: Int)
}
var chip = Computer.cpu(core: 8, ghz: 3.1)
// 1. switch문
switch chip { // 수십가지로도 처리 가능 (필요한 처리 확장 가능)
case .cpu(core: 8, ghz: 3.1):
print("CPU 8코어 3.1GHz입니다.")
case .cpu(core: 8, ghz: 2.6):
print("CPU 8코어 2.6GHz입니다.")
case .cpu(core: 4, ghz: let ghz): // let ghz = 연관값 (cpu가 4코어인 경우, ghz에 저장된 연관값을 꺼내서 바인딩)
print("CPU 4코어 \(ghz)HGz입니다.")
case .cpu(core: _, ghz: _):
print("CPU 칩 입니다.")
case .ram(32, _):
print("32기가램 입니다.")
case .ram(_, _):
print("램 입니다.")
case let .hardDisk(gb: gB): // let gB = 연관값
print("\(gB)기가 바이트 하드디스크 입니다.")
default: // 대부분 default문이 필요하기도 함
print("그 외에 나머지는 관심이 없습니다. 그렇지만 칩이긴 합니다.")
}
- 연관값(Associated Values)이 있는 경우, if / guard / for-in / while 문
- 특정 케이스만 다루기 위해서 if문이나 반복문(for문) 사용 가능
if case Computer.hardDisk(gb: let gB) = chip {
print("\(gB)기가 바이트 하드디스크임")
}
if case Computer.hardDisk(gb: let gB) = chip , gB == 256 { // 처리를 다양하게 활용 가능
print("256기가 바이트 하드디스크임")
}
let chiplists: [Computer] = [
.cpu(core: 4, ghz: 3.0), // 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)헤르츠")
}
// 옵셔널 타입을 포함하는 배열에서 반복문을 사용하는 경우
let arrays: [Int?] = [nil, 2, 3, nil, 5]
for case let .some(number) in arrays {
print("Found a \(number)")
}
옵셔널 패턴
- 옵셔널의 두가지 케이스 중 “.some” 케이스의 연관 값을 상수 또는 변수에 매칭하는 것을 말함.
- 옵셔널 패턴은 식별자 패턴 뒤에 물음표를 붙이고 열거형 케이스 패턴이 있는 자리에 표기하면 된다.
switch num {
case .some(let x): // Optional.some(let x) = Optional.some(num)
print(x)
case .none:
print("nil")
}
switch num {
case let x?: // let x? = Optional.some(num), ? 는 옵셔널의 의미이므로..
print(x) // ?를 떼어내면 -> let x = num
case nil:
print("nil")
}
if case .some(let x) = optionalValue {
print(x)
}
if case let x? = optionalValue { // let x? = Optional.some(value)
print(x) // let x = value
}
@unknown 키워드
- switch문에서 열거형의 모든 케이스를 다루지 않는 경우, 스위치문에서 모든 열거형의 케이스를 다루지 않았다고 경고로 알려주어 개발자의 실수 가능성을 컴파일 시점에 알려주는 것
switch num {
case ...
//
@unknown default:
print("etc 경우"
}
반응형
'iOS > Swift' 카테고리의 다른 글
Swift No.12 (0) | 2024.01.13 |
---|---|
Swift No.11 (0) | 2024.01.13 |
Swift No.9 (0) | 2024.01.10 |
Swift No.8 (1) | 2024.01.09 |
Swift Day 6 (0) | 2024.01.09 |