본문 바로가기

전공/swift

swift (7) - Optional, Dictionary, Protocol

1. 옵셔널 (optional)
(1) 옵셔널 바인딩 (Optional Binding)
옵셔널 바인딩은 옵셔널이 실제 값을 포함하고 있는지 확인하고, 값이 존재하는 경우 해당 값을 임시 상수 또는 변수로 사용할 수 있게 한다. 
if 와 while 구문에서 해당 과정을 수행할 수 있다. 기본적인 형태는 아래와 같다. 

if let constantName = someOptional {
	statement
}

 
이때 비교 연산자가 아닌 일반 등호가 사용됨에 주의하자.
강제적 언래핑(forced unwrapping) 대신, 옵셔널 바인딩을 사용하여 코드를 작성할 수 있다. possibleNumber가 Int 타입의 옵셔널이었던 앞선 예시의 코드를 다시 작성해보자.
 

let possibleNumber = "123"

if let actualNumber = Int(possibleNumber){
    print("\(possibleNumber)는 정수 값 \(actualNumber) 을 가지고 있습니다")
}
else {
    print("\(possibleNumber) 는 정수로 전환될 수 없습니다.")
}
// "123는 정수 값 123 을 가지고 있습니다" 출력

 
Int(possibleNumber) 에 의해 반환된 옵셔널 Int 에 nil이 아닌 값이 포함되어 있을 경우, actualNumber 라는 상수에 옵셔널에 들어있던 값을 할당한다. 이때 actualNumber는 옵셔널 타입이 아니고, 옵셔널에 들어있던 값으로 초기화 된 상수이다. 
Int(possibleNumber)의 타입이 Int? 이므로 actualNumber의 타입은 Int인 것이다.
아래는 위와 같은 예시에서 반환된 옵셔널 값이 nil일 경우의 코드이다.

let possibleNumber = "엥?"

if let actualNumber = Int(possibleNumber){
    print("\(possibleNumber)는 정수 값 \(actualNumber) 을 가지고 있습니다")
}
else {
    print("\(possibleNumber) 는 정수로 전환될 수 없습니다.")
}
// "엥? 는 정수로 전환될 수 없습니다." 출력

 
옵셔널 바인딩은 상수와 변수 둘 다 사용이 가능하다. 아래의 예시를 살펴보자.

let possibleNumber = "123"

if var actualNumber = Int(possibleNumber){
    print("\(possibleNumber)는 정수 값 \(actualNumber) 을 가지고 있습니다")
    actualNumber = 32
    print(possibleNumber)
    print(actualNumber)
}
else {
    print("\(possibleNumber) 는 정수로 전환될 수 없습니다.")
}
// possibleNumber는 여전히 "123", actualNumber의 값만 32로 수정

 
if 구문에 첫번째 중괄호에서 actualNumber의 값을 변경하고 싶다면 if var actualNumber로 써주면 된다. actualNumber 의 값을 수정한다고 해서 possibleNumber의 값까지 수정이 되는 것은 아니다. possibleNumber는 기존의 값을 유지한다.
 
 
 
(2) 암시적으로 언래핑된 옵셔널(Implicitly Unwrapped Optionals)
옵셔널은 nil(값이 없음) 또는 실제 값을 포함한다. 이때 이를 옵셔널 바인딩을 통해 실제로 값이 존재하는지 확인하고, 값이 존재할 경우 적절하게 출력할 수 있다. 그런데, 프로그램 구조 상 옵셔널이 항상 nil이 아닌 실제값을 가질 것이라 확실시되는 경우가 있다. 이러한 경우에는 접근할 때마다 옵셔널 바인딩을 통해 옵셔널에 값이 존재하는지 확인하는 것은 불필요한 과정이다. 
 
이러한 상황에서 암시적으로 언래핑된 옵셔널이 사용된다. 타입 뒤에 물음표 (Int?) 를 붙이는 것 대신, 느낌표 (Int!) 를 붙여 작성한다.
옵셔널 이름의 뒤에 느낌표를 위치시키는 것보다는 옵셔널 타입 뒤에 느낌표를 위치시키는 것이 더욱 좋다. 
아래의 예시를 살펴보자.

let possibleInt: Int? = 131
let forcedInt: Int = possibleInt!
print(type(of: possibleInt)) // 옵셔널
print(type(of: forcedInt)) // 일반 Int

let assumedInt: Int! = 1010
let implicitInt: Int = assumedInt
print(type(of: assumedInt)) // 옵셔널
print(type(of: implicitInt)) // 일반 Int

