أنواع البيانات (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

إذا كان كلٌّ من المعاملين (operands) على طرفي المعامل (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
"""

مصادر