티스토리 뷰

swift/문법

07. Swift 문법 - Optionals

DevBee 2020. 10. 13. 11:23

이번 글에서는 Swift Optionals 에 대해 알아보겠습니다.

 

1. Optionals

Optionals 은 값을 가지지 않아도 되는 형식을 말합니다.
 

다음과 같은 특징을 가집니다.
     1. Optional 에 저장되어 있는 값을 사용하려면 값을 Unwrapping 해야 한다.
     2. nil 이 저장되어 있는 상태에서 강제 추출하면 에러가 발생한다.
     3. Optional 표현식을 Unwrapping 하면 Non-Optional 타입으로 결과가 리턴된다.

 

지금까지 살펴본 코드에서 일반적으로 사용한 타입은 Non-Optional 타입으로 이 경우 변수와 상수는 반드시 초기화를 해서 사용해야 했습니다.

let num: Int
// print(num) - 에러

 

하지만 값을 미리 지정할 수 없는 경우도 존재합니다. (ex. 네트워크 데이터를 가져오는 경우 등)

이럴 때는 값을 가지지 않아도 되는 타입인 Optional 타입으로 변수와 상수를 선언하면 됩니다.

 

Optional 타입의 형식은 기존 자료형에  ? 를 붙인 형태입니다.

let optionalNum: Int? = nil
let optionalStr: String? = nil

// Non-Optional 타입은 항상 값을 가져야 함
let str: String = "Swift"
// let str: String = nil - 에러

 

위에서 Optional 타입의 초기값으로 사용된 nil 은 값이 없음을 의미합니다. Objective-C 의 nil 과는 다르게 포인터가 아니며 자체로는 추론할 형식이 없기 때문에 반드시 타입 annotation을 같이 사용하여야 합니다.

 

이런 Optional 타입을 그냥 사용할 수 없고 위에서 설명한 대로 값을 추출하여 (Unwrapping) 사용해야 합니다.

// Unwrapping

var number: Int? = nil
print(number)        // 결과: nil

number = 123
print(number)        // 결과: Optional(123)

// Forced Unwrapping (강제 추출)
// optionalExpression!
print(number!)       // 결과: 123

number = nil
// print(number!) - 에러: 값이 없는 경우에는 강제 추출이 불가능함
// 좀 더 안전하게 확인
if number != nil {
    print(number!)
}

number = 123
let before = number
let after = number! // 강제 추출해 값을 전달하기 때문에 Non-Optional 타입으로 전달

type(of: before)    // 결과: Optional<Int>.Type
type(of: after)     // 결과: Int.Type

 

2. Optional Binding

위 글에서 강제 추출(Forced Unwrapping)을 통해 Optional 타입의 값에 접근했는데 이 경우 값이 nil 이면 에러가 발생합니다. 따라서 값을 더 안전하게 추출하는 방식인 Optional Binding 에 대해 알아보겠습니다.

 

Optional Binding 의 형태는 다음과 같습니다.

if let name: Type = OptionalExpression {
    statements
 }

while let name: Type = OptionalExpression {
    statements
}

guard let name: Type = OptionalExpression else {
    statements
}

 

OptionalExpression 을 평가해서 값이 리턴이 되면 Unwrapping 되어서 상수 (또는 변수)에 저장이 되고 이 상태가 바인딩에 성공한 상태입니다. 이때 if 문이라면 if 문이 실행이 되고, while 문이라면 반복이 되고, guard 문의 경우 그 다음 문장이 실행됩니다. OptionalExpression 을 평가했을 때 nil 이 리턴이 되면 실패한 상태가 되며, if 문과 while 문의 경우 그 다음 문장이 실행되고 guard 문은 else 문이 실행됩니다.

var num: Int? = nil

if let num = num {
    print(num)
    type(of: num)
} else {
    print("Empty")
}

// 결과: Empty

var str: String? = "str"
guard let str = str else {
    fatalError()
}

str
type(of: str)

// 결과: str
// 결과: String.Type

num = 123
if var num = num {
    num = 456
    print(num)
}

// 결과: 456

let a: Int? = 12
let b: String? = "str"

// 바인딩이 모두 성공하고 조건 또한 true 인 경우에 if 문이 실행됨.
if let num = a, let str = b, str.count > 5 {
    print("All success")
}

 

3. Implicitly Unwrapped Optionals

특정 조건에서 강제 추출되는 Optional 타입을 의미하며 iOS 앱 개발 시 이를 사용하는 경우는 2가지가 있습니다.

 

    1. Outlet 을 연결할 때

    2. API 에서 IUO 를 리턴하는 경우

 

하지만, 2가지 경우에도 그냥 Optional과 Optional Binding 을 사용하면 되기 때문에 자주 이 문법을 사용하지는 않습니다.

let num: Int! = 12

// 형식 추론을 사용하는 경우에는 자동으로 추출되지 않음
let a = num
type(of: a)  // 결과: Optional<Int>.Type

// IUO 는 Non-Optional 타입으로 처리되어야 할 때 자동으로 Unwrapping 됨
let b: Int = num
type(of: b)  // 결과: Int.Type

let number: Int! = nil
// IUO 는 값을 강제로 자동 추출하기 때문에 nil 이 저장된 상태에서는 에러가 발생
// let c: Int = number - 에러

 

4. Nil-Coalescing Operator

값이 저장되어 있는지 확인하는 코드와 값을 추출하는 코드를 직접 작성하지 않아도 값을 안전하게 추출하게 도와주는 연산자입니다.

형태는 다음과 같습니다.

OptionalExpression ?? Expression

