Optional에 대하여 포스팅에서 Optional에 대해 다뤘다.
오늘은 Swift Optional 코드를 살펴보자.
(사실... 그냥 재밌어 보였음ㅋㅋㅋㅋ)
@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
은 struct
또는 enum
에 type의 변경을 제한하는 용도로 선언할 수 있다.
이 속성은 라이브러리 evolution 코드에서 컴파일 할 때에만 사용된다.
이후 버전의 라이브러리는 enum
case 또는 struct
저장 인스턴스 property를 추가, 제거, 재정렬 같은 변경을 실행할 수 없다.
🤔 Optional은 @frozen
으로 선언되었으므로 case가 추가 되지 않음을 보장한다.
공식 문서를 봐보자.
ExpressibleByNilLiteral
은 프로토콜이다.
nil
을 사용하여 초기화 할 수 있는 type이다.
nil
은 Swift에서 값이 없다는 의미를 가지고 있다.
Optional type만 ExpressibleByNilLiteral
을 준수한다.
해당 프로토콜은 init(nilLiteral:())
을 구현해야 한다.
init(nilLiteral:())
은 nil
로 인스턴스를 초기화한다.
🤔 Optional은 값이 있을수도 없을수도(nil
) 있는 타입이므로 nil
로 초기화되는 인스턴스가 필요하다.
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로 초기화한다.
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
을 준수하기 위해서는 ==
을 구현해야야 한다.
var dic: [Int?: String] = [:]
dic[nil] = "nil"
dic[1] = "1"
print(dic[nil])
print(dic[1])
dic
은 Int?
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과 래핑된 값으로 해시 값을 생성한다.
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번 ??
이 호출되었다.
🤔 흠... 그런데 나는 number ?? 0
이라고 코드를 작성해서 잘 돌아갔단 말이지?
근데 ??
연산자가 구현된 곳을 보니 defaultValue
가 클로저네...?
하지만 0은 클로저가 아닌데.... 어떻게 에러가 안나지??
정답은 @autoclosure
를 선언했기 때문이다.
공식 문서를 확인해보자.
autoclosure
는 함수에 인수로 전달된 expression을 자동으로 closure로 래핑한다.
일반 구문을 인자값으로 넣어도 컴파일러가 자동으로 클로저를 만들어서 사용한다.
인자 값을 클로저 형식으로 넣어줄 필요가 없어 {}
형태가 아닌 ()
형태로 사용할 수 있다.
autoclosure
를 사용하면 클로저를 호출할 때까지 내부 코드가 실행되지 않아 지연 실행할 수 있다.
🤔 @autoclosure
로 선언했기 때문에 number ?? 0
이라고 선언한 코드에서 0도 자동으로 closure로 래핑이 되는구나!
@autoclosure
는 클로저가 호출 될때 실행되므로 Optional이 nil
일 경우에만 클로저가 호출된다.
Optional은 바인딩하지 않고 Optional에 안전하게 접근하기 위해 default value를 지정할 수 있는 ??
연산자를 제공한다.
??
연산자는 default value도 Optional value인 경우와 default value는 non-Optional value인 경우 두가지 방식으로 구현되어 있다.
default value를 반환할 때 클로저를 사용하는데 해당 연산자에는 @autoclosure
키워드를 사용해 자동으로 클로저를 생성하는 방식을 채택하고 있다.
??
연산자의 반환 값은 Optional type의 type을 반환한다.
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
, rhs
는 Comparable
을 준수하고 있으므로 두 값을 부등호로 비교한 값을 반환한다.
두 값 중 하나라도 nil
이라면 false
를 주고 있지만 이 부분은 요구사항에 따라 달라질 수 있는 부분이다.
오늘은 Optional 코드를 한 번 뜯어봤다.
그럼 이만👋