الأصناف (Classes) والوراثة (Inheritance)

من موسوعة حسوب
مراجعة 04:52، 10 مارس 2018 بواسطة Nourtam (نقاش | مساهمات) (إضافة فقرات تحت عنوان الوراثة)

الأصناف (Classes)

تُستخدم الكلمة المفتاحيّة class للتصريح (declaration) عن الصنف بالصيغة الآتية: (اسم الصنف Invoice)

class Invoice {
}

ويحتوي التصريح على اسم الصنف (class name) وترويسة الصنف (class header) (والتي تُحدِّد متحولات النوع والباني الأساسيّ و.. إلخ.) وبُنية الصنف (class body) محاطةً بالقوسين {}، وإن كلًا من ترويسة الصنف وبُنيته اختياريتان؛ فإذا كان الصنف خاليًا لا حاجة للأقواس، مثل:

class Empty

الباني (Constructor)

يوجد لكلّ صنف في لغة Kotlin بانٍ رئيسيّ (primary) واحدٌ وبانٍ -أو أكثر- ثانويّ (secondary)، إذ يُعدُّ الباني الرئيسيّ جزءًا من ترويسة الصنف (header) حيث يُضاف بعد اسم الصنف مباشرةً (وقد تُضاف له المتحولات (parameters))، مثل:

class Person constructor(firstName: String) {
}

وإن لم يكن للباني الرئيسيّ حاشيةٌ (annotation) أو مُحدِّد وصول (visibility modifier) فتُحذَف حينئذِ الكلمة المفتاحيّة constructor مثل:

class Person(firstName: String) {
}

وكما يُلاحظ أن الباني الرئيسيّ لا يحتوي على أيّة شيفرةٍ لأنّ شيفرة التهيئة الأولية (initialization code) تُكتب في أجزاء خاصّةٍ للتهيئة (initializer blocks) والتي تُسبَق بالكلمة المفتاحيّة init، إذ تُنفَّذُ هذه الأجزاء -عند عملية الإنشاء من هذا الصنف- بنفس الترتيب الموجودة فيه ضمن الصنف متداخلةً مع عمليات تهيئة الخاصّيّات (property initializers)، مثل:

class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)

    init {
        println("First initializer block that prints ${name}")
    }

    val secondProperty = "Second property: ${name.length}".also(::println)

    init {
        println("Second initializer block that prints ${name.length}")
    }
}

ويٌسمَح باستخدام متحوِّلات (parameters) الباني الأساسيّ في أجزاء التهيئة كما يُمكِن استخدامها أيضًا في تهيئة الخاصّيّات (properties) المُصرَّح عنها في بُنية الصنف (class body)، مثل:

class Customer(name: String) {
    val customerKey = name.toUpperCase()
}

وتدعم Kotlin صيغةً مختصرةً للتصريح عن الخاصّيّات وتهيئتها الأولية في الباني الأساسيّ، وهي:

class Person(val firstName: String, val lastName: String, var age: Int) {
    // ...
}

وكما هو الحال في الخاصّيّات بشكلٍ عامّ، قد تُعرَّف الخاصّيّات في الباني الأساسيّ كخاصيّات متغيّرة (mutable)‏ var أو خاصّيات للقراءة فقط (read-only)‏ val. وفي حال وجود حاشيةٍ (annotation) أو مُحدِّد وصولٍ (visibility modifier) للباني فلا بُدَّ من وجود الكلمة المفتاحيّة constructor إذ تُوضع الحاشية أو المُحدِّدات قبلها بالصيغة:

class Customer public @Inject constructor(name: String) { ... }

المزيد عن مُحدِّدات الوصول (visibility modifiers).

الباني الثانويّ (Secondary Constructors)

يُصرَّح عن الباني الثانويّ في الصنف (class) بالبدء به عبر الكلمة المفتاحيّة constructor، مثل:

class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

فإذا احتوى الصنف بانيًا رئيسيًا (primary constructor) فإن كلَّ بانٍ ثانويّ سيُفضي في النهاية إليه إما مباشرةً أو بطريقةٍ غير مباشرة عبر بانٍ ثانويّ آخر، وبكلا الحالتين يكون الوصول للباني الرئيسيّ من خلال الكلمة المفتاحيّة this، مثل:

class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

