이 글의 코드들은 Swift 5.1.3, Swift 5.2에서 테스트했습니다.
Optional은 nil 값으로 비교할 수가 있어 조건문에서는 보통 다음과 같이 사용하게 됩니다.
let value: Value?
...
if value == nil {}
if value != nil {}
guard value == nil else {}
guard value != nil else {}
Optional의 값 비교가 가능한 이유는 swift에서 Optional을 구현할 때, ==, =! 연산자를 구현해놨기 때문입니다.
스위프트에서는 아래와 같은 함수를 구현하고 있습니다. 링크
static public func ==(lhs: Wrapped?, rhs: Wrapped?) -> Bool {...} // 값 끼리 비교
static public func !=(lhs: Wrapped?, rhs: Wrapped?) -> Bool {...} // 값 끼리 비교
static public func ==(lhs: _OptionalNilComparisonType, rhs: Wrapped?) -> Bool {} // nil과 optional 비교
static public func !=(lhs: _OptionalNilComparisonType, rhs: Wrapped?) -> Bool {} // nil과 optional 비교
static public func ==(lhs: Wrapped?, rhs: _OptionalNilComparisonType) -> Bool {} // optional과 nil 비교
static public func !=(lhs: Wrapped?, rhs: _OptionalNilComparisonType) -> Bool {} // optional과 nil 비교
따라서
let value: Value?
if value == nil {...}
와 같은 코드는
==(lhs: Wrapped?, rhs: _OptionalNilComparisonType) -> Bool
함수를 통해 값을 비교하게 됩니다. *이 부분은 SIL단계에서 명확하게 드러나게 되는데, 쓰고 싶지만 글이 쓸데없이 길어지니 참겠습니다.
이제 다음과 같은 코드가 있다고 해보겠습니다.
let value: Int? = ...
...
여기서 value라는 변수가 0보다 클 경우는 특정한 코드를 호출하고, 0과 같거나 작거나 혹은 값이 없을 경우에는 에러 코드를 호출하는 코드를 작성해보겠습니다.
if let value = value, value > 0 {
callSuccess()
} else {
callError()
}
옵셔널 체이닝의 바인딩을 이용해서 깔끔하게 로직을 분리할 수 있었습니다. 여기까지는 괜찮은거 같습니다.
이제부터 이 글을 쓰게 된 코드가 나옵니다.
피치못할 사정에 의해서 value가 0이거나 nil일 경우에 return문을 통해 해당 함수를 이탈해야 하는 경우가 생겼습니다.
// 1
if value == nil || (let value = value && value == 0) {
return
}
//혹은, 또는 or
// 2
if value == nil {
return
}
if let value = value, value == 0 {
return
}
// 아니면
// 3
if (value ?? 0) == 0 {
return
}
// |
// 4
if let value = value, value > 0 {
// do nothing
} else {
return
}
마지막 코드는 너무나 호러블하군요. 장난삼아 넣어봤습니다. 이런 코드를 실제로 사용할 때도 있다는 소문이 있지만, 루머에 현혹되지 마세요~
저는 1번은 길어서 싫고, 2번은 두 개의 조건문이 싫고, 3번은 괄호에 둘러쌓인 Nil-Coalescing(Nil 병합) 연산자가 마음에 들지 않았습니다. 이제 더 없죠?
Optional은 두 개의 고차함수를 제공하고 있습니다.
func map<U>(_ transform: (Int) throws -> U) rethrows -> U?
func flatMap<U>(_ transform: (Int) throws -> U?) rethrows -> U?
자 그럼 map 함수를 이용해서 바로 위의 제 마음에 들지 않는 코드를 좀 더 보기 좋게 바꿔보겠습니다.
처음에 연산자 오퍼레이터를 보시면 아시겠지만 Optional은 같은 값과의 비교가 가능합니다. 그래서 숫자와 nil 비교대신 Optional의 타입이 Bool이 되도록 바꿔보는 것이 열쇠입니다.
let value: Int? = nil
if value.map({ $0 == 0 }) == true {
print("값이 없거나 0이에요")
} else {
print("값이 있고, 0이 아니에요.")
}
결과는요?
값이 있고, 0이 아니에요.
입니다. 그러면 안되겠죠.
이번에는 map의 결과값 비교를 false로 해보겠습니다.
let value: Int? = nil
if value.map({ $0 == 0 }) == true {
print("값이 없거나 0이에요")
} else {
print("값이 있고, 0이 아니에요.")
}
결과는요?
값이 있고, 0이 아니에요.
입니다. 이래도 안되겠죠?
사실 Optional이 nil(== Optional.none) 인 경우는 어떤 값과 비교해도 false가 됩니다. 이것은
func ==(lhs: Wrapped?, rhs: Wrapped?)
함수에서 둘다 nil이 아니거나 둘다 nil인 경우에만 true를 반환하고 그 외에는 무조건 false를 반환하기 때문입니다.
이제 지금까지 알게 된 지식에 Nil 병합함수를 첨가해서 if문을 좀 더 개선해보겠습니다.
var value: Int? = nil
if value.map({ $0 == 0 }) ?? true {
print("값이 없거나 0이에요")
} else {
print("값이 있고, 0이 아니에요.")
}
value = 0
if value.map({ $0 == 0 }) ?? true {
print("값이 없거나 0이에요")
} else {
print("값이 있고, 0이 아니에요.")
}
결과는요?
nil일 때도 값이 없거나 0이에요
0일 때도 값이 없거나 0이에요
를 출력합니다.
value.map 함수를 통해 nil이 아닌 경우에는 0으로 비교를 하게 되어서 true가 됩니다. nil인 경우에는 Nil 병합 연산자를 통해 true가 됩니다. 따라서 원하는 조건에서 항상 true가 되게 됩니다. 아니 그런데 if (value ?? 0) == 0
가 차라리 더 낫지 않나요? 라고 여쭤보신다면, 그러게요. 그래도 언젠가 이 지식이 쓸모 있기를 바랍니다. 제게는 쓸모가 있었습니다 후후.
이상으로 Optional의 비교에 대해서 조금 알아봤습니다.
고민하고 있는 코드를 소개합니다.
var count: Int? = nil
var mapResult = count.map({ _ -> Int? in Optional.none })
print("result(\(mapResult)) is \( mapResult == nil ? "is nil" : "is not nil")")
var flatMapResult = count.flatMap({ _ -> Any in Optional<Any>.none })
print("result(\(flatMapResult)) is \( flatMapResult == nil ? "is nil" : "is not nil")")
print(mapResult == nil)
print(flatMapResult == nil)
print("")
count = 0
mapResult = count.map({ _ -> Int? in Optional.none })
print("result(\(mapResult)) is \( mapResult == nil ? "is nil" : "is not nil")")
flatMapResult = count.flatMap({ _ -> Any in Optional<Any>.none })
print("result(\(flatMapResult)) is \( flatMapResult == nil ? "is nil" : "is not nil")")
print(mapResult == nil)
print(flatMapResult == nil)
결과가 어떨거 같으세요?