let optionalInt = assumedInt
print(type(of: optionalInt)) // 옵셔널

 
암시적으로 언래핑 된 옵셔널은 내부적으로는 옵셔널이지만, 옵셔널에 접근할 때마다 옵셔널 값을 풀 필요 없이 옵셔널이 아닌 값처럼 활용할 수 있다. 암시적으로 언래핑 된 옵셔널은 옵셔널을 강제적으로 언래핑하는 것을 허락한 것이라 생각하면 된다.
암시적으로 언래핑 된 옵셔널을 사용할 때 스위프트는, 일반적인 상황에선 기존 옵셔널 타입으로 사용하려고 한다. 
그러나 옵셔널 타입으로 사용이 불가능한 상황에서는, 자동으로 강제적 언래핑을 실행한다. 
위 코드에서 assumedInt 는 암시적으로 언래핑 된 옵셔널이다.
이때 implicitInt 에는 옵셔널 타입을 집어넣을 수 없으므로 자동적으로 강제적 언래핑이 실행되고,
optionalInt 에는 옵셔널 타입을 넣을 수 있으므로 기존 옵셔널 타입으로 사용된다. 
아래의 예시는 암시적으로 언래핑 된 옵셔널에 nil 체크를 수행하는 코드이다. 

let assumedInt: Int! = 1010

if assumedInt != nil {
    print(assumedInt)
} // "Optional(1010)" 출력 -> 기존 옵셔널을 사용할 수 있는 상황이기 때문

if assumedInt != nil {
    print(assumedInt!)
} // "1010" 출력 -> 강제적 언래핑 실행

if let definiteInt = assumedInt {
    print(definiteInt)
} // 옵셔널 바인딩, "1010" 출력

 
 
 
(3) 옵셔널 체이닝(Optional Chaining)
옵셔널 체이닝은 옵셔널 값이 nil인지 확인하면서 프로퍼티, 메서드, 서브스크립트에 접근하는 방법이다.
만약 옵셔널이 실제 값을 가지고 있으면 해당 작동은 성공하며, 옵셔널이 nil일 경우엔 호출한 전체 값으로 nil을 반환한다.
여러 개의 체인이 함께 연결될 수 있으며, 그 중 하나라도 nil이면 전체 작동은 실패한다. (nil 반환)
 
프로퍼티, 메서드 또는 서브 스크립트를 호출할 때 옵셔널 값 뒤에 물음표를 넣어서 옵셔널 체이닝을 할 수 있다. 
강제적 언래핑 시에 옵셔널 값 뒤에 느낌표를 붙였던 느낌하고 유사하다. 
이 둘의 주요 차이점은 옵셔널이 nil일 때 전자는 nil을 반환하고 작동에 실패하는 반면에 (에러 발생까진 x)
강제적 언래핑은 아예 런타임 에러가 발생한다는 점이다. 
 
옵셔널 체이닝에 성공했을 때, 프로퍼티, 메서드, 서브 스크립트가 옵셔널 값을 반환하지 않더라도 반환값은 항상 옵셔널이다. 
즉, 예상되는 반환값과 동일하긴 한데 옵셔널 상태로 반환이 된다는 것이다. 
예를 들어 Int 를 반환하는 프로퍼티인 경우 옵셔널 체이닝 과정을 거치면 Int?로 반환된다.
이를 통해 옵셔널이 반환되었다면 옵셔널 체이닝이 성공한 것이고 nil이 반환되었다면 옵셔널 체이닝에 실패한 것임을 알 수 있다.
 
다음 코드는 강제적 언래핑과 옵셔널 체이닝의 차이를 보여주는 코드이다.

class Person {
    var residence: Residence?
}

class Residence{
    var numberOfRooms = 1
}

let john = Person()
let roomCount=john.residence!.numberOfRooms
// 강제적 언래핑 -> 런타임 에러 !!

 
Residence 인스턴스는 기본 값이 1인 numberOfRooms 라는 Int 프로퍼티를 가지고 있고, Person 인스턴스는 옵셔널 Residence 타입의 residence 프로퍼티를 가지고 있다. 새로운 Person 인스턴스를 생성하면 residence 프로퍼티는 nil이 된다. (특별히 넣어준 내용이 없으므로) john은 nil의 residence 프로퍼티 값을 가지고 있는 것이다. 
 
이때 강제적 언래핑을 residence 프로퍼티에 실행하면 residence 값이 nil이기 때문에 런타임 에러가 뜬다. 
만약 residence가 nil 값이 아니라면 위의 코드는 정상적으로 동작했을 것이다. 
 
