Optional Code

이원희·2021년 3월 13일
1

 🐧 Swift

목록 보기
32/32
post-thumbnail

Optional에 대하여 포스팅에서 Optional에 대해 다뤘다.
오늘은 Swift Optional 코드를 살펴보자.
(사실... 그냥 재밌어 보였음ㅋㅋㅋㅋ)

Optional enum

@frozen
public enum Optional<Wrapped>: ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
    
    public init(_ some: Wrapped) { self = .some(some) }
    public init(nilLiteral: ()) { self = .none }
}

Optional 코드를 보면 위와 같이 구현되어 있다.

@frozen

공식 문서를 봐보자.

@frozenstruct 또는 enumtype의 변경을 제한하는 용도로 선언할 수 있다.

이 속성은 라이브러리 evolution 코드에서 컴파일 할 때에만 사용된다.

이후 버전의 라이브러리는 enum case 또는 struct저장 인스턴스 property를 추가, 제거, 재정렬 같은 변경을 실행할 수 없다.

🤔 Optional은 @frozen으로 선언되었으므로 case가 추가 되지 않음을 보장한다.

ExpressibleByNilLiteral

공식 문서를 봐보자.

ExpressibleByNilLiteral프로토콜이다.

nil을 사용하여 초기화 할 수 있는 type이다.

nil은 Swift에서 값이 없다는 의미를 가지고 있다.

Optional type만 ExpressibleByNilLiteral을 준수한다.

해당 프로토콜은 init(nilLiteral:())을 구현해야 한다.

init(nilLiteral:())nil로 인스턴스를 초기화한다.

🤔 Optional은 값이 있을수도 없을수도(nil) 있는 타입이므로 nil로 초기화되는 인스턴스가 필요하다.

Code

Optional은 @frozen 선언으로 이후에 case가 추가되지 않음을 보장한다.

<Wrapped>를 통해 Generics로 구현되어 있어 Int?, Double?과 같이 다양한 type에 대응할 수 있도록 설계되었다.

만약, <Wrapped: Hashable>Generics에 제한이 있다면 Hashable을 준수하는 type에서만 Optional을 사용할 수 있지만 Optional은 제한 없는 Generics을 사용해 어떤 type이든 Optional을 사용할 수 있도록 설계되어 있다.

Optional은 두 개의 init()을 가지고 있다.

init(_ some: Wrapped)입력받은 값으로 인스턴스를 초기화 한다.
즉, self = .some(some) 구문을 통해 입력 받은 값을 지닌 .some case로 초기화 한다.

init(nilLiteral: ())ExpressibleByNilLiteral 프로토콜의 구현부nil로 인스턴스를 초기화한다.
즉, nil로 초기화하게 되면 self = .none 구문을 통해 .none case로 초기화한다.


Optional, Equatable

let group1 = [1, 2, 3]
let group2 = [1, 3, 4]

if group1.first == group2.first {
    print("Equal")
} else {
    print("not Equal")
}

group1.first, group2.first는 Optional type이다.

위의 코드에서 ==을 통해 두 값이 같은지 비교하고 있다.

이전 포스팅에서 Equatable 프로토콜을 통해 값이 같은지 비교할 수 있다고 했다.

그럼 Optional은 어떻게 Equatable을 준수하고 있는지 확인해 보자.

extension Optional: Equatable where Wrapped: Equatable {
    @inlinable
    public static func ==(lhs: Wrapped?, rhs: Wrapped?) -> Bool {
        switch (lhs, rhs) {
        case let (l?, r?):
            return l == r
        case (nil, nil):
            return true
        default:
            return false
        }
    }
}

🤔 extension으로 Wrapped가 Equatable을 준수하고 있다면 ==을 사용할 수 있도록 되어 있다.

Equatable을 준수하기 위해서는 ==을 구현해야야 한다.


Optional, Hashable

var dic: [Int?: String] = [:]
dic[nil] = "nil"
dic[1] = "1"

print(dic[nil])
print(dic[1])

dicInt? type을 key로 갖는 dictionary이다.

이전 포스팅에서 dictionary의 key로 사용하는 type은 Hashable을 준수하고 있어야 한다고 했다.

그럼 Optional이 어떻게 Hashable을 준수하고 있는지 확인해 보자.

extension Optional: Hashable where Wrapped: Hashable {
    @inlinable
    public func hash(into hasher: inout Hasher) {
        switch self {
        case .none:
            hasher.combine(0 as UInt8)
        case .some(let wrapped):
            hasher.combine(1 as UInt8)
            hasher.combine(wrapped)
        }
    }
}

🤔 extension으로 Wrapped가 Hashable을 준수하고 있다면 해시값을 생성할 수 있도록 되어 있다.

Hashable을 준수하기 위해서는 hash()를 구현해야 한다.

hash()Hasher 구조체에 있는 메서드로 내부에서 combine(_:)을 호출해 해시 값을 생성한다.
combine(_:)은 입력 받은 값을 해시 시드와 혼합해 해시 값을 생성한다.

nil일 경우에는 0으로 해시 값을 생성한다.
값이 있는 경우에는 1과 래핑된 값으로 해시 값을 생성한다.


nil-coalescing operation

