التحميل الزائد للمعاملات (Operator Overloading) في لغة Kotlin

من موسوعة حسوب

تُتيح لغة Kotlin إجراءَ مجموعةٍ مُعرَّفة مسبقًا من العمليات على أنواع البيانات المختلفة وذلك باستخدام رموزٍ ثابتةٍ تعتمدها لغة Kotlin مثل الرمز + أو الرمز * وتختلف فيما بينها بالأولويّة (precedence)، حيث توجد دالةٌ (إما دالة من الصنف [member] أو دالة إضافيّة [extension]) باسمٍ ثابتٍ لكلّ معاملٍ مُعرَّف بحسب النوع (نوعٌ يساريٌّ للعمليات الثنائيّة [binary operations] ونوع وسائط [argument type] للعمليات الأحاديّة [unary operations])، ويجب تحديد الدوال التي تحتوي على تحميلٍ زائدٍ للمعاملات بالمُحدَّد operator، تناقش الصفحة الاصطلاحات (conventions) التي تنظِّم التحميل الزائد لمختلف المعاملات.

العمليات الأحادية (Unary Operations)

المعاملات الأحادية البادئة (Unary Prefix Operators)

التعبير يُترجَم إلى
a+ a.unaryPlus()‎
a- a.unaryMinus()‎
a! a.not()‎

يقوم المُترجِم (compiler) بالخطوات الآتية عند إجراء إحدى العمليات السابقة ولتكن a+ :

  • تحديد نوع المتغيِّر a وليكن T
  • البحث عن الدالة unaryPlus()‎ المسبوقة بالمُحدِّد operator والتي لا تحتوي أيّة معاملات للمستقبِل T (إما دالة من الصنف [member] أو دالة إضافيّة [extension]) 
  • إن لم تكن الدالة موجودةً أو كانت غامضةً فسينتُج خطأٌ بالترجمة (compilation error)
  • إذا وُجدت الدالة وكان نوعها المُعاد R فسيكون للتعبير a+ النوع R أيضًا

وتُتاح هذه العمليات (بالإضافة لعملياتٍ أخرى) للأنواع الأساسيّة في لغة Kotlin ولا تتطلَّب استدعاء الدوال، فيمكن مثلًا استخدام التحميل الزائد للمعامل الأحاديّ - بالشكل:

data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus() = Point(-x, -y)

val point = Point(10, 20)
println(-point)  // "(-10, -20)" سيظهر

معاملات الزيادة والنقصان

التعبير يُترجَم إلى
++a a.inc()‎
--a a.dec()‎

إذ ستُسنَد القيمة التي تعيدها الدالة inc()‎ أو dec()‎ إلى المتغيِّر الذي يُلحَق به المعامل ++ أو -- دون أن تغيّر الدالةُ الكائنَ الذي استُدعيت من خلاله. يقوم المترجم بالخطوات الآتية لتنفيذ العملية اللاحقة (postfix) ولتكن ++a مثلًا:

  • تحديد نوع المتغيِّرa وليكن النوع T
  • البحث عن الدالة inc()‎ والمُعرَّفة بالمُحدِّد operator وتكون بدون معاملات وقابلةً للتطبيق على النوع T
  • التحقُّق من أنّ النوع المُعاد من هذه الدالة هو نوعٌ فرعيٌّ للنوع T

وينتُج عن حساب التعبير:

  • تخزين القيمة الابتدائيّة (initial value) للمتغيِّر a في متغيِّر مؤقت a0
  • إسناد قيمة نتيجة الدالة inc()‎ إلى المتغيِّر السابق a
  • إعادة المتغيِّر a0 كنتيجةٍ للتعبير

وتماثل خطوات تنفيذ العملية --a الخطوات السابقة.

أمّا معاملات الزيادة والنقصان بصيغتها البادئة (prefix) بالشكل: a++ و a-- فهي تعمل بالطريقة السابقة ذاتها وتكون النتيجة:

  • إسناد نتيجة الدالة a.inc()‎ إلى المتغيِّر a
  • إعادة القيمة الجديدة للمتغيِّر a كنتيجةٍ للتعبير

العمليات الثنائيّة (Binary Operations)

المعاملات الحسابيّة (Arithmetic Operators)

التعبير يُترجم إلى
a + b a.plus(b)‎
a - b a.minus(b)‎
a * b a.times(b)‎
a / b a.div(b)‎
a % b a.rem(b)‎ و a.mod(b)‎ (مُستبعدة)
a..b a.rangeTo(b)‎