옵셔널 체이닝의 경우를 살펴보자.

let john = Person()
if let roomcount = john.residence?.numberOfRooms {
    print("존 : \(roomcount)")
}
else{
    print("nil 값")
}
// 옵셔널 체이닝 -> else 블럭 작동, "nil 값" 출력

 
옵셔널 체이닝을 사용하기 위해 느낌표 자리에 물음표를 사용해준다. 
이는 residence 값이 nil이 아니면 numberOfRooms 값에 조회하도록 작동된다. 
numberOfRooms 에 접근하려는 시도가 실패할 수 있으므로 옵셔널 체이닝은 옵셔널 Int 타입의 값을 반환한다. 
위 코드에서는 residence가 nil이었기에, 반환되는 옵셔널 Int 의 값은 nil이다.
이 옵셔널 Int의 값은 옵셔널 바인딩으로 접근되고, 따라서 else 블럭이 실행된다. 
 
numberOfRooms 가 옵셔널 Int 가 아니어도 옵셔널 체이닝으로 조회되었기에 옵셔널 값이 반환된다는 것에 유의하자.
다음은 residence에 Residence 인스턴스를 할당하여 nil 값을 반환하지 않도록 한 코드이다.

let john = Person()
john.residence = Residence()
if let roomcount = john.residence?.numberOfRooms {
    print("존 : \(roomcount)")
}
else{
    print("nil 값")
}
// 옵셔널 체이닝 -> "존 : 1" 출력

 
john.residence 는 nil이 아닌 실제 Residence 인스턴스를 가지고 있다. 
따라서 numberOfRooms에 옵셔널 체이닝으로 접근하면 값이 1인 Int? 를 반환하게 된다. 
 
 
 
 
 
 
2. 딕셔너리 (dictionary)
swift는 여러 개의 값을 한번에 저장하기 위한 배열(array), 집합(set), 딕셔너리(dictionary)와 같은 3개의 콜렉션 타입(collection types) 을 제공한다. 

  • 배열(array) - 순서대로 같은 타입의 값을 저장한다. (인덱스 존재)
  • 집합(set) - 순서와 상관 없이 같은 타입의 다른 값을 저장한다. (인덱스 없음, 순서 없음. 그러나 한 개의 집합 내에서 같은 값이 존재할 수 없음!!)
  • 딕셔너리(dictionary) - 순서와 상관없이 같은 타입의 key와 같은 타입의 value를 쌍으로 저장한다. 

이 중에서 딕셔너리에 대해 알아보자. 
 
(1) 딕셔너리 타입
딕셔너리는 key-value 쌍으로 이루어져 있다. 이때 key는 key끼리, value는 value끼리 같은 타입이다. 
각 value는 딕셔너리 내부에서 값에 대한 일종의 식별자로 작동하는 key와 관계를 맺는다.
배열과는 다르게 딕셔너리는 순서가 없다. 
사전에서 key값을 통해 내용을 찾듯이 딕셔너리 또한 비슷하게 구성되어 있다고 생각할 수 있다. 
 
(2) 딕셔너리 타입을 생성하기
딕셔너리 타입은 Dictionary<Key, Value> 로 적는다. 또는 [Key: Value] 와 같이 적을 수도 있다.
이때 key와 value 자리에는 key와 value에 사용될 값의 타입을 적어준다. 
아래 예시에서 확인해보자. 

var integers: [Int: String]
var animal: Dictionary<String, Int> // 딕셔너리에 저장된 값은 따로 없다...

 
빈 딕셔너리에서 시작해보자.
이때 [:] 는 빈 딕셔너리 리터럴이다. 이를 이용하여 빈 딕셔너리를 생성할 수 있다. 

var integers_1 = [Int:String]() // 빈 딕셔너리를 만드는 법 1
var integers: [Int: String] = [:] // 빈 딕셔너리를 만드는 법 2
// [Int: String] 딕셔너리 두 개가 비어있다.

integers[16]="sixteen"
// integers 딕셔너리는 key값으로 16, value 값으로 sixteen을 갖는다.

integers = [:]
// integers 딕셔너리가 다시 비게 된다.

 
타입이 다양하게 바뀌어도 괜찮다. 만약 [String: String] 타입이었으면 
integers["hey"] = "sixteen" 또한 문제 없이 작동한다. 
 
내용을 가진 딕셔너리 리터럴로 딕셔너리를 초기화할 수 있다. 다음과 같은 형식으로 작성하면 된다. 