OptionalExpression 을 평가해서 값이 있으면 값을 Unwrapping 해 리턴하고 그 값을 연산에 사용하게 됩니다. 값이 없는 경우에는 오른쪽 피연산자를 평가해 그 값을 그냥 사용하는 방식입니다. 주로 오른쪽 피연산자에는 값이 없을 때 사용할 기본값을 지정하게 되며 오른쪽 피연산자에서는 side effect 를 발생 시키는 코드를 쓰면 안됩니다.

var msg = ""
var input: String? = "Swift"

if let inputName = input {
    msg = "Hello, " + inputName
} else {
    msg = "Hello, Stranger"
}
print(msg)

var str = "Hello, " + (input != nil ? input! : "Stranger")
print(str)

str = "Hello, " + (input ?? "Stranger")
print(str)

// 결과는 3개 모두 Hello, Swift

 

5. Optional Chaining

Optional 을 연달아서 호출하는 것으로 특징을 살펴보면 다음과 같습니다.

    1. Optional Chaining 의 결과는 항상 Optional 입니다.

    2. Optional Chaining 에 포함된 표현식 중에서 하나라도 nil 을 리턴한다면 이어지는 표현식을 평가하지 않고 nil 을 리턴합니다.

struct Contects {
    var email: [String: String]
    var address: String
}

struct Person {
    var name: String
    var contacts: Contects
    
    init(name: String, email: String) {
        self.name = name
        contacts = Contects(email: ["home": email], address: "Seoul")
    }
}

var p = Person(name: "James", email: "swift@example.com")
let a = p.contacts.address
type(of: a)  // 결과: String.Type

var optionalP: Person? = Person(name: "James", email: "swift@example.com")
// optionalP 에 인스턴스가 저장되어 있다면, 이어지는 contacts 속성에 접근하고
// 반대로 nil 이 저장되어 있다면, nil 을 리턴하고 바로 종료
let b = optionalP?.contacts.address
type(of: b)  // 결과: Optional<String>.Type (값은 Seoul)

optionalP = nil
let c = optionalP?.contacts.address
type(of: c)  // 결과: Optional<String>.Type (값은 nil)

Optional Chaining 의 리턴 형식은 마지막 표현식에 따라서 결정됩니다.

struct Contects {
    var email: [String: String]?
    var address: String?
    
    func printAddress() {
        return print(address ?? "no address")
    }
}

struct Person {
    var name: String
    var contacts: Contects?
    
    init(name: String, email: String) {
        self.name = name
        contacts = Contects(email: ["home": email], address: "Seoul")
    }
    
    func getContacts() -> Contects? {
        return contacts
    }
}


var p = Person(name: "James", email: "swift@example.com")
let a = p.contacts?.address
type(of: a)  // 결과: Optional<String>.Type

var optionalP: Person? = Person(name: "James", email: "swift@example.com")
let b = optionalP?.contacts?.address
type(of: b)  // 결과: Optional<String>.Type

optionalP = nil
let c = optionalP?.contacts?.address
type(of: c)  // 결과: Optional<String>.Type

/*
 -----------------------------------------------------------------------------
 */

// Optional 형식으로 선언되어 있는 값을 통해서 속성이나 메소드를 호출할 때는 ?를 반드시 붙여야 함
// p.contacts?.address.count - 에러
p.contacts?.address?.count  // 결과: 5

// 메소드가 Optional 타입을 리턴하고 이 타입을 통해 다른 멤버에 접근할 때는 항상 () 뒤에 ? 추가
p.getContacts()?.address  // 결과: Seoul

/*
 -----------------------------------------------------------------------------
 */

// 함수 자체가 Optional 인 경우 함수 이름 뒤에 (즉, () 앞에) ? 를 붙임
let f: (() -> Contects?)? = p.getContacts
f?()?.address  // 결과: Seoul

// () = void 값을 리턴하지 않음
let d = p.getContacts()?.printAddress()
type(of: d)  // 결과: Optional<()>.Type

// 이를 통해 함수가 실행되었는지 아닌지를 파악할 수 있음
if p.getContacts()?.printAddress() != nil {
    
}

if let _ = p.getContacts()?.printAddress() {
    // printAddress() 함수가 호출된 경우에 바인딩이 성공해서 실행
}

/*
 -----------------------------------------------------------------------------
 */
 
let e = p.contacts?.email?["home"]  // 결과: swift@example.com
p.contacts?.email?["home"]?.count   // 결과: 17

// 최종 속성까지 접근했다면 값을 저장하고 중간에 끝나면 변화가 없음
p.contacts?.address = "Daegu"       // 결과: ()
p.contacts?.address                 // 결과: Daegu

optionalP?.contacts?.address = "Daegu"  // 결과: nil
optionalP?.contacts?.address            // 결과: nil

 

6. Optional Pattern

let a: Int? = 0
let b: Optional<Int> = 0

// 축약형
if a == nil {
    
}

// 위와 같은 enum pattern
if a == .none {
    
}

// 축약형
if a == 0 {
    
}

// 위와 같은 enum pattern
if a == .some(0) {
    
}

/*
 ----------------------------------------
 */

// 축약형
if let x = a {
    print(x)  // 결과: 0
}

// 위와 같은 enum pattern
if case .some(let x) = a {
    print(x)  // 결과: 0
}

// 위와 같은 optional pattern
if case let x? = a {
    print(x)  // 결과: 0
}

/*
 ----------------------------------------
 */

let list: [Int?] = [0, nil, nil, 3, nil, 5]

for item in list {
    guard let x = item else { continue }
    print(x)  // 결과: 0, 3, 5
}

for case let x? in list {
    print(x)  // 결과: 0, 3, 5
}

 

같은 기능의 코드를 더 간략하게 쓸 수 있다는 장점이 있습니다.


지금까지 Swift 의 Optionals 에 대해 알아보았습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함