تسمح Crystal للمتغير بأن يتكوّن من أنواع متعددة، ويُسمى هذا نوع الاتحاد. وفي Crystal، من الشائع أن يستنتج المترجم نوع الاتحاد بنفسه.
يظل نوع الاتحاد نوعًا واحدًا في وقت التشغيل، حتى لو كان يتكوّن من أنواع متعددة.
وهذا يعني أنه إذا كان نوع الاتحاد مبنيًا من String وInt32، فلن يكون كليهما في الوقت نفسه.
بل سيكون إما String أو Int32.
يُعرَّف نوع الاتحاد بفصل الأنواع بخط عمودي (|). وغالبًا ما تُوضع هذه الأنواع داخل الأقواس الهلالية، لكن ذلك ليس شرطًا. وأكثر أنواع الاتحاد شيوعًا هو (T | Nil) حيث T نوع ما، وقد يظهر في الطرق التي يمكنها إرجاع Nil. ويُعرف هذا أيضًا بأن المتغير قابل لأن يكون 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
النوع القابل لـ Nil يعني أن المتغير يمكن أن يكون إما نوعًا ما أو Nil. ويمكن كتابته على الصورة (T | Nil). ولكن بما أن الأنواع القابلة لـ 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"
سجّل في Exercism لتتعلّم وتتقن Crystal عبر 26 مفهومًا142 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.