var airports: [String: String] = ["alswn": "seoul", "tjdgns": "busan"] 
// 딕셔너리 초기화하는 방법 1, 딕셔너리의 모든 타입을 명시함
var airports = ["alswn": "seoul", "tjdgns": "busan"] 
// 딕셔너리 초기화하는 방법 2, 타입 추론을 통해 딕셔너리의 타입이 [String: String] 임을 추론

 
 
(3) 딕셔너리 타입에 접근하고 내용을 수정하기
딕셔너리 타입에 몇 개의 쌍이 들어있는지, 딕셔너리가 아예 비어있는지 확인할 수 있다. (프로퍼티)

print("airports 딕셔너리 안에는 \(airport.count) 개의 key-value 쌍이 들어있습니다.")
// "airports 딕셔너리 안에는 2 개의 key-value 쌍이 들어있습니다." 출력

if airport.isEmpty {
	print("딕셔너리가 비어있어요.")
} else {
	print("딕셔너리에 값이 존재합니다.")
}
// "딕셔너리에 값이 존재합니다." 출력
// .isEmpty 는 빈 딕셔너리는 true, 그렇지 않으면 false를 반환한다.

 
딕셔너리 타입에 다음과 같이 새로운 쌍을 추가하고, 기존에 존재하는 쌍의 내용을 수정할 수 있다. (서브 스크립트 구문)

airports["wjddnjs"] = "guri" 
// airport 딕셔너리에는 3개의 쌍이 존재하게 된다.

airports["wjddnjs"] = "suwon" 
// "wjddnjs" 이라는 키의 값이 "guri"에서 "suwon"으로 변경되었다.

 
 
(4) 딕셔너리를 출력하기 -> 옵셔널! 
위와 같은 서브 스크립트 외에 딕셔너리의 updateValue 메서드를 통해 특정 키에 값을 설정하거나 업데이트할 수 있다. 
해당 메서드는 기존 키에 값을 업데이트 한 후 기존에 존재했던 딕셔너리의 값을 반환한다. 
만약 해당 키가 존재하지 않는 경우를 대비하여 updateValue 메서드는 딕셔너리 값 타입의 옵셔널을 반환한다. 
해당 키가 존재하지 않을 경우엔 nil 값을 반환한다. 
다음의 예시를 살펴보자. 

var airport = ["alswn":"seoul","tjdgns":"busan","wjddnjs":"suwon"]

if let oldValue = airport.updateValue("yangyang", forKey: "wjddnjs"){
    print("wjddnjs이의 업데이트 전 값은 \(oldValue)")
}
else{
    print("작동 실패")
}
// "wjddnjs이의 업데이트 전 값은 suwon" 출력
var airport = ["alswn":"seoul","tjdgns":"busan","wjddnjs":"suwon"]

if let oldValue = airport.updateValue("yangyang", forKey: "엥?"){
    print("wjddnjs이의 업데이트 전 값은 \(oldValue)")
}
else{
    print("작동 실패")
}
// "작동 실패" 출력

 
특정 키를 통해 딕셔너리의 값을 읽을 때 서브 스크립트 구문을 사용할 수도 있다. 존재하지 않는 키로도 요청이 가능하기 때문에 이 또한 해당 상황을 대비하여 딕셔너리 값 타입의 옵셔널을 반환한다. 딕셔너리에 요청된 키의 값이 존재하는 경우엔 그 값의 옵셔널 값을 반환하고, 키가 존재하지 않는 경우에는 nil을 반환한다. 
다음 예시를 살펴보자.
 

var airport = ["alswn":"seoul","tjdgns":"busan","wjddnjs":"suwon"]

if let airportName = airport["wjddnjs"]{
    print("\(airportName)")
} else {
    print("작동 실패")
}
// "suwon" 출력
var airport = ["alswn":"seoul","tjdgns":"busan","wjddnjs":"suwon"]

if let airportName = airport["엥?"]{
    print("\(airportName)")
} else {
    print("작동 실패")
}
// "작동 실패" 출력

 
 
(5) 딕셔너리 일부를 삭제하기
딕셔너리의 해당 키 값에 nil을 할당하여 키-값 쌍을 삭제할 수 있다. 

var airport = ["alswn":"seoul","tjdgns":"busan","wjddnjs":"suwon"]

airport["wjddnjs"] = nil
// "wjddnjs" 은 딕셔너리에서 삭제

 
또는 딕셔너리에 removeValue 메서드를 사용하여 키-값 쌍을 삭제할 수 있다. 
이 메서드는 해당 키가 존재하면 삭제하고, 삭제된 딕셔너리 값을 옵셔널 타입으로 반환한다.
해당 키가 존재하지 않으면 nil을 반환한다.

