Crystal में एक वेरिएबल कई टाइप का हो सकता है। इसे यूनियन टाइप कहा जाता है। Crystal में यह आम बात है कि कंपाइलर खुद यूनियन टाइप का अनुमान लगा लेता है।
रनटाइम पर यूनियन टाइप फिर भी एक ही टाइप होता है, भले ही वह कई टाइपों से मिलकर बना हो।
इसका मतलब है कि अगर कोई यूनियन टाइप String और Int32 से बना है, तो वह एक ही समय में दोनों नहीं होगा।
वह या तो String होगा या Int32।
यूनियन टाइप घोषित करने के लिए टाइपों को पाइप (|) से अलग किया जाता है।
इन्हें अक्सर गोल ब्रैकेट में लिखा जाता है, लेकिन यह ज़रूरी नहीं है।
सबसे आम यूनियन टाइप (T | Nil) है, जहाँ T कोई टाइप है। यह उन मेथड में मिल सकता है जो Nil लौटा सकते हैं।
इस स्थिति को वेरिएबल का निलेबल होना भी कहा जाता है।
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
निलेबल का मतलब है कि वेरिएबल किसी एक टाइप का या Nil का हो सकता है।
इसे (T | Nil) लिखा जा सकता है।
लेकिन चूँकि निलेबल टाइप काफी आम हैं, इनके लिए एक शॉर्टहैंड है: T?।
# This:
foo : (String | Nil) = "Hello"
foo = nil
# Is the same as:
foo : String? = "Hello"
foo = nil
अगर कंपाइलर को पक्का नहीं पता कि टाइप कौन-सा है, तो वह यूनियन टाइप मान लेता है।
उदाहरण के लिए, नीचे दिए कोड में कंपाइलर को यह नहीं पता चलेगा कि foo किस टाइप का है, क्योंकि वह String भी हो सकता है और Int32 भी।
if true
foo = 1
else
foo = "Hello"
end
typeof(foo) # => (Int32 | String)
यह अनुमान अपने आप लगता है। ऐसे और भी मौके हैं, जहाँ कंपाइलर मान लेता है कि कोई मेथड यूनियन टाइप लौटाता है।
character = "Hello world"[10]?
typeof(character) # => (Char | Nil)