[Swift] Optional Type 그리고 ?

Optional의 존재는 nil 로부터 온다.

 nil 의미

Swift 에서의 nil 은 Objective-C 에서의 nil 과 다르다.

  • nil of Swift : 값이 없다  in 모든타입 (Reference or Value Type)
  • nil of Objective-C : 값이 없다 in Reference Type

즉, Swift에선 어떤 타입이든 값이 없으면  nil 이다.

Optional Type 

Optional Type 은 nil이 될 수도  있는 변수 (or 상수)를 의미하며 선언할 때 ‘?’ 를 사용한다.  ‘?’  이 없으면 Non-Optional Type 으로 nil이 될 수 없다.

// optional type.
var optionalString : String?   // nil 로 초기화

// non-optional type.
var nonOptionalString : String // nil 이 될 수 없음. 초기화 필요

WWDC 2014 에서 Safe, Power, Modern 세가지를 Swift의 특징으로 꼽았다. Optional은  Safe에 속한다.

기존 nil 로 인한 앱 크래쉬 문제를  nil 전용 변수 타입을 따로 정의해서 compile 시점에서 막아줄께 하는 것이다. (실제로 이것이 얼마나 안전한지는 더 써봐야 알겠다.)

개발자는 변수 선언할 때마다 Optional Type으로 선언할 것인지 판단해야 한다. 꼭 필요한 경우에만 Optional Type 사용을 권장한다.

Optional은 enum 으로 정의된다.

enum Optional<T>{
    case None
    case Some(T)
}

! (forced-unwrapping operator)

Optional을 이용하여 안전한 코드를 짜려면 반드시 if 문으로 값이 있는지 확인을 해야 한다. 값이 있는 경우엔 ‘!’ 를 이용하여 값이 있음을 명시적으로 알려줘야 한다.

‘!’ 는 forced-unwrapping operator 로서 애매모호한 Optional Type 값을 명확하게 들어낸다. 이를 통해 불필요한 Optional Type 추가 발생을 막는다.

let numCards = ["one" : 1 , "two" : 2, "three" : 3,
               "four" : 4 , "five": 5]

// dictionary[key] 는 optional type을 반환 (즉,myCard 는 Int?)
let myCard = numCards["two"]
if myCard { // 값을 체크

    let card = myCard! // 값이 있으므로 !로 값을 드러냄
    // card의 타입은 Int (Int? 가 아님)
   
    println("card \(card)")

}else {

    println("no card")
}

Runtime Error

Optional Type을 if 문 없이 unwrapping 하여 사용하면 Runtime Error가 발생한다. 더 이상 안전하지 않다.

let numCards = ["one" : 1 , "two" : 2, "three" : 3, "four" : 4 , "five": 5]
let myCard = numCards["nine"]    // myCard = nil 

println("\(myCard!)")      // Runtime Error 발생

 Optional Binding

결국 Optional Type을 다룰때는 항상

  1. 값 여부를 확인 하고
  2. 값이 있으면 unwrapping 하여

사용해야 한다.

그러나 매번 위처럼 단계적으로 처리하는 건 불편하다. Optional Binding은 이를 동시에 할 수 있다.

Optional Binding 포멧은 if let ~ 이다.

let numCards = ["one" : 1 , "two" : 2, "three" : 3,
               "four" : 4 , "five": 5]

if let card = numCards["two"] { // 값 체크 & unwapping 

    println("card \(card)")  // card 는 Int 타입.

}else{
    println("no card")
}

Optional Chaining

코드를 짜다보면 Optional Binding 이 중첩될 수 있다. 반복되는  if let~ 은  귀찮다.

여러 층의 Optional Binding을 한 줄에 쓰게 하는 것이 Optional Chaining이다. (Swift 개발팀에서도 Optional이 불편하긴 했나보다.)

Optional Chaining은 ‘?’ 기호를 사용한다. (Optional Type 선언에 사용하는 ‘?’ 과 동일시 하면 헷갈린다. 비슷하지만 다른것으로 보자.)

아래 예제 코드는 WWDC2014 에서 소개된 코드다.

// 중첩 Optional Binding
if let home = paul.residence {
    if let postalAddress = home.address{
        if let building = postalAddress.buildingNumber{
            if let convertedNumber = building.toInt(){
                println("building number of paul is \(convertedNumber)")
            }
         }
     }
}

