أنواع البيانات (Data Types) في Kotlin
إن كلَّ عنصرٍ في Kotlin يعد كائنًا إذ يمكن استدعاء الدوال (member functions) والخاصّيّات (properties) عبر أي متحول (variable)، ولبعض الأنواع تمثيلها الداخلي الخاص بها؛ فعلى سبيل المثال تُمثَّل الأعداد والمحارف والقيم المنطقية (boolean) كقيمٍ أساسيّةٍ أثناء التشغيل (runtime) ولكنها بالنسبة للمستخدم مجرّد أصنافٍ عادية، وتناقش هذه الصفحة الأنواع الرئيسيّة للبيانات في Kotlin وهي: الأعداد، والمحارف، والقيم المنطقية (boolean)، والمصفوفات، والسلاسل النصيّة.
الأعداد (Numbers)
تتعامل لغة Kotlin مع البيانات العدديّة بطريقةٍ مماثلةٍ للغة Java ولكن بفوارق بسيطة، فلا تدعم مثلًا لغة Kotlin التحويل الضمني (implicit) (من نوع البيانات الأصغر إلى الأكبر [widening]) للأعداد وتختلف القيم المباشرة (literals) في بعض الحالات.
توفِّر Kotlin الأنواع الأساسيّة الآتية للتعبير عن الأعداد (وهي مشابهةٌ لما في لغة Java):
النوع | سعته (bit) |
---|---|
Double | 64 |
Float | 32 |
Long | 64 |
Int | 32 |
Short | 16 |
Byte | 8 |
ولا تُعدُّ المحارفُ أرقامًا في Kotlin.
القيم المباشرة الثابتة (Literal Constants)
ولها الأنواع الآتية للأعداد الصحيحة:
- الأعداد العشرية (decimals) مثل:
123
وتُميَّز الأعداد العشرية الطويلة بالحرفL
(بحالته الكبيرة فقط) لتصبح بالشكل:123L
- الأعداد الست عشرية (hexadecimal) مثل
0x0F
- الأعداد الثنائية (binaries) مثل
0b00001011
ولا تدعم لغة Kotlin القيم الثمانيّة (octal)، لكنّها تدعم التسمية الاصطلاحيّة (conventional notation) للأعداد ذات الفاصلة العائمة:
- من نوع Double (وهي الحالة الافتراضية) مثل:
123.5
و123.5e10
- من نوع Float (والتي تُلحَق بالحرف
f
أوF
) مثل:123.5f
الشرطة السفلية (Underscore) في القيم العدديّة المباشرة (Numeric Literals) بدءًا من الإصدار 1.1
تُستخدم الشرطة السفلية لتسهيل قراءة الأعداد الكبيرة مثل:
val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010
تمثيل الأعداد (Representation)
تُخزَّن الأعداد فيزيائيًا في منصّة Java كأنواعٍ أساسيّة في JVM ما لم يكن هناك مرجعيّة nullable للعدد (مثل: Int? ) أو لم يكن النوع generics مضمّنًا، حينئذٍ يتم تغليف الأعداد (boxed numbers) وهذا لا يحافظ بالضرورة على ماهيتها (identity)، كما في الشيفرة:
val a: Int = 10000
print(a === a) // سيطبع true
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA === anotherBoxedA) // سيطبع false
ولكنه يحافظ على التساوي بالقيمة (equality) (بالرمز ==
):
val a: Int = 10000
print(a == a) // سيطبع true
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA) // أيضًا سيطبع true
التحويل الصريح (Explicit Conversion)
لا تُعدُّ الأنواع الأصغر في لغة Kotlin أنواعًا فرعيةً (subtypes) من الأنواع الأكبر بسبب اختلاف طرائق تمثيل البيانات العددية بينها، وإن كانت كذلك فستنجم المشاكل الآتية:
// هذه شيفرة وهمية ولن تعمل بشكل صحيح
val a: Int? = 1 // عدد صحيح مُغلَّف (java.lang.Integer)
val b: Long? = a // تحويل ضمني ينتج عنه نوع مغلَّف بنوع Long (java.lang.Long)
print(a == b) // هذا سينتج عنه نتيجة false
والسبب بظهور نتيجة false
في السطر الأخير من الشيفرة السابقة هو أنّ الدالة equals
تتحقّق من كون المتحول الآخر من نوع Long
أيضًا وهذا غير محقّق، وبهذا فإننا لم نفقد فقط الماهيّة (identity) بل التساوي (equality) أيضًا، ولذلك لا تُحوَّل الأنواع الأصغر ضمنيًّا إلى الأنواع الأكبر، وبالتالي لا يُمكن إسناد قيمة من نوع Byte
مثلًا إلى قيمة من نوع Int
بدون التصريح عن ذلك التحويل، مثل:
val b: Byte = 1 // لا مشكلة هنا
val i: Int = b // إسناد خاطئ
وبإضافة التحويل الصريح تصبح الشيفرة بالشكل:
val i: Int = b.toInt() // تحويل صريح من النوع الأصغر للنوع الأكبر
ويمكن استخدام أيّ من التحويلات الآتية على الأعداد:
toByte()
: للتحويل إلى نوعByte
toShort()
: للتحويل إلى نوعShort
toInt()
: للتحويل إلى نوعInt
toLong()
: للتحويل إلى نوعLong
toFloat()
: للتحويل إلى نوعFloat
toDouble()
: للتحويل إلى نوعDouble
toChar()
: للتحويل إلى نوعChar
وليس غياب التحويلات الضمنية (implicit) بمشكلة؛ لأنّ النوع يُخمَّن من خلال السياق، وكذلك لوجود زيادة تحميل (overloading) في العمليات الحسابية (arithmetical operations) بما يتوافق مع التحويل الأنسب لها، مثل:
val l = 1L + 3 // Long + Int => Long
العمليات (Operations)
تدعم Kotlin مجموعة العمليات الحسابيّة القياسيّة على الأعداد وهي مُعرَّفة كعناصر (members) في الأصناف المناسبة إذ يُؤقلِم المُترجم (compiler) الاستدعاءات بحسب التعليمات الموافقة. (راجع التحميل الزائد للمعاملات [Operator overloading])
أما في العمليات على مستوى الخانة (bitwise operations) فلا توجد محارف خاصةٌ بها بل مجرّد دوال يمكن استدعاؤها بالشكل الداخليّ (infix form)، مثل:
val x = (1 shl 2) and 0x000FF000
وفيما يلي قائمةٌ بالعمليات الثنائيّة بمستوى الخانة (والمتاحة للنوعين Int
و Long
فقط)
shl(bits)
الإزاحة مع إشارة نحو اليسار (مثل المعامل>>
في Java)shr(bits)
الإزاحة مع إشارة نحو اليمين (مثل المعامل<<
في Java)ushr(bits)
الإزاحة بدون إشارة نحو اليمين (مثل المعامل<<<
في Java)and(bits)
العملية المنطقية AND على مستوى الخانة الثنائيةor(bits)
العملية المنطقية OR على مستوى الخانة الثنائيةxor(bits)
العملية المنطقية XOR على مستوى الخانة الثنائيةinv()
لعكس حالة الخانات الثنائية (NOT)
مقارنة الأعداد ذات الفاصلة العائمة (Floating Point Numbers)
تناقش هذه الفقرة عمليات المقارنة (بين الأعداد ذات الفاصلة العائمة) الآتية:
- التحقق من التساوي
a == b
وعدم التساويa != b
- معاملات المقارنة
a < b
وa > b
وa <= b
وa >= b
- تمثيل المجالات والتحقق منها مثل
a..b و
x in a..b و
x !in a..b
إذا كان كلٌّ من المتحولين على طرفي المعامل (a
وb
) من نوع Float
أو Double
أو ما يقابلهما من نوع nullable (يُصرَّح عن النوع أو يُخمَّن كنتيجةٍ عن التحويل [[[Kotlin/typecasts|smart cast]]]) فإن العمليات على الأعداد والمجالات تكون وفقًا للمقاييس IEEE 754 في العمليات الحسابيّة للأعداد ذات الفاصلة العائمة.
أمّا لدعم حالات الاستخدام المُعمّمة (generic use cases) وللتزويد بتنظيمٍ إجمالي عندما لا تكون المتحولات على طرفيّ المُعامِل معرَّفةً كأعدادٍ بفاصلةٍ عائمة (مثل Any
أو Comparable<...>
أو متحول نوع [type parameter]) فإنّ العمليات تعتمد على الدوال equals
و compareTo
للنوعين Float
و Double
، وهذا لا يتوافق مع المعايير القياسيّة، وبالتالي:
- يعد
NaN
مساويًا لنفسه - يعد
NaN
أكبر من أي عنصر آخر يتضمنPOSITIVE_INFINITY
- 0.0- (بقيمة سالبة) أصغر من 0.0 (بالقيمة الموجبة)
المحارف (Characters)
إنّ النوع الذي تُخزَّن به المحارف هو Char
ولا يُمكن التعامل معها مباشرةً كما هو الحال في البيانات العدديّة، مثلما في الشيفرة الآتية:
fun check(c: Char) {
if (c == 1) { // الخطأ هنا بعدم التوافق ما بين النوعين
// ...
}
}
يُحاط المحرف دائمًا بإشارتيّ تنصيصٍ أحاديتين مثل القيمة '1'، وتصبح لبعض المحارف دلالةٌ خاصّةٌ إذا سُبِقت بالرمز \ وذلك بهدف جعلها من بين المحارف escape sequence إذ تدعم لغة Kotlin المحارف الخاصّة: \t
و \b
و \n
و \r
و \'
و \"
و \\
و \$
، أمّا للتعبير عن شيفرة Unicode لأي محرفٍ آخر يستخدم المحرف \u مثل: '\uFF00'
.
ويُمكن تحويل المحرف إلى قيمةٍ عدديّةٍ صحيحةٍ Int
بشكلٍ صريحٍ، كما هو موضَّح في الشيفرة الآتية:
fun decimalDigitValue(c: Char): Int {
if (c !in '0'..'9')
throw IllegalArgumentException("Out of range")
return c.toInt() - '0'.toInt() // تحويل صريح للقيمة العددية
}
و قد تُغلَّف المحارف (boxed character) -كما هو الحال في القيم العددية- عندما يتطلَّب الأمر وجودَ مرجعيّة nullable، حيث لا تحافظ عملية التغليف هذه على الماهيّة (identity).
القيم المنطقيّة (Booleans)
يُعبِّر نوع البيانات Boolean
عن البيانات المنطقية التي لها إحدى القيمتين: true
أو false
.
ويًمكن تغليف القيمة المنطقية (boxed boolean) عند الحاجة لمرجعيّة nullable.
العمليات الأساسيّة المتاحة للبيانات الثنائية:
- عملية الفصل ||
- عملية الوصل &&
- عملية النفي !
المصفوفات (Arrays)
تتمثّل أيّ مصفوفةٍ في لغة Kotlin من خلال صنف المصفوفات Array
والذي يحتوي على دالتيّ get
و set
(واللتان يُستعاض عنهما بالقوسين [ ]
عبر التحميل الزائد للمعامل [operator overloading]) إضافةً إلى خاصيّة (property) طول المصفوفة size
بالإضافة إلى العديد من الدوال الأخرى مثل:
class Array<T> private constructor() {
val size: Int
operator fun get(index: Int): T
operator fun set(index: Int, value: T): Unit
operator fun iterator(): Iterator<T>
// ...
}
وتُستخدم الدالة arrayOf()
لإنشاء مصفوفةٍ جديدةٍ إذا تُمرَّرُ قيم عناصرها كمتحولات (arguments) لهذه الدالة، فمثلًا؛ لإنشاء المصفوفة [3, 2, 1] يكون استدعاء الدالة بالشكل: arrayOf(1, 2, 3)
، أما لإنشاء مصفوفةٍ فارغةٍ (قيم عناصرها null) بطولٍ محدَّدٍ (size) تُستخدَم الدالة arrayOfNulls()
.
أما الطريقة الثانية لإنشاء مصفوفةٍ فهي بالاعتماد على الباني (constructor) الموجود في الصنف Array
والذي يتطلَّب حجم المصفوفة والدالة التي تعيد القيمة الابتدائية لكل عنصر موجود في المصفوفة من خلال دليله (index)، مثل:
val asc = Array(5, { i -> (i * i).toString() })
حيث تُنشِئ هذه الشيفرة مصفوفةً من السلاسل النصيّة: ["16", "9", "4", "1", "0"]، وكما ذكرنا سابقًا فإن استخدام القوسين [ ]
يكون فقط لاستدعاء الدالتين get()
وset()
.
ويُلاحَظ بأنه على عكس لغة Java تكون المصفوفات في لغة Kotlin ثابتةً (invariant) وهذا يعني أنّه من غير المسموح إسناد Array<String>
إلى Array<Any>
، وهذا بدوره يمنع حدوث أي خلل (failure) أثناء التنفيذ، ولكن يمكن استخدام Array<out Any>
(لمزيدٍ من التفاصيل راجع Type Projection).
وقد خصّصت لغة Kotlin بعض الأصناف لتمثيل المصفوفات ذات الأنواع الأساسيّة (primitive) دون الحاجة إلى التغليف (boxing)، مثل: ByteArray
و ShortArray
و IntArray
وهكذا. ولا توجد أي علاقة وراثة (inheritance) لهذه الأصناف من الصنف Array
ولكنها تحتوي على مجموعة التوابع (methods) والخصائص (properties) نفسها، ولكلِّ منها أيضًا دالة منتِجة (factory function)، مثال عن استخدام المصفوفات:
val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]
السلاسل النصية (Strings)
يعبِّر النوع String
عن السلاسل النصيّة وهي ثابتة (immutable) تحتوي على محارف يُمكن الوصول إليها عبر الدليل (index) مثل: s[i]
، كما ويمكن استخدام حلقة for
مع المحارف في السلسلة بالشكل الآتي:
for (c in str) {
println(c)
}
قيم السلاسل النصية (Literals)
لها نوعان: السلاسل النصيّة الخامّ والتي قد تحتوي على الانتقال لأسطر جديدةٍ، والسلاسل النصيّة ذات المحارف escape والتي تتشابه لحدٍ كبيرٍ مع السلاسل النصيّة في لغة Java، مثل:
val s = "Hello, world!\n"
إذا تمّ الانتقال للسطر التالي عبر المحرف "n\"
ولمعرفة كافة تلك المحارف يمكن الرجوع إلى فقرة Character بنفس الصفحة الحاليّة.
أما السلاسل النصيّة الخامّ فتُحاط بإشارات تنصيصٍ ثلاثيّة """
ولا تحتوي على أي محارف escape و لكن من الممكن أن تتضمن الانتقال لسطرٍ جديدٍ مثل:
val text = """
for (c in "foo")
print(c)
"""
ومن السهل إزالة الفراغات المساعدة (بالجهة اليساريّة من كل سطر جديد) عبر استدعاء الدالة trimMargin()
مثل:
val text = """
|Tell me and I forget.
|Teach me and I remember.
|Involve me and I learn.
|(Benjamin Franklin)
""".trimMargin()
إذ يستخدم الرمز |
بالحالة الافتراضيّة كبادئ محاذاةٍ، ويمكن اختيار أي محرفٍ آخر وتمريره كمتحوّلٍ للدالة (parameter) مثل: trimMargin(">")
.
قوالب السلاسل النصية (String Templates)
قد تحتوي السلاسل النصيّة أحيانًا على بعض القوالب المخصصة للتعابير (وهي أجزاء من الشيفرة ينتج عنها قيمة تُضاف للسلسلة النصيّة) إذ تبدأ بالرمز $
وتتألف من:
- إما اسم بسيط مثل:
val i = 10
val s = "i = $i" // ستحسب بالشكل "i = 10"
- أو تعبير واقع ما بين قوسين
{ }
مثل:
val s = "abc"
val str = "$s.length is ${s.length}" // تُحسب بالشكل "abc.length is 3"
إذ تُستخدَم هذه القوالب في السلاسل النصيّة بنوعيها، ولدى الحاجة إلى المحرف $
كقيمةٍ مباشرةٍ في السلسلة الخامّ (لا للدلالة على قالب تعبيريّ) فيمكن اعتماد الصيغة الآتية:
val price = """
${'$'}9.99
"""