var airport = ["alswn":"seoul","tjdgns":"busan","wjddnjs":"suwon"]

if let removedValue = airport.removeValue(forKey: "wjddnjs"){
    print("삭제된 딕셔너리 값은 \(removedValue)")
}else{
    print("작동 실패")
}
// "삭제된 딕셔너리 값은 suwon" 출력
var airport = ["alswn":"seoul","tjdgns":"busan","wjddnjs":"suwon"]

if let removedValue = airport.removeValue(forKey: "엥?"){
    print("삭제된 딕셔너리 값은 \(removedValue)")
}else{
    print("작동 실패")
}
// "작동 실패" 출력

 
 
(6) 딕셔너리 반복하기
for-in 루프로 딕셔너리의 키-값 쌍을 반복할 수 있다. 
딕셔너리의 각 아이템은 (key, value) 형태의 튜플로 반환된다. 

var airport = ["alswn":"seoul","tjdgns":"busan","wjddnjs":"suwon"]

for (airportCode, airportName) in airport{
    print("\(airportCode), \(airportName)")
}

/* alswn, seoul
 tjdgns, busan
 wjddnjs, suwon
 출력 */

 
딕셔너리의 key와 value 프로퍼티로 따로 접근하여 반복문을 출력할 수도 있다. 

var airport = ["alswn":"seoul","tjdgns":"busan","wjddnjs":"suwon"]

for airportCode in airport.keys{
    print("\(airportCode)")
}
for airportName in airport.values{
    print("\(airportName)")
}

/* tjdgns
 alswn
 wjddnjs
 busan
 seoul
 suwon
 출력 */

 
딕셔너리의 키 또는 값 들만을 따로 모아서 배열을 생성할 수 있다. 

var airport = ["alswn":"seoul","tjdgns":"busan","wjddnjs":"suwon"]

let airportCodes = [String](airport.keys)
// airportCodes 는 ["alswn","tjdgns","wjddnjs"]
let airportNames = [String](airport.values)
// airportNames 는 ["seoul","busan","suwon"]

for a in 0..<3 {
    print(airportCodes[a])
}

/* alswn
 wjddnjs
 tjdgns 출력 */

 
 
 
 
 
 
3. 프로토콜
프로토콜은 메서드, 프로퍼티, 기타 특정 작업 등 구현해야 하는 요구사항을 정의한다. 
프로토콜의 요구 사항을 구현하기 위해 클래스, 구조체, 열거형이 이 프로토콜을 채택(adopt)한다.
프로토콜의 요구사항에 충족하는 모든 타입은 프로토콜에 준수(conform) 한다고 한다. 
 
즉, 프로토콜은 특정 기능을 보장하기 위해 사용한다. 어떤 객체가 특정 메서드를 반드시 구현하도록 강제하고 싶을 때 프로토콜을 사용할 수 있다. 다음과 같은 방법으로 프로토콜을 구현한다. 

protocol SomeProtocol {
    // 프로토콜 정의를 작성
}

 
타입 이름 뒤에 콜론으로 구분하고, 특정 프로토콜의 이름을 위치시켜 프로토콜을 채택하게 할 수 있다. 
스위프트는 단일 상속을 지원한다. 즉, 하나의 클래스가 오직 하나의 상위 클래스만 가질 수 있다. 
그에 반해, 프로토콜은 여러 개를 채택할 수 있다. 여러 프로토콜은 콤마로 구분하여 나열한다. 

protocol SomeProtocol {
    // 프로토콜 정의를 작성
}

protocol NextProtocol {
}

class Shape : SomeProtocol, NextProtocol{
}

 
클래스가 상위 클래스를 가진 경우에는 상위 클래스를 가장 앞에 써주어야 한다. 

protocol SomeProtocol {
    // 프로토콜 정의를 작성
}

protocol NextProtocol {
}

class Math {
}

class Shape : Math, SomeProtocol, NextProtocol{
}

 
위임(Delegation)은 클래스 또는 구조체가 다른 객체에 일부 작업을 위임하는 디자인 패턴이다.
이를 구현하기 위해서는 보통 프로토콜이 사용된다.
위임할 기능을 캡슐화하는 프로토콜을 정의하여 구현되며, 프로토콜을 채택하는 타입(위임 객체)은 위임된 기능을 제공할 것이 보장된다.