// Optional Chaining
if let addressNumber = paul.residence?.address?.buildingNumber?.toInt(){

    println("building number of paul is \(addressNumber)")</pre>
}

코드를 간단히 설명하면 paul 은 class 개체로 Non-Optional Type 이고 나머지 residence, address, buildingNumber 는 Optional Type 이다.

paul 의 집 주소를 알기 위해 Optioanl Binding으로 순차적으로 값을 확인한다. 이를 Optional Chaining 을 이용하면 한 줄에 끝이다.

Optional Chaining을 철도 선로로 이해하면 쉽다. 값이 없으면 nil 행이고 값이 있으면 keep going. (WWDC2014 비디오 스크린샷을 올리기가 좀 그렇지만, 좋은 설명이라 일단 첨부한다.)

< Optional Chaining from WWDC2014 >

 

Optional 과 Enum Pattern Matching

앞서 살펴봤듯이 Optional 은 enum 타입이다.

enum Optional<T>{
    case None
    case Some(T)
}

따라서 enum을 이용한 Pattern Matching 이 가능하다.

let numberCards : Dictionary<String,Int> = [ "one" : 1, "two" : 2, "three": 3, "four" : 4,
                  "five" : 5, "six": 6, "seven" : 7]

for num in numberCards.keys{

    // Dictionary[key] 리턴은 optional type
    switch numberCards[num]{

        case .Some(let v) where v > 3 :
            println("\(v)")
        default:
          println("...")

    }

}

numberCards[num] 의 리턴값은 Optional Type 이므로 – 여기선 Int? – 위와 같이 switch, enum 을 이용하여 처리할 수 있다.

 

* Reference

[Objective-C] nil 제대로 알자

Objective-C의 nil 과 NULL 이라는 상수는 기술적으로 혼용가능하다. 그러나 일반적으로 nil 은 객체 참조에 사용되고, NULL 은 그 밖의 다른 포인터 자료형에 사용된다. (예를 들면  MyClass * myObject = nil,  int *iPtr = NULL )

자바에서는 객체 참조값이 null이면 보통 NullPointerException 이 발생하고 프로그램이 종료된다. 따라서 자바  프로그램밍에서는 참조값이 null 인지 확인하는 구문이 자주 쓰인다.

그러나 Objective-C 의 메시지 전송은 자바의 그것과 다르다. 이전 포스팅(Objective-C 메시지 전송)에서 정리한 내용처럼, Objective-C에서는 메시지 전송 과정에서 중앙 메시지 전송 함수가 수신인의 포인터 값을 확인하는데, 이 때 수신자 포인터가 nil 값이면 메시지 전송 함수는 즉시 반환된다.

즉, 메시지 수신 객체가 nil 값이라도 Exception이 발생하지 않고, 아무런 작업 없이 바로 반환된다. 리턴값이 있는 경우에는 0 이나 nil 또는 NO 값을 반환한다.

따라서 자바와 달리 Objective-C에서는 메시지를 보내기 전에 메시지 수신자 값의 nil 여부를 확인하는 것은 불필요하다. nil 확인작업이 이미 내부적으로 진행되기 때문이며 그렇기 때문에 코드는 간결해 질 수 있다.

다음과 같이 무엇인가를 그리는 구문을 생각해보자.

-(void)drawRect:(NSRect)dirtyRect {

    ...
    MyMap *map = [self myMap];
    [map drawRect:dirtyRect];
    [[map customLocationColor] setStroke];
    [[map locationHighlightPath] stroke];

}

만약 map 객체가 nil 이면 그 아래 메시지는 모두 전달되지 않는다. 반면 map 객체는 nil 이 아니지만 -customLocationColor 메소드의 결과가 nil 이면 -setStroke 메시지가 호출되지 않는다. 마찬가지로 map 객체값이 nil 아니고-locationHighlightPath 메소드 반환값이 nil 이면 -stroke 메시지는 전달되지 않는다.

nil 객체에 메시지를 전달해도 아무런 Exception이 발생하지는 않지만 메시지에 전달되는 인자는 정상적으로 진행된다. 즉, 인자에서 메시지를 전달하거나 증가연산자(++) 가 사용되는 경우에는 비록 메시지 수신 객체가 nil 이라 하더라도 인자값은 정상적으로 처리되므로 주의해야 한다.

nil 수신자에 메시지를 전송할 경우 반환되는 값은 아래와 같다.

