در Crystal یک متغیر میتواند از چند نوع تشکیل شود. به این نوع اجتماع میگویند. در Crystal رایج است که کامپایلر یک نوع اجتماع را استنتاج کند.
یک نوع اجتماع در زمان اجرا همچنان یک نوع واحد است، حتی اگر از چند نوع تشکیل شده باشد.
یعنی اگر یک نوع اجتماع از String و Int32 ساخته شده باشد، در آن واحد هر دو نخواهد بود.
بلکه یا String خواهد بود یا Int32.
یک نوع اجتماع با جدا کردن نوعها با یک خط عمودی (|) اعلام میشود.
اغلب آنها را داخل پرانتز میگذارند، اما این کار الزامی نیست.
رایجترین نوع اجتماع (T | Nil) است که در آن T یک نوع است و میتواند در متدهایی ظاهر شود که میتوانند 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
اگر کامپایلر مطمئن نباشد که نوع چیست، یک نوع اجتماع استنتاج میکند.
برای مثال، در کد زیر کامپایلر نمیداند که نوع foo چیست، چون میتواند یا String باشد یا Int32.
if true
foo = 1
else
foo = "Hello"
end
typeof(foo) # => (Int32 | String)
این استنتاج به صورت خودکار انجام میشود و سناریوهای دیگری هم وجود دارد که برای مثال کامپایلر استنتاج میکند یک متد یک نوع اجتماع برمیگرداند.
character = "Hello world"[10]?
typeof(character) # => (Char | Nil)