ويُلاحظ بأن الشيفرة في أجزاء التهيئة (initializer blocks) تصبح جزءًا من الباني الأساسيّ حيث يكون الانتقال للباني الرئيسيّ بمثابة التعليمة الأولى في الباني الثانويّ، وبالتالي فإن كلّ الشيفرات الموجودة في أجزاء التهيئة ستُنفَّذ قبل الباني الثانويّ، حتى وإن لم يكن هناك بانٍ رئيسيّ للصنف فإن هذا الانتقال سيكون ضمنيًا (implicit) وستُنفَّذ أجزاء التهيئة أيضًا، مثل:

class Constructors {
    init {
        println("Init block")
    }

    constructor(i: Int) {
        println("Constructor")
    }
}

أما إذا لم يحتوِ الصنف (غير المُجرَّد non-abstract) على أي تصريحٍ لبانٍ (رئيسيّ أو ثانويّ) فسيُولَّد تلقائيًا بانٍ افتراضيّ بدون متحولات (arguments)، ويكون مُحدِّد الوصول له من النوع العامّ (public)، فإذا أردت ألا يكون عامًّا فعليك بإنشاء بانٍ رئيسيٍّ فارغٍ وبالمُحدِّد المناسب، مثل:

class DontCreateMe private constructor () {
}

ويُلاحظ في بيئة JVM أنّه إن كان لكل متحوِّلات (parameters) الباني قيمٌ افتراضيّةٌ فإن المُترجم (compiler) سيُولِّد بانيًا إضافيًا بدون متحولات (parameterless)، والذي سيستخدم القيم الافتراضيّة، وهذا ما يجعل لغة Kotlin سهلة الاستخدام مع المكتبات (libraries) مثل Jackson أو JPA اللتان تنشِئان الكائنات باستخدام البواني بدون المتحولات. مثل:

class Customer(val customerName: String = "")

إنشاء كائنات (Instances) من الصنف

لإنشاء كائنٍ من أحد الأصناف يُستدَعى الباني (constructor) فيه كما لو أنّه دالةٌ عاديّة، مثل:

val invoice = Invoice()

val customer = Customer("Joe Smith")

إذ لا تعتمد لغة Kotlin على وجود الكلمة المفتاحيّة new كما هو الحال في لغة Java.

أمّا لإنشاء الكائنات من الأصناف المتداخلة (nested) أو الأصناف الداخليّة (inner) أو الداخليّة المجهولة (anonymous inner) راجع الأصناف المتداخلة.

عناصر الصنف (Class Members)

قد يحتوي الصنف على:

الوراثة (Inheritance)

تشترك كل الأصناف في لغة Kotlin بالصنف Any باعتباره الصنفَ الأعلى (superclass) الافتراضيّ لأي صنفٍ لم يُصرَّح عن صنفٍ أعلى له، مثل:

class Example // وراثة ضمنيّة من الصنف Any

ملاحظة: إن الصنف Any ليس من java.lang.Object;‎ إذ لا يحتوي إلا على الدوال الثلاثة: equals()‎ و hashCode()‎ و toString()‎. وللتصريح عن الصنف الأعلى (superclass) بشكلٍ واضحٍ يُوضَع النوع بعد الرمز : في ترويسة الصنف (class header)، مثل:

open class Base(p: Int)

class Derived(p: Int) : Base(p)

تعبِّر الكلمة المفتاحيّة open (قبل اسم الصنف) عن المعنى المعاكس للكلمة المفتاحية final في لغة Java، إذ يُسمَح (بوجود open) بعملية الوراثة من هذا الصنف لأنّ الحالة الافتراضيّة للأصناف في لغة Kotlin هي final. وإذا احتوى الصنف المُشتق (derived) على بانٍ أساسيٍّ فتجب تهيئة الصنف الأساسي (base) هناك بالاعتماد على متحوِّلات (parametera) هذا الباني، أما إن لم يكن للصنف بانٍ رئيسيّ فإن كلَّ بانٍ ثانويّ يجب أن يُهيِئ النوعَ الأساسيّ (base type) باستخدام الكلمة المفتاحيّة super أو أن ينقل تلك المهمة لبانٍ آخر غيره، وبالتالي فإنه من الممكن أن تستدعي عدة بوانٍ ثانويّة العديدَ من بواني النوع الأساسيّ، مثل:

class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

إعادة تعريف التوابع (Overriding Methods)

تتطلَّب لغة Kotlin -على عكس لغة Java- التصريح بشكلٍ واضحٍ عن إمكانيّة إعادة تعريف العنصر من خلال إضافة الكلمة المفتاحيّة open قبله وبإضافة الكلمة المفتاحيّة override عند إعادة تعريفه، مثل:

open class Base {
    open fun v() {}
    fun nv() {}
}
class Derived() : Base() {
    override fun v() {}
}

إذ من الضرور يّ إضافة override إلى الدالة Derived.v()‎، وإذا لم تُضف سينجم خطأ في الترجمة (compilation)، أمّا إن لم تٌضف الكلمة المفتاحيّة open الموجودة عند الدالة Base.nv()‎ فلا يُسمَح بإعادة تعريفها سواءً باستخدام override أو بدونها، لأنّ الوصول إلى العناصر (members) الموجودة في الأصناف من نوع final (بدون وجود open) غير مسموحٍ به، ويُعدُّ العنصر المسبوق بالكلمة المفتاحيّة override من نوع open بالحالة الافتراضيّة (أي من الممكن إعادةُ تعريفه أيضًا في الأصناف الفرعيّة (subclasses))، ولمنع ذلك تجب إضافة الكلمة المفتاحيّة final كما في الشيفرة:

open class AnotherDerived() : Base() {
    final override fun v() {}
}

إعادة تعريف الخاصّيّات (Overriding Properties)

تماثل إعادة تعريف الخاصّيّات طريقة إعادة تعريف التوابع (methods) في الفقرة السابقة؛ إذ لا بُدّ من وجود الكلمة المفاحيّة override قبل الخاصيّات التي يُعاد تعريفها في الصنف المُشتقّ (derived) والموجودة بالأصل في الصنف الأعلى (superclass) بشرط أن تكون الخاصّيّتان متوافقتَين (compitable)، ويُعاد التعريف إمّا باستخدام التهيئة (initializer) أو من خلال تابع الوصول get()‎، مثل:

open class Foo {
    open val x: Int get() { ... }
}

class Bar1 : Foo() {
    override val x: Int = ...
}

ويُسمح بإعادة تعريف الخاصّيّة من النوع val بخاصّيّةٍ من النوع var لكن تُمنَع الحالة العكسية؛ وذلك لأنّ الخاصيّة من النوع val تٌصرِّح بالأصل عن تابع getter وتعيد تعريفه كنوع var وكذلك تُصرِّح عن تابع setter في الصنف المُشتقّ(derived). وقد تُستخدَم الكلمة المفتاحيّة override كجزءٍ من تعريف الخاصّيّة في الباني الأساسيّ (primary constructor)، مثل:

interface Foo {
    val count: Int
}

class Bar1(override val count: Int) : Foo

class Bar2 : Foo {
    override var count: Int = 0
}

ترتيب تهيئة الصنف المشتق

استدعاء إعادة تعريف الاستخدام من الصنف الأعلى

إعادة تعريف القواعد

الأصناف المُجرَّدة (Abstract Classes)

قد يُصرَّح عن الصنف أو بعضٍ من عناصره (members) بالكلمة المفتاحيّة abstract (مُجرَّد) بحيث تكون هذه العناصر دون تعريفٍ للاستخدام (implementation) ضمن الصنف الموجودة فيه، ولا حاجة لإضافة الكلمة المفتاحيّة open للصنف أو العناصر المجرَّدة فهي موجودةٌ بالحالة الافتراضيّة.

وبالإمكان إعادة تعريف (override) العنصر المفتوح غير المُجرَّد في عنصرٍ آخر مُجرَّد، مثل:

open class Base {
    open fun f() {}
}

abstract class Derived : Base() {
    override abstract fun f()
}

الكائنات المرافقة (Companion Objects)

لا وجود للتوابع الستاتيكيّة (static methods) في Kotlin -على عكس لغتيّ البرمجة Java وC#‎- ويُستعاض عنها بالدوال على مستوى الحزمة (package-level functions).

وعند الحاجة إلى كتابة دالةٍ قابلةٍ للاستدعاء دون إنشاء كائنٍ (instance) من الصنف ولكن لها صلاحيّة الوصول (access) إلى ما داخل الصنف (للتابع المُنتِج (factory method) مثلًا) فيُمكن جعلها كعنصرٍ (member) من تصريح الكائن نفسه داخل ذلك الصنف.

وبشكل أكثر تفصيلًا؛ إذا صُرِّح عن الكائن المرافق داخل الصنف فيمكن حينئذٍ استدعاء عناصره بنفس صيغة استدعاء التوابع الستاتيكيّة في لغتيّ Java وC#‎ بالاعتماد على اسم الصنف فقط.

مصادر