let number: Int? = 5
let newNumber = number ?? Int("1")
let newNumber2 = number ?? 0

print(newNumber)	// Optional(5)
print(newNumber2)	// 5

?? 연산자는 Optional 바인딩 하지 않고 Optional 값에 안전하게 접근하는 방법 중 하나이다.

?? 연산자를 이용해 Optional 값이 nil인 경우 default value를 지정하는 방식이다.

number ?? Int("1")number ?? 0의 결과가 다르다.

같은 ?? 연산자를 사용했는데 왜 다를까?

@_transparent
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T)
rethrows -> T {
    switch optional {
    case .some(let value):
        return value
    case .none:
        return try defaultValue()
    }
}	// 1번 ??

@_transparent
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?)
rethrows -> T? {
    switch optional {
    case .some(let value):
        return value
    case .none:
        return try defaultValue()
    }
}	// 2번 ??

Optional에는 ?? 연산자가 두개 구현되어 있다.

두 개의 차이점은 defaultValue반환 값이 다르다.
(하나는 Optional, 하나는 non-Optional)

non-Optional을 반환하는 ??를 1번 ??로 부르고, Optional을 반환하는 ??를 2번 ??로 부르자.

number ?? Int("1")에는 2번 ??이 호출되었다.
number ?? 0에는 1번 ??이 호출되었다.

@autoclosure

🤔 흠... 그런데 나는 number ?? 0이라고 코드를 작성해서 잘 돌아갔단 말이지?
근데 ?? 연산자가 구현된 곳을 보니 defaultValue가 클로저네...?
하지만 0은 클로저가 아닌데.... 어떻게 에러가 안나지??

정답은 @autoclosure를 선언했기 때문이다.

공식 문서를 확인해보자.

autoclosure는 함수에 인수로 전달된 expression을 자동으로 closure로 래핑한다.

일반 구문을 인자값으로 넣어도 컴파일러가 자동으로 클로저를 만들어서 사용한다.

인자 값을 클로저 형식으로 넣어줄 필요가 없어 {} 형태가 아닌 () 형태로 사용할 수 있다.

autoclosure를 사용하면 클로저를 호출할 때까지 내부 코드가 실행되지 않아 지연 실행할 수 있다.

🤔 @autoclosure로 선언했기 때문에 number ?? 0이라고 선언한 코드에서 0도 자동으로 closure로 래핑이 되는구나!
@autoclosure는 클로저가 호출 될때 실행되므로 Optional이 nil일 경우에만 클로저가 호출된다.

Code

Optional은 바인딩하지 않고 Optional에 안전하게 접근하기 위해 default value를 지정할 수 있는 ??연산자를 제공한다.

?? 연산자는 default value도 Optional value인 경우와 default value는 non-Optional value인 경우 두가지 방식으로 구현되어 있다.

default value를 반환할 때 클로저를 사용하는데 해당 연산자에는 @autoclosure 키워드를 사용해 자동으로 클로저를 생성하는 방식을 채택하고 있다.

??연산자의 반환 값은 Optional type의 type을 반환한다.


Optional, Comparable

Optional 코드를 다 다뤄봤다.

근데 Optional에서 Comparable을 찾아볼 수가 없다...?

🤔 Comparable비교를 가능하게 해주는 타입인데... 그럼 Optional은 비교가 불가한가?

let one: Int? = 1
let two: Int? = 2

if one < two {
    print("yes")
}

위와 같은 코드는 가능한가?

정답은 불가능하다.

있는데 옵셔널을 바인딩하던지!
?? 연산자로 default value를 지정하던지!
! 연산자로 강제 언래핑하던지!

라고 error를 내뱉는다.

결국은 Optional type으로는 비교 연산자를 사용 할 수 없다는 뜻!

오... 평소에 Optional 값을 비교하면 error를 뱉던 이유가 Optional이 Comparable을 채택하고 있지 않아서였다...!ㅋㅋㅋ(대발견....)

그렇다면 Optional에서 부등호를 사용해 값을 비교하고 싶다면 어떻게 할 수 있을까?

Optional이 Equatable, Hashable을 채택하는 방식에서 아이디어를 가져와 보자.

extension Optional: Comparable where Wrapped: Comparable {
    public static func < (lhs: Wrapped?, rhs: Wrapped?) -> Bool {
        switch (lhs, rhs) {
        case let (l?, r?):
            return l < r
        case (nil, nil): 
            return false
        default:
            return false
        }
    }
}

이렇게 extension으로 Comparable을 준수하도록 했다.
Generics으로 받는 타입이 Comparable을 준수하고 있어야 부등호 비교가 가능하기 때문에 where Wrapped: Comparable로 제약을 걸어준다.

Comparable을 준수하기 위해서는 < 메서드를 구현해야 한다.
switch 문으로 비교 하려는 두 값의 케이스를 나눈다.
만약 두 값이 모두 값이 있다면 이미 lhs, rhsComparable을 준수하고 있으므로 두 값을 부등호로 비교한 값을 반환한다.

두 값 중 하나라도 nil이라면 false를 주고 있지만 이 부분은 요구사항에 따라 달라질 수 있는 부분이다.


마무리

오늘은 Optional 코드를 한 번 뜯어봤다.
그럼 이만👋

0개의 댓글