반환값으로 구조체를 넘겨받는 경우에는 앞에서 설명한 nil 수신자 처리가 정상적으로 동작하지 않는다. 따라서 이 경우에는 자바에서처럼 메시지를 전송하기 전에 수신자의 nil 여부를 확인해야 한다.  CGRect 등의 구조체는 어플리케이션에서 많이 사용되므로 주의할 필요가 있다.  (구조체를 반환하는 메시지 전송의 경우, 구조체가 하드웨어 레지스터를 통해 반환되는 경우는 예외라고 한다. 이에 대한 확인은  ABI Function Calling Guide 를 통해 확인가능하다고 하는데 구조체 반환 메시지에서는 수신자 nil 참조 확인을 무조건 하는 것으로 기억하는 것이 편할 것 같다.)

위와 같은 nil의 특성을 고려해 프로그램을 구성한다면 다음의 세 가지의 원칙을 지키는 것이 바람직하다.

1) 프로퍼티 접근자 생성

프로퍼티의 접근 메소드를 만들어 사용하면 프로퍼티 소유 객체의 값이 nil 인 경우에도  [myObject myProperty] 와 같이 사용할 수 있다. (프로퍼티 접근자 생성은 애플에서 권장하는 방법이다.)

그러나 만약 프로퍼티의 접근 메소드를 만들지 않고, myObject -> myProperty 로 코드를 작성할 경우엔 프로퍼티 소유 객체 myObject 가 nil 이면 프로그램은 종료된다.

2) 부재중 동작 패턴

객체가 부재중, 즉 nil 일 때 상황에 따라 추가적인 동작을 지정해야 하는 경우가 있다.

예를 들어 다음과 같다.


...
NSLock *lock;
...
-(void)method_O1{
    [lock lock];
    // do something
    [lock unlock];
}

-(void)method_02{
    [lock lock];
    // do something
    [lock unlock];
}

-(void)makeThreadSafe{

    lock = [NSLock new];
    ...
}

처음에는 lock 변수는 nil로 설정되어 있기 때문에, [lock lock] , [lock unlock] 은 아무런 작업을 하지 않는다. 실제로 스레드 안정성을 확보할 필요가 없을 때 이는 의미있게 사용될 수 있다.

그러나 다중 스레드로 동작하는 환경에서 스레드 안정성을 확보해야 하는 상황이 생길 수 있다. 이 때 위와 같은 방식으로 프로그래밍을 했다면 -makeThreadSafe 메소드를 호출해 lock 변수에 객체를 만들어 넣으면 된다.

위 예시처럼, 객체가 부재중(nil)일 때와 그렇지 않을 때의 용도를 고려해서 프로그래밍을 해야 할 필요도 있다. 다음과 같은 상황을 참고하자.

  • 로그 객체 : 로그 출력 객체가 설정돼 있다면 전달받은 메시지를 로그에 출력하고, 로그 객체가 없다면 로그 관련 작업을 하지 않는다.
  • 리스너 객체 : 리스너가 설정된 상태에서만 변경된 정보에 대한 메시지가 전송된다. 이미 리스너가 설정된 경우, nil 로 재설정하면 더 이상 변경사항이 전달되지 않는다.
  • 위임 객체 : 위임 객체가 있다면 위임 객체의 기능을 호출하고 없다면 기본 기능을 사용한다.

3) 없음의 일관성

프로퍼티에서는 0 또는 nil 의 개념을 없는 객체의 개념과 일관되게 설계하는 것이 좋다. 즉, 프로퍼티가 부정적인 값보다 긍정적인 값을 표현하도록 프로퍼티를 정의하는 것이 좋은 습관이다.

예를 들어 -(BOOL)isEmpty 보다 -(BOOL)hasObjects 가 바람직하다. isEmpty를 사용하면 수신자가 nil 일때 NO를 반환하므로 수신자가 nil 이란 의미인지, 객체를 가지고 있다는 의미인지 알 수 없다.

nil 을 잘 활용하기 위해서는 문서를 확인해야 한다. 자바와는 다르게 코코아 컬렉션 클래스는 nil 객체를 값이나 키로 지정할 수 없게 제한한다. 따라서 nil 값이 컬렉션에 추가되지 않도록 주의가 필요하다.

*Reference : Learn Objective-C for Java Developers

%d bloggers like this: