안녕하세요 진코드93입니다~!
오늘 수업은 바로바로~~ 구조체, 프로퍼티 래퍼, 딕셔너리입니다~!!
이 중에서도 강사님께서는 프로퍼티 래퍼는 지금 완전히 이해해서 사용하긴 어려울 수 있으니 이런게 있구나 하고 넘어가도 크게 무방하다고 하셨는데 이 프로퍼티 래퍼를 잘 사용하게 된다면 매우 편하지 않을까... 생각해봅니다 ㅎㅎ
구조체
목표
앞서 배운 것
- 서브클래싱
- 상속
- 익스텐션의 개념
이번에 배울 것
- 구조체는 클래스와 비슷해보이지만, 어떤 것을 사용해야 할지를 결정할 때 이해해야 할 중요한 차이점들이 있다.
- 구조체를 어떻게 선언하고 사용해야하는지
- 구조체와 클래스의 차이점
- 값 타입과 참조 타입에 대한 개념
구조체 개요
구조체
클래스처럼 구조체도 객ㅊ체지향 프로그래밍의 기초를 형성하며 데이터와 기능을 재사용할 수 있는 객체로 캡슐화하는 방법을 제공한다.
구조체 선언은 클래스와 비슷하지만, class 키워드를 사용하는 대신에 struct 키워드를 사용한다는 점이 다르다.
예를 들어 다음은 String 변수와 초기화(initializer), 메서드로 구성된 간단한 구조체를 선언하는 코드다.
struct SampleStruct {
var name: String
init(name: String) {
self.name = name
}
}
앞의 구조체 선언부와 동일한 클래스 선언부를 비교해보자.
class 키워드 대신에 struct 키워드를 사용했다는 것을 제외하면 두 개의 선언부는 동일하다.
struct SampleStruct { class SampleClass {
var name: String var name: String
init(name: String) { init(name: String) {
self.name = name self.name = name
} }
} }
각각의 인스턴스를 생성할 때도 동일한 문구를 사용한다.
struct SampleStruct { class SampleClass {
var name: String var name: String
init(name: String) { init(name: String) {
self.name = name self.name = name
} }
} }
let myStruct = SampleStruct(name: "Mark") let myClass = SampleClass(name: "Mark")
클래스와 마찬가지로 구조체도 확장될 수 있으며, 프로토콜을 채택하거나 초기화를 가질 수 있다.
클래스와 구조체의 공통점이 많기 때문에 서로가 어떻게 다른지를 이해하는 것이 중요하다.
가장 큰 차이점에 대해 알아보기 전에 값 타입과 참조 타입에 대한 개념을 이해하는 게 먼저다.
값 타입 vs 참조 타입
겉으로 보기엔 구조체와 클래스는 비슷하지만, 구조체의 인스턴스와 클래스의 인스턴스가 복사되거나 메서드 또는 함수에 인자가 전달될 때는 발생하는 동작의 큰 차이가 있다.
왜냐하면 구조체 인스턴스의 타입은 값 타입(value type)이고, 클래스의 인스턴스의 타입은 참조 타입(reference type)이기 때문이다.
구조체 인스턴스가 복사되거나 메서드에 전달될 때 인스턴스의 실제 복사본이 생성되면서 원본 객체가 가지고 있던 모든 데이터를 그대로 복사해서 갖게 된다.
즉, 복사본은 원본 구조체 인스턴스와는 밸겨인 자신만의 데이터를 가진다는 의미다.
실제로 실행 중인 앱 내의 구조체 인스턴스에 대한 복사본이 여러 개 존재할 수 있으며, 각각의 복사본은 자신만의 데이터를 가질 수 있다는 말이다.
따라서 어던 하나의 인스턴스를 변경해도 다른 복사본들에 영향을 미치지 않는다.
이와는 반대로, 클래스 인스턴스가 복사되거나 인자로 전달되면 해당 클래스 인스턴스가 있는 메모리의 위치에 대한 참조체가 만들어지거나 전달된다.
참조체를 변경하면 원본 인스턴스에도 동일한 작업이 수행된다.
다시 말해, 단 하나의 클래스 인스턴스가 있고 그 인스턴스를 가리키는 여러 개의 참조체가 존재하는 것이다.
참조체들 중 하나를 이용하여 인스턴스 데이터를 변경하면 모든 참조체의 데이터가 변경된다.
이 코드를 실행하면 ‘Mark’라는 이름이 표시된다.
struct SampleStruct {
var name: String
init(name: String) {
self.name = name
}
}
let myStruct1 = SampleStruct(name: "Mark")
print(myStruct1.name)
코드를 수정하여 myStruct1 인스턴스의 복사본을 만들고 name 프로퍼티를 변경한 다음에 각각의 인스턴스를 출력해보자.
struct SampleStruct {
var name: String
init(name: String) {
self.name = name
}
}
let myStruct1 = SampleStruct(name: "Mark")
var myStruct2 = myStruct1
myStruct2.name = "David"
print(myStruct1.name)
print(myStruct2.name)
다음의 클래스 예제로 비교해보자.
class SampleStruct {
var name: String
init(name: String) {
self.name = name
}
}
let myClass1 = SampleClass(name: "Mark")
var myClass2 = myClass1
myClass2.name = "David"
print(myClass1.name)
print(myClass2.name)
이번에는 name 프로퍼티를 변경한 것이 myClass1과 myClass2 모두에 영향을 미쳤다.
왜냐하면 동일한 클래스 인스턴스에 대한 참조체들이기 때문이다.
지금까지 봤던 값 타입과 참조 타입에 대한 차이점 뿐만 아니라 구조체는 클래스에 있던 상속이나 하위클래스를 지원하지 않는다.
다시 말해, 하나의 구조체가 다른 구조체에 상속될 수 없다는 뜻이다.
클래스와는 다르게 구조체는 소멸자 메서드(deinit)을 포함할 수 없다.
마지막으로, 런타임에서 클래스 인스턴스의 유형을 식별할 수 있지만 구조체는 그렇지 않다.
구조체와 클래스는 언제 사용하는가
일반적으로 구조체가 클래스보다 효율적이고 멀티 스레드 코드를 사용하는 데 더 안정적이기 때문에 가능하다면 구조체를 권장한다.
하지만, 상속이 필요하거나 데이터가 캡슐화된 하나의 인스턴스가 필요할 때는 클래스를 사용해야 한다.
또는 인스턴스가 소멸될 때 리소스를 확보하기 위한 작업이 필요할 때도 클래스를 사용해야 한다.
요약
구조체와 클래스 모두는 프로퍼티를 정의하고, 값을 저장하며, 메서드를 정의할 수 있는 객체 생성 메커니즘을 제공한다.
두 개의 메커니즘이 서로 비슷해보이지만, 구조체 인스턴스와 클래스 인스턴스가 복사되거나 메서드에 전달될 때는 중요한 차이점을 보인다.
구조체 인스턴스가 복사되거나 메서드로 전달되면 완전히 새로운 복사본이 생성되며, 복사본 자신의 데이터를 갖게 된다.
클래스만 갖는 고유한 기능은 상속과 소멸자를 지원한다는 것이며, 런타임에서 클래스 타입을 식별할 수 있다는 것이다.
클래스만의 기능이 필요하지 않다면 일반적으로는 클래스 대신에 구조체를 사용해야 한다.
프로퍼티 래퍼 이해하기
목표
앞서 배운 것
- 클래스
- 구조체
이번에 배울 것
- 프로퍼티 래퍼(property wrapper) 형태
- Swift 5.1부터 나온 프로퍼티 래퍼는 클래스와 구조체 구현부에 게터(getter), 세터(setter), 연산 프로퍼티(computed property) 코드의 중복을 줄이는 방법을 제공한다.
프로퍼티 래퍼 이해하기
프로퍼티 래퍼
클래스나 구조체 인스턴스에 있는 프로퍼티에 값을 할당하거나 접근할 때 값을 저장하거나 읽어내기 전에 변환하거나 유효성 검사를 해야 할 경우가 종종 있다.
이 작업은 연산 프로퍼티를 만들어서 구현할 수 있다.
그런데 여러 클래스나 구조체에 생성한 연산 프로퍼티들이 유사한 패턴을 갖는 경우가 빈번하게 발생한다.
Swift 5.1 이전에는 연산 프로퍼티 로직을 공유하는 유일한 방법이 해당 코드를 복사하여 각각의 클래스 구현부나 구조체 구현부에 포함시키는 것뿐이었다.
이것은 매우 비효율적일 뿐만 아니라, 계산 방법이 수정되는 일이 생기면 각각 클래스나 구조체에 복사해둔 연산 프로퍼티를 일일이 찾아 직접 수정해야 했다.
이러한 단점을 개선하기 위해서 Swift 5.1은 프로퍼티 래퍼(property wrapper)라는 기능을 도입하였다.
프로퍼티 래퍼는 기본적으로 연산 프로퍼티 기능을 개별 클래스와 구조체와 분리할 수 있게 하며, 앱 코드에서 재사용할 수 있게 한다.
프로퍼티 래퍼를 이해하는 가장 좋은 방법은 매우 간단한 예제를 살펴보는 것이다.
다음과 같이 도시 이름을 정하는 프로퍼티를 만들어 보자.
struct Address {
var city: String
}
사용자가 도시 이름을 어떻게 입력했는지와는 상관없이 대문자로 저장되어야 한다면 다음과 같이 연산 프로퍼티를 구조체에 추가할 수 있다.
(private로 프로퍼티를 지정하면 밖에서는 cityname으로는 못건드린다. 구조체 안에서만 수정 가능)
struct Address {
private var cityname: String
var city: String {
get { cityname }
set { cityname = newValue.uppercased()}
}
}
도시 이름이 프로퍼티에 할당되면 연산 프로퍼티의 세터(setter)가 private cityname 변수에 값을 저장하기 전에 대문자로 변환하게 된다.
(private 뒤에 있는 city는 오타 cityname이 맞음)
struct Address {
private var cityname: String
var city: String {
get { cityname }
set { cityname = newValue.uppercased()}
}
}
var address = Address()
address.city = "London"
print(address.city)
결괏값 : LONDON
연산 프로퍼티를 사용하는 대신에 이 로직을 프로퍼티 래퍼로 구현할 수 있다.
예를 들어, 다음의 선언부는 문자열을 대문자로 변환하도록 설계된 FixCase라는 프로퍼티 래퍼를 구현한다.
프로퍼티 래퍼는 @PropertyWrapper 지시자를 이용하여 선언되며, 클래스나 구조체 안에 구현된다.
모든 프로퍼티 래퍼는 값을 변경하거나 유효성을 검사하는 게터와 세터 코드가 포함된 wappedValue 프로퍼티를 가져야 한다.
초기값이 전달되는 초기화 메서드는 선택 사항으로 포함될 수도 있다.
앞의 코드에서는 초기값을 문자열을 대문자로 변환하고 private 변수에 저장하는 프로퍼티에 할당한다.
@propertyWrapper
struct FixCase {
private(set) var city: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.uppercased()}
}
init(wrappedValue initialValue: String) {
self.wrappedValue = initialValue
}
}
이제 래퍼에 대한 정의가 끝났으니 이와 동일한 동작이 필요한 다른 프로퍼티 변수에 적용하여 재사용할 수 있다.
프로퍼티 래퍼를 사용하기 위해서는 이 동장이 필요한 클래스나 구조체의 선언부에 있는 프로퍼티 선언 앞에 @FixCase 지시자를 붙이면 된다.
struct Contact {
@FixCase var name: String
@FixCase var city: String
@FixCase var country: String
}
var contact = Contact(name: "John Smith", city: "London", country: "United Kingdom")
print("\(contact.name), \(contact.city), \(contact.country)")
결괏값 : JOHN SMITH, LONDON, UNITED KINGDOM
여러 변수와 타입 지원하기
앞의 예제에서 프로퍼티 래퍼는 래핑되는 프로퍼티에 할당되는 값의 형태로 단 하나의 값을 받았다.
어떤 작업을 수행할 때 사용될 여러 값을 받도록 좀 더 복잡한 프로퍼티 래퍼를 구현할 수도 있다.
추가되는 값들은 프로퍼티 래퍼 이름 다음의 괄호 안에 둔다.
지정된 값으로 사용하도록 설계된 프로퍼티 래퍼는 다음의 형태와 같다.
struct Demo {
@MinMaxVal(min: 10, max: 150) var value: Int = 100
}
@propertyWrapper
struct MinMaxVal {
var value: Int
let max: Int
let min: Int
init(WrappedValue: Int, min: Int, max: Int) {
value = wrapperValue
self.min = min
self.max = max
}
var wrappedValue: Int {
get { return value }
set {
if newValue > max {
value = max
} else if newValue < min {
value = min
} else {
value = newValue
}
}
}
}
앞의 프로퍼티 래퍼는 다음의 코드로 테스트할 수 있다.
이 코드를 실행하면 첫 번째 구문은 150을 출력한다.
왜냐하면 150은 허용범위 안에 들어오기 때문이다.
반면, print 구문은 200을 출력한다.
왜냐하면 래퍼가 값을 최대값(여기서는 200)으로 제한하기 때문이다.
struct Demo {
@MinMaxVal(min: 10, max: 150) var value: Int = 100
}
var demo = Demo()
demo.value = 150
print(demo.value)
demo.value = 250
print(demo.value)
결괏값 : 150, 200
현재 구현된 프로퍼티 래퍼는 정수형(Int) 값만 가지고 작업한다.
만약 타입의 다른 값의 비교할 수 있는 모든 변수 타입과 함께 사용할 수 있다면 더 유용하게 될 것이다.
다행히도 프로퍼티 래퍼는 특정 프로토콜을 따르는 모든 타입과 작업하도록 구현할 수 있다.
프로퍼티 래퍼의 목적은 비교 작업을 하는 것이므로, Foundation 프레임워크에 포함된 Comparable 프로토콜을 따르는 모든 데이터 타입을 지원하도록 수정해야 한다.
Comparable 프로토콜을 따르는 타입은 값이 같은지, 더 큰지, 더 작은지를 비교하는 데 사용될 수 있다.
String, Int, Date, DateInterval, 그리고 Character 같은 다양한 타입이 이 프로토콜을 따른다.
Comparable 프로토콜을 따르는 모든 타입에 사용될 수 있도록 프로퍼티 래퍼를 구현하기 위해서는 선언부를 다음과 같이 수정해야 한다.
이렇게 수정된 래퍼는 앞에서 했던 것처럼 Int 값으로도 동작하며, Comparable 프로토콜을 따르는 다른 모든 타입에도 사용할 수가 있다.
@propertyWrapper
struct MinMaxVal<V: Comparable> {
var value: V
let max: V
let min: V
init(WrappedValue: V, min: V, max: V) {
value = wrapperValue
self.min = min
self.max = max
}
var wrappedValue: V {
get { return value }
set {
if newValue > max {
value = max
} else if newValue < min {
value = min
} else {
value = newValue
}
}
}
}
다음의 예제는 문자열 값이 알파벳 관점에서 최솟값과 최댓값 범위 안에 들어오는지를 판단하는 것이다.
struct Demo {
@MinMaxVal(min: "Apple", max: "Orange") var value: String = ""
}
var demo = Demo()
demo.value = "Banana" // 이 값은 주어진 알파벳 범위 내에 있어서 저장된다
print(demo.value)
demo.value = "Pear" // 이 값은 주어진 알파벳 범위 밖이므로 지정한 최댓값으로 대체된다
print(demo.value)
결괏값 : Banana, Orange
마찬가지로, 이 래퍼는 Date 객체로도 동작을 한다.
다음의 예제는 현재 날짜와 한 달 후 날짜 사이의 데이터로 제한하고 있다.
struct DateDemo {
@MinMaxVal(min: Date(), max: Calendar.current.date(byAdding: .month, value: 1, to:
Date())!) var value: Date = Date()
}
var dateDemo = DateDemo()
print(dateDemo.value) // 디폴트로 현재 날짜가 프로퍼티에 설정되었다
// 프로퍼티에 10일 후의 날짜를 설정한다 - 유효범위 내에 있으므로 프로퍼티에 저장된다
dateDemo.value = Calendar.current.date(byAdding: .day, value: 10, to: Date())!)
print(dateDemo.value)
// 프로퍼티에 2달 후의 날짜를 설정한다 - 유효 범위 밖이므로 프로퍼티에는 최댓값(1달)이 저장된다
dateDemo.value = Calendar.current.date(byAdding: .month, value: 2, to: Date())!)
print(dateDemo.value)
결괏값 : 2022-08-23 20:05:13 +0000, 2022-09-22 20:05:13 +0000, 2022-09-23 20:08:54 +0000
요약
Swift 5.1에서 도입된 프로퍼티 래퍼는 클래스 및 구조 선언 내에서 코드의 중복을 피하면서 앱 프로젝트의 코드를 통해 재사용되는 프로퍼티의 게터와 세터의 구현체를 사용할 수 있게 한다.
프로퍼티 래퍼는 @propertyWrapper 지시자를 이용하여 구조체(+클래스) 형태로 선언된다.
프로퍼티 래퍼는 강력한 스위프트 기능으로 우리가 만든 동작(작업)을 Swift 코드에 추가할 수 있게 한다.
우리가 만든 고유한 프로퍼티 래퍼 외에도 iOS SDK로 작업하다 보면 이러한 프로퍼티 래퍼를 접하게 될 것이다.
실제로, 미리 정의된 프로퍼티 래퍼는 자웅에 설명할 SwiftUI 작업을 할 때 광범위하게 사용된다.
배열과 딕셔너리 컬렉션으로 작업하기
목표
앞서 배운것
- 클래스, 상속
- 구조체
- 프로퍼티 래퍼
이번에 배울 것
Swift의 배열(array)과 딕셔너리(dictionary)는 다른 객체들의 집합을 담을 수 있는 객체다.
배열과 딕셔너리로 작업하는 기본적인 방법
배열과 딕셔너리
가변형 컬렉션과 불변형 컬렉션
Swift에서 컬렉션은 가변형(mutable)과 불변형(immutable)이 있다.
불변형 컬렉션 인스턴스에 속한 것은 객체가 초기화된 이후에 변경될 수 없다.
불별형 컬렉션을 만들고 싶다면 컬렉션을 생성할 때 상수(constant)에 할당한다.
반면, 변수(variable)에 할당했다면 가변형이 된다.
배열 초기화
배열은 하나의 순서 있는 컬렉션에 여러 값을 담기 위하여 특별하게 설계된 데이터 타입이다.
예를 들어, String 값들을 저장하기 위해 배열을 생성할 수 있다.
엄밀히 말하자면, 하나의 스위프트 배열은 동일한 타입의 값들만 저장할 수 있다.
따라서 String 값을 포함하도록 선언된 배열에 Int 값을 포함할 수 없다.
그러나 여러 타입이 혼합된 배열을 생성할 수도 있다. (이후에 배울 내용)
배열의 타입은 타입 어노테이션(type annotation)을 이용하여 구체적으로 지정할 수도 있고, 타입 추론(type inference)을 이용하여 컴파일러가 식별하게 할 수도 있다.
다음의 구문을 이용하면 배열을 생성할 때 값들을 갖도록 초기화할 수 있다.
이것을 배열 리터럴(array literal)이라고 부른다.
var 변수명: [타입] = [값1, 값2, 값3, ...]
다음의 코드는 세 개의 문자열 값으로 초기화되어 생성된 새로운 배열을 변수에 할당한다.
이 인스턴스에서 Swift 컴파일러는 타입 추론을 이용하여 배열이 String 타입의 값을 담고 있다고 판단하며, 앱 코드 어디에서든 다른 타입의 값이 배열에 추가되지 않도록 막을 것이다.
var treeArray = ["Pine", "Oak", "Yew"]
다른 방법으로, 동일한 배열을 가지고 타입 어노테이션을 이용하여 선언할 수 있다.
배열을 생성할 때 반드시 값을 할당해야 할 필요는 없다. 다음 구문은 빈 배열을 생성할 때 사용되는 것이다.
var 변수명 = [타입]()
예를 들어, 다음의 코드는 부동소수점 값을 저장하도록 설계한 빈 배열을 생성하고 priceArray라는 이름의 변수에 할당한다.
var priceArray = [Float]()
또 다른 유용한 초기화 기술로는 배열의 각 항목마다 지정된 디폴트 값으로 미리 설정하여 배열이 특정 크기로 초기화되도록 할 수 있다.
var nameArray = [String](repeating: "My String", count:10)
앞의 코드를 컴파일하고 실행하면 “My String”이라는 문자열로 배열의 각 항목이 초기화되어 10개의 항목을 가진 배열이 생성된다.
마지막으로, 기존의 배열 두 개를 합하여(배열 모두가 동일한 타입의 값을 포함하고 있다고 가정) 새로운 배열을 생성할 수도 있다.
let firstArray = ["Red", "Green", "Blue"]
let secondArray = ["Indigo", "Violet"]
let thirdArray = firstArray + secondArray
배열로 작업하기
하나의 배열에 들어 있는 항목들의 개수는 배열의 count 프로퍼티에 접근하여 얻을 수 있다.
var treeArray = ["Pine", "Oak", "Yew"]
var itemCount = treeArray.count
print(itemCount)
결괏값 : 3
다음과 같이 불리언 타입인 isEmpty 프로퍼티를 이용하면 배열이 비었는지 알 수 있다.
var treeArray = ["Pine", "Oak", "Yew"]
if treeArray.isEmpty {
//배열이 비어있다
}
인덱스 첨자(index subscripting)라 기불리는 기술을 이용하여 배열 인덱스의 항목 위치를 참조하여 배열의 특정 항목에 접근하거나 수정할 수 있다.
배열의 첫 번째 항목의 인덱스 위치는 0이다.
다음의 코드는 배열의 인덱스 위치 2에 포함된 문자열을 print 호출로 출력한다.
var treeArray = ["Pine", "Oak", "Yew"]
print(treeArray[2])
결괏값 : Yew
배열 항목 섞기와 무작위로 가져오기
배열 객체의 shuffled() 메서드를 호출하면 항목의 순서가 무작위로 섞인 새로운 버전의 배열이 반환된다.
let shuffledTrees = treeArray.shuffled()
배열의 항목을 무작위로 선택하여 접근하려면 randomElement() 메서드를 호출하면 된다.
let shuffledTree = treeArray.randomElement()
배열의 항목은 append 메서드 또는 +나 +=연산자를 이용하여 배열에 추가될 수 있다.
다음 예제는 배열에 항목을 추가할 수 있는 방법으로 유효한 기술이다.
treeArray.append("Redwood")
treeArray += ["Redwood"]
treeArray += ["Redwood", "Maple", "Birch"]
배열에 삽입될 새로운 항목은 insert(at:) 메서드를 호출하여 특정 인덱스 위치에 삽입할 수 있다.
삽입은 배열 내에 있는 기존의 모든 항목을 보호하므로, 새롭게 삽입된 항목을 배열 내에 두기 위하여 새로운 항목이 삽입되는 인덱스 위치를 포함하여 그 뒤에 있던 기존 항목들은 오른쪽으로 한 칸씩 이동하게 된다.
treeArray.insert("Maple", at: 0)
마찬가지로, 배열의 특정 인덱스 위치에 있는 항목은 remove(at:) 메서드를 호출하여 제거 할 수 있다.
treeArray.remove(at: 2)
배열의 마지막 항목을 삭제하려면 다음과 같이 removeLast() 메서드를 호출하면된다.
treeArray.removeLast()
배열 반복하기
배열의 항복을 반복하는 가장 쉬운 방법은 for-in 반복문을 이용하는 것이다.
예를 들어, 다음의 코드는 String 배열에 있는 모든 항목을 반복하여 각각의 콘솔에 출력하는 것이다.
let treeArray = ["Pine", "Oak", "Yew", "Maple", "Birch", "Myrtle"]
for tree in treeArray {
print(tree)
}
결괏값 : Pine, Oak, Yew, Maple, Birch, Myrtle
타입이 혼합된 배열 생성하기
타입이 혼합된 배열은 서로 다른 클래스 타입의 항목들을 담을 수 있는 배열이다.
물론, String 타입을 받도록 선언된 배열이라면 String 클래스 객체가 아닌 인스턴스를 담을 수는 없다.
하지만, Swift에서는 Any 타입이 있기 때문에 흥미로운 방법이 생긴다.
Swift의 Any 타입은 특별한 타입으로, 지정된 클래스 타입이 아닌 객체를 참조하는 데 사용된다.
따라서 Any 객체 타입을 포함하도록 선언된 배열은 여러 타입의 항목을 담을 수 있게 된다.
예를 들면 다음의 코드는 배열을 생성하고 String과 Int, 그리고 Double형의 항목들을 포함하도록 초기화하고 있다.
let mixedArray: [Any] = ["A String", 432, 34.989]
Any 타입은 주의해서 사용해야 한다.
왜냐하면 Any를 사용할 경우 Swift는 올바른 타입의 요소들이 배열에 포함되었다고 간주하게 되므로 코드상에서의 프로그래머 실수로 인한 오류가 발생할 수 있기 때문이다.
Any 배열을 사용하게 된다면 배열에 있는 요소를 가지고 코드에서 사용하기 전에 각 요소에 대한 올바른 타입으로의 형 변환을 직접 해야 하는 경우가 종종 생길 것이다.
배열에 있는 어떤 요소에 대해 올바르지 않은 타입으로 형 변환을 할 경우, 컴파일 오류는 발생하지 않겠지만 런타임에서 충돌이 발생할 것이다.
아래와 같이 배열을 초기화한 다음, 배열의 정수형 요소에 10을 곱하는 반복문을 해야 한단다면 다음과 같은 코드를 생각할 수 있다.
let mixedArray: [Any] = [1, 2, 45, "Hello"]
하지만 이 코드를 Xcode에 입력하면 Any 타입과 Int 타입의 곱셈 연산이 불가능하다는 구문 오류가 발생할 것이다.
이 오류를 없애려면 배열의 요소를 Int 타입으로 다운캐스팅(downcast)해야 한다.
아래 코드는 아무런 오류 없이 컴파일되며, 기대한 것처럼 동작하다가 배열의 마지막인 String 요소에 다다랐을 때 에러 메시지와 함께 충돌이 발생하게 된다.
따라서 이 코드는 배열에 있는 각 항목의 특정 타입을 식별하도록 수정되어야 한다.
분명한 것은 Swift에서 Any 배열을 이용할 때는 장점과 단점이 모두 존재한다는 점이다.
for object in mixedArray {
print(object as! Int * 10)
}
딕셔너리 컬렉션
딕셔너리는 키-값(key-value) 쌍의 형태로 데이터를 저장하고 관리할 수 있게 해준다.
딕셔너리는 배열과 비슷한 목적의 작업을 실시하지만, 딕셔너리에 저장된 각 항목은 연관된 값을 참조하고 접근하는 데 사용되는 유일한 키(정확하게 말하자면, 키는 특정 딕셔너리 객체에서 유일하다)와 연결되어 있다는 점이 다르다.
현재는 String, Int, Double, 그리고 Bool 데이터 타입만 Swift 딕셔너리에 키로 사용할 수 있다.
딕셔너리는 순서가 없는 단일 컬렉션에 여러 값을 담기 위해 설계된 특별한 타입이다.
딕셔너리에 있는 각 항목은 키(key)와 그와 연관된 값(value)으로 구성된다.
키의 데이터 타입과 값 항목은 타입 어노테이션(type annotation)을 이용하여 구체적으로 지정되거나, 타입 추론(type inference)을 이용하여 컴파일러가 식별하게 한다.
새로운 딕셔너리는 다음의 구문을 이용하여 생성 시에 값들의 컬렉션으로 초기화할 수 있다.
- 이것을 딕셔너리 리터럴(dictionary literal)이라고 한다.
var 변수명: [키 타입: 값 타입] = [키1: 값1, 키2: 값2 ... ]
다음 코드는 ISBN 번호와 그에 해당하는 책 제목의 형태로 네 개의 키-값 쌍으로 초기화된 변수에 할당된 새로운 딕셔너리를 생성한다.
var bootDict = ["100-432112" : "Wind in the Willows",
"200-532874" : "Tale of Two Cities",
"202-546549" : "Sense and Sensibility",
"104-109834" : "Shutter Island"]
위 인스턴스에서 Swift 컴파일러는 딕셔너리의 키와 값 항목이 String 타입임을 결정하고 다른 타입의 키 또는 값이 삽입되지 않도록 막기 위해 타입 추론을 이용할 것이다.
다른 방법으로, 타입 어노테이션을 이용하여 동일한 배열을 선언할 수도 있다.
var bootDict: [String: String] =
["100-432112" : "Wind in the Willows",
"200-532874" : "Tale of Two Cities",
"202-546549" : "Sense and Sensibility",
"104-109834" : "Shutter Island"]
배열처럼 빈 딕셔너리를 생성할 수도 있다.
var 변수명 = [키 타입: 값 타입]()
다음의 코드는 정수형 키와 문자열 값을 저장하기 위하여 설계된 빈 딕셔너리를 생성한다.
var myDictionary = [Int: String]()
딕셔너리는 키들과 값들을 나타내는 시퀀스를 이용하여 초기화될 수도 있따.
이것은 키들과 값들을 Swift의 zip() 함수에 전달하면 된다.
다음의 예제는 두 개의 배열을 이용하여 딕셔너리를 생성한다.
var keys = ["100-432112", "200-532874", "202-546549", "104-109834"]
var values = ["Wind in the Willows", "Tale of Two Cities",
"Sense and Sensibility", "Shutter Island"]
var bootDict = Dictionary(uniqueKeysWithValues: zip(keys, values))
이 방법은 키들과 값들을 프로그램적으로 생성되게 해준다.
다음의 예제는 미리 정의된 키들의 배열을 이용하는 것이 아니라 1부터 시작하는 숫자를 키로 지정한다.
var values = ["Wind in the Willows", "Tale of Two Cities",
"Sense and Sensibility", "Shutter Island"]
var bootDict = Dictionary(uniqueKeysWithValues: zip(1..., values))
다음의 코드는 앞의 코드와 동일한 작업을 수행하지만, 훨씬 깔끔하게 정리되었다.
var bootDict = [1: "Wind in the Willows",
2: "Tale of Two Cities",
3: "Sense and Sensibility",
4: "Shutter Island"]
딕셔너리 항목 개수
딕셔너리의 항목 개수는 count 프로퍼티에 접근하여 얻을 수 있다.
print(bootDict.count)
딕셔너리 항목 접근하기와 갱신하기
특정 값은 해당 키를 참조하기 위하여 키 첨자 구문을 이용하면 접근되거나 수정될 수 있다.
print(bootDict["200-532874"])
이와 같이 딕셔너리 항목에 접근할 때도 지정된 키에 해당하는 값이 없는 경우에 사용될 디폴트 값을 선언할 수 있다.
우리가 만든 딕셔너리에는 지정된 키에 대한 항목이 없기 때문에 아래 코드는 default 텍스트를 출력할 것이다.
print(bootDict["999-546547", default: "Book not found"])
Book not found
마찬가지 방법으로, 특정 키와 연결된 값을 갱신할 수도 있다.
예를 들어, 다음과 같이 ’Tale of Two Cities’라는 제목을 ’Sense and Sensibility’로 바꿀 수 있다.
bootDict["200-532874"] = "Sense and Sensibility"
변경될 값과 해당 키를 전달하여 updateValue(forKey:) 메서드를 호출해도 같은 동작을 한다.
bootDict.updateValue("The Ruins", forKey: "200-532874")
딕셔너리 항목 추가하기와 제거하기
다음의 키 첨자 구문을 이용하면 딕셔너리에 항목을 추가할 수 있다.
딕셔너리 변수[키] = 값
예를 들어, 다음과 같이 책 딕셔너리에 새로운 키-값 쌍을 추가할 수 있다.
bookDict["300-898871"] = "The Overlook"
어떤 키-값을 딕셔너리에서 제거할 때는 해당 항목에 nil 값을 할당하거나 딕셔너리 인스턴스의 removeValue(forKey:) 메서드를 호출하면 된다.
다음 두 줄은 모두 책 딕셔너리에서 특정 항목을 삭제하는 결과를 만든다.
bookDict["300-898871"] = nil
bookDict.removeValue(forKey: "300-898871")
딕셔너리 반복
배열과 마찬가지로 for-in 반복문을 이용하면 딕셔너리의 항목들을 반복할 수 있다.
예를 들어, 다음 코드는 책 딕셔너리에 있는 모든 항목을 가져다가 각각의 키와 값을 출력한다.
for (bookId, title) in bookDict {
print("Book ID: (bookId) Title: \(title)")
}
요약
Swift의 컬렉션은 딕셔너리(Dictionary), 세트(Set) 또는 배열(Array)의 형태를 취한다.
이들 모두 하나의 객체에 여러 항목을 담을 수 있는 방법을 제공한다.
배열은 항목들을 순서대로 담을 수 있는 방법을 제공하여 배열에 담긴 항목 위치에 해당하는 인덱스 값으로 항목에 접근할 수 있도록 한다.
딕셔너리는 키-값 쌍으로 저장하는 플랫폼을 제공하며, 키는 저장된 값에 접근하는 데 사용된다.
Swift 컬렉션의 항목들은 for-in 반복문을 이용하여 반복할 수 있다.
'멋쟁이 사자처럼 앱스쿨 1기 > 회고록' 카테고리의 다른 글
멋사 앱 스쿨 1기 14일차 회고록/TIL (22.10.04) - 에러 핸들링, Swift 심화 (1) | 2022.10.13 |
---|---|
멋사 앱 스쿨 1기 13일차 회고록/TIL (22.09.30) - DevOps, Git (0) | 2022.10.12 |
멋사 앱 스쿨 1기 11일차 회고록/TIL (22.09.28) - 서브클래싱, 익스텐션 (2) | 2022.10.05 |
멋사 앱 스쿨 1기 10일차 회고록/TIL (22.09.27) - 클래스, 프로토콜 (0) | 2022.10.04 |
멋사 앱 스쿨 1기 9일차 회고록/TIL (22.09.26) - 함수, 메서드, 클로저 (0) | 2022.09.27 |