Crystal에서는 변수가 여러 타입으로 이루어질 수 있어요. 이것을 유니언 타입이라고 해요. Crystal에서는 컴파일러가 유니언 타입을 추론하는 경우가 흔해요.
유니언 타입은 여러 타입으로 이루어져 있어도 런타임에서는 여전히 하나의 타입이에요.
즉, 유니언 타입이 String과 Int32로 이루어져 있다면 동시에 둘 다가 되는 것이 아니에요.
대신 String이거나 Int32 중 하나가 돼요.
유니언 타입은 타입들을 파이프(|)로 구분해서 선언해요.
보통 괄호 안에 넣지만, 꼭 그럴 필요는 없어요.
가장 흔한 유니언 타입은 T가 어떤 타입일 때 (T | Nil)이고, Nil을 반환할 수 있는 메서드에서 나타날 수 있어요.
이것은 변수가 nilable하다고도 해요.
foo : String = "Hello"
foo = nil # Error: type must be String, not (String | Nil)
foo : (String | Nil) = "Hello"
foo = nil
타입이 꼭 두 개로 제한되는 것은 아니고, 원하는 만큼 많아도 돼요.
foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0
typeof와 Object#class
변수의 타입을 알아내는 방법은 두 가지가 있어요.
typeof를 사용하는 방법과 Object#class를 사용하는 방법이에요.
차이점은 typeof는 컴파일 타임에 변수의 타입을 반환하고, Object#class는 런타임에 타입을 반환한다는 거예요.
즉, 변수가 유니언 타입인지 확인하고 싶을 때 Object#class는 그것을 알려줄 수 없어요. 런타임에는 단일 타입인 타입만 반환하기 때문이에요.
foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String
유니언 타입은 런타임에서는 단일 타입이기 때문에, 모든 일반적인 연산이 동작해요. 하지만 코드를 컴파일할 때 컴파일러는 그것이 어떤 타입인지 알아야 해요. 따라서 타입별 연산을 사용하려면, 코드가 해당 타입 중 하나만 될 수 있는 방식으로 작성되어야 해요.
foo : (String | Int32) = "Hello"
foo.downcase # Error: undefined method 'downcase' for (String | Int32)
Crystal에는 유니언 타입을 위한 특별한 메서드가 있어요. 바로 is_a? 메서드로, 타입을 인자로 받아 불리언을 반환해요.
nil? 메서드는 is_a?(Nil)의 축약형이에요.
is_a? 메서드를 제어 표현식에 넣으면 컴파일러에게 어떤 타입인지 알려주게 되고, 따라서 그것이 그 타입임을 보장해요.
그리고 else 분기에서는 그것이 그 타입이 아니라는 것이 보장돼요.
foo : (String | Int32) = "Hello"
if foo.is_a?(String)
typeof(foo) # => String
foo.downcase # => "hello"
end
이 is_a?는 인자로 단일 타입만 받는 것으로 제한되지 않고, 유니언 타입도 받을 수 있어요.
여러 타입에 대해 &&와 함께 사용할 수도 있어요.
is_a? 메서드는 제어 표현식과 함께 사용할 때 인스턴스 변수나 클래스 변수에는 쓸 수 없어요.
대신 이런 것들은 먼저 지역 변수에 할당해야 해요.
유니언 타입을 단일 타입으로 만드는 한 가지 방법은, 특정 타입일 때만 분기에 들어갈 수 있게 만드는 거예요.
또 다른 방법은 as 메서드를 사용하는 거예요.
이 메서드는 런타임 검사를 통해 유니언 타입을 단일 타입으로 만들어요.
타입이 기대한 타입이 아니면 예외가 발생해요.
foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"
foo.as(Int32) # Error: can't cast String to Int32
이 방법은 타입이 기대한 타입이라고 확신할 때, 또는 그렇지 않을 때 예외를 발생시키고 싶을 때만 쓰는 거예요.
이 방법을 잘못된 설정과 함께 사용하면 예상치 못한 동작이 생길 수 있어요.
as?는 as와 아주 비슷하게 동작하지만, 타입이 기대한 타입이 아니면 예외를 발생시키는 대신 nil을 반환해요.
즉, 기대한 타입과 Nil의 유니언 타입을 반환해요.
foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"
foo.as?(Int32) # => nil
nilable은 변수가 어떤 타입이거나 Nil일 수 있다는 뜻이에요.
이것은 (T | Nil)로 쓸 수 있어요.
그런데 nilable 타입은 비교적 흔하기 때문에, T?라는 축약형이 있어요.
# This:
foo : (String | Nil) = "Hello"
foo = nil
# Is the same as:
foo : String? = "Hello"
foo = nil
여러분이 일하는 회사에서 곧 Smarty 5라는 새로운 스마트폰을 출시하려고 해요. 이 스마트폰에는 완전히 새로운 카메라 시스템과 새로운 디자인, 그리고 smartyOS라는 새로운 운영 체제에서 동작하는 혁신적인 새 프로세서가 들어 있어요.
새 프로세서는 그 어느 때보다 안전한 비밀번호를 처리할 수 있는 성능을 갖추고 있어서, 회사는 이를 활용하기로 했어요. 그래서 회사는 사용자가 비밀번호를 설정하고, 입력한 비밀번호가 올바른지 확인할 수 있는 휴대폰용 비밀번호 잠금 시스템을 구현해 달라고 요청했어요.
이 연습 문제에서 다루는 비밀번호 시스템은 안전하지 않으며, 교육 목적으로만 사용해요. 실제 애플리케이션에는 절대 사용하면 안 돼요.
이 휴대폰에서는 단순한 숫자 비밀번호부터 더 안전한 영숫자 비밀번호, 심지어 지문을 저장하는 비밀번호까지 여러 가지 유형의 비밀번호를 사용할 수 있어요.
이 비밀번호들은 각각 다른 타입을 사용해요. 숫자 비밀번호는 Int32, 영숫자 비밀번호는 String, 지문 비밀번호는 Float64를 사용해요.
세 가지 유형의 비밀번호 중 하나를 인자로 받아 @password라는 인스턴스 변수에 저장하는 초기화 메서드를 가진 PasswordLock 클래스를 구현해요.
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
회사는 비밀번호가 평문으로 저장되지 않도록 암호화하기로 했고, 이를 위한 메서드를 구현해 달라고 요청했어요.
비밀번호 유형마다 고유한 암호화 방식이 있어요.
Int32 비밀번호: 비밀번호를 2로 나누고 가장 가까운 정수로 반올림해요.String 비밀번호: 비밀번호를 뒤집어요.Float64 비밀번호: 비밀번호에 4를 곱해요.인자를 받지 않고 @password 인스턴스 변수를 암호화된 값으로 수정하는 encrypt 인스턴스 메서드를 구현해요.
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
password_lock.encrypt
# => #<PasswordLock:0x7f8e1b8c0b80 @password=617>
회사는 주어진 비밀번호가 올바른지 확인하는 메서드도 구현해 달라고 요청했어요.
비밀번호가 틀리면 nil을, 올바르면 "Unlocked"를 반환하는 메서드를 원해요.
확인할 비밀번호를 인자로 받는 unlock? 인스턴스 메서드를 구현해요. 이 메서드는 비밀번호가 틀리면 nil을, 올바르면 "Unlocked"를 반환해야 해요.
password_lock = PasswordLock.new(1234)
password_lock.encrypt
password_lock.unlock?(1234)
# => "Unlocked"
Exercism에 가입하고 Crystal 트랙을 개념 26개연습 문제 142개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.