تدعم Kotlin المعامل rem بدءًا من الإصدار Kotlin 1.1 ليحل محلّ mod في الإصدار Kotlin 1.0 (مُستبعد في الإصدار الحاليّ).

مثال

يبدأ الصنف Counter الآتي بقيمةٍ ابتدائيّةٍ من الممكن زيادتها باستخدام التحميل الزائد للمعامل + بالشكل:

data class Counter(val dayIndex: Int) {
    operator fun plus(increment: Int): Counter {
        return Counter(dayIndex + increment)
    }
}

المعامل in

التعبير يُترجَم إلى
a in b b.contains(a)‎
a !in b ‎!b.contains(a)‎

وتكون الإجراءات نفسها في المعاملين حيث يُعكَس ترتيب الوسائط (arguments) فقط.

معاملات الوصول المفهرس (Indexed Access Operators)

التعبير يُترجَم إلى
a[i]‎ a.get(i)‎
a[i,j]‎ a.get(i, j)‎
a[i_1, ..., i_n]‎ (a.get(i_1, ..., i_n
a[i] = b a.set(i, b)‎
a[i, j] = b a.set(i, j, b)‎
a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)‎

إذ تُترجَم الأقواس [] إلى استدعاء الدالة get أو set بما يتناسب مع عدد الوسائط.

معاملات الاستدعاء (Invoke Operators)

التعبير يُترجَم إلى
a()‎ a.invoke()‎
a(i)‎ a.invoke(i)‎
a(i,j)‎ a.invoke(i,j)‎
a(i_1, ..., i_n)‎ a.invoke(i_1, ..., i_n)‎

إذ تُترجَم الأقواس () إلى استدعاءات للدالة invoke بالعدد المناسب من الوسائط.

عمليات الإسناد المُحسَّنة (Augmented Assignments)

التعبير يُترجَم إلى
a += b a.plusِAssign(b)‎
a -= b a.minusAssign(b)‎
a *= b a.timesAssign(b)‎
a /= b a.divAssign(b)‎
a %= b a.remAssign(b)‎ و a.modAssign(b)‎ (مُستبعدة)

إذ يقوم المُترجِم (compiler) بالخطوات الآتية لعمليات الإسناد، ولتكن العملية a += b مثلًا:

  • إن كانت الدالة متاحةً:
    • إن كانت الدالة الثنائيّة الموافقة لها (مثل الدالة plus()‎ بالنسبة للدالة plusAssign()‎) متاحةً أيضًا فيجب الإعلام بخطأ الالتباس (ambiguity)
    • التأكّد من أنّ النوع المُعاد هو Unit والإعلام بالخطأ خلاف ذلك
    • توليد الشيفرة المناسبة للدالة a.plusِAssign(b)
  • إن لم تكن الدالة متاحةً: يحاول توليد الشيفرة a = a + b وهذا يتضمَّن التحقُّق من النوع؛ أي أن يكون نوع التعبير a + b نوعًا فرعيًا (subtype) من a

ولا تُعدُّ عمليات الإسنادا تعابيرًا (expressions) في Kotlin.

معاملات المساواة (Equality) والاختلاف (Inequality)

التعبير يُترجَم إلى
a == b a?.equals(b) ?: (b === null)‎‎
a != b ‎!(a?.equals(b) ?: (b === null))‎

ويكون معاملا التحقُّق من المرجعيّة (=== و ==!) غير قابلين للتحميل الزائد (not overloadable) وبالتالي ما من استخدامٍ اصطلاحيٍّ لهما.

ويتميز المعامل == بأنه يُترجَم إلى تعبيرٍ معقَّدٍ يُضمِر القيمة null أي تكون نتيجة العملية null == null صحيحةً دائمًا، وتكون قيمة x == null خاطئةً دائمًا إن كان المتغيِّر x متغيِّرًا nullable (أي لا يقبل القيمة الفارغة null) ولن تُستدعَى بذلك الدالة x.equals()‎.

معاملات المقارنة (Comparison Operators)

التعبير يُترجم إلى
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

إذ تُترجَم كافّة عمليات المقارنة إلى استدعاءاتٍ للدالة compareTo التي تعيد النوع Int.

معاملات تعميم الخاصّيّات (Property Delegation Operators)

وهي الدوال provideDelegate و getValue و setValue ولمزيدٍ من المعلومات راجع الخاصّيّات المُعمَّمة (delegated properties).

استدعاء الدوال المسماة بالصيغة الضمنيّة (Infix)

تمكن محاكاة العمليّات بالصيغة الضمنيّة (infix) بالاعتماد على استدعاءات الدوال الضمنيّة.

مصادر