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
شرکتی که در آن کار میکنید در آستانهی عرضهی گوشی هوشمند تازهاش به نام Smarty 5 است؛ این گوشی به یک سیستم دوربین تازه، یک طراحی جدید و یک پردازندهی انقلابی مجهز است که روی سیستمعامل جدیدی به نام smartyOS اجرا میشود.
پردازندهی جدید این توان را دارد که رمزهای عبور امنتری از همیشه را پردازش کند و شرکت تصمیم گرفته از این مزیت به نفع خودش استفاده کند. آنها از شما خواستهاند که یک سیستم قفل رمز عبور برای این گوشی پیادهسازی کنید تا کاربر بتواند رمزی تعیین کند و سپس بررسی کند که رمز دادهشده درست است یا نه.
سیستم رمز عبوری که در این تمرین با آن کار میکنید امن نیست و فقط با هدف آموزشی به کار میرود. این سیستم نباید در هیچ برنامهی واقعی به کار رود.
گوشی به کاربران اجازه میدهد از چند نوع مختلف رمز عبور استفاده کنند، از یک رمز عبور عددی ساده تا یک رمز عبور حرفوعددی امنتر و حتی رمزی که اثر انگشتشان را ذخیره میکند.
این انواع مختلف رمز عبور از نوعهای متفاوتی استفاده میکنند: رمز عبور عددی از Int32 استفاده میکند، رمز عبور حرفوعددی از String و رمز عبور اثر انگشت از Float64.
کلاس PasswordLock را با یک سازنده پیادهسازی کنید که هر یک از این سه نوع رمز عبور را به عنوان آرگومان میگیرد و آن را در یک متغیر نمونهای به نام @password ذخیره میکند.
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
شرکت تصمیم گرفته رمز عبور را رمزگذاری کند تا بهصورت متن ساده ذخیره نشود و از شما خواسته است متدی برای رمزگذاری آن پیادهسازی کنید.
هر نوع رمز عبور، روش یکتای خودش را برای رمزگذاری دارد:
Int32: رمز عبور بر دو تقسیم میشود و به نزدیکترین عدد صحیح گرد میشود.String: رمز عبور برعکس میشود.Float64: رمز عبور در چهار ضرب میشود.یک متد نمونهای به نام encrypt پیادهسازی کنید که هیچ آرگومانی نمیگیرد و متغیر نمونهای @password را طوری تغییر میدهد که رمزگذاری شود.
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"