الفرق بين المراجعتين لصفحة: «Kotlin/classes»
أنشأ الصفحة ب'<noinclude>{{DISPLAYTITLE:الأصناف (Classes) والوراثة (Inheritance)}}</noinclude> == الأصناف (Classes) == تُستخدم الكلمة المفتاح...' |
ط تعديل مصطلح متحول |
||
(9 مراجعات متوسطة بواسطة مستخدمين اثنين آخرين غير معروضة) | |||
سطر 1: | سطر 1: | ||
<noinclude>{{DISPLAYTITLE:الأصناف (Classes) والوراثة (Inheritance)}}</noinclude> | <noinclude>{{DISPLAYTITLE:الأصناف (Classes) والوراثة (Inheritance) في لغة Kotlin}}</noinclude> | ||
تُستخدم الكلمة المفتاحيّة <code>class</code> للتصريح (declaration) عن الصنف بالصيغة الآتية (اسم الصنف <code>Invoice</code>):<syntaxhighlight lang="kotlin"> | |||
تُستخدم الكلمة المفتاحيّة <code>class</code> للتصريح (declaration) عن الصنف بالصيغة الآتية | |||
class Invoice { | class Invoice { | ||
} | } | ||
</syntaxhighlight>ويحتوي التصريح على اسم الصنف (class name) وترويسة الصنف (class header) (والتي تُحدِّد | </syntaxhighlight>ويحتوي التصريح على اسم الصنف (class name) وترويسة الصنف (class header) (والتي تُحدِّد معاملات النوع والباني الأساسيّ ...إلخ.) وبُنية الصنف (class body) محاطةً بالقوسين <code>{}</code>، وإن كلًا من ترويسة الصنف وبُنيته اختياريتان؛ فإذا كان الصنف خاليًا لا حاجة للأقواس، مثل:<syntaxhighlight lang="kotlin"> | ||
class Empty | class Empty | ||
</syntaxhighlight> | </syntaxhighlight> | ||
== الباني (Constructor) == | |||
يوجد لكلّ صنف في لغة Kotlin بانٍ رئيسيّ (primary) واحدٌ وبانٍ -أو أكثر- ثانويّ (secondary)، إذ يُعدُّ الباني الرئيسيّ جزءًا من ترويسة الصنف (header) حيث يُضاف بعد اسم الصنف مباشرةً (وقد تُضاف له | يوجد لكلّ صنف في لغة Kotlin بانٍ رئيسيّ (primary) واحدٌ وبانٍ -أو أكثر- ثانويّ (secondary)، إذ يُعدُّ الباني الرئيسيّ جزءًا من ترويسة الصنف (header) حيث يُضاف بعد اسم الصنف مباشرةً (وقد تُضاف له المعاملات [parameters])، مثل:<syntaxhighlight lang="kotlin"> | ||
class Person constructor(firstName: String) { | class Person constructor(firstName: String) { | ||
} | } | ||
</syntaxhighlight>وإن لم يكن للباني الرئيسيّ | </syntaxhighlight>وإن لم يكن للباني الرئيسيّ توصيفٌ (annotation) أو مُحدِّد وصول (visibility modifier) فتُحذَف حينئذِ الكلمة المفتاحيّة <code>constructor</code> مثل:<syntaxhighlight lang="kotlin"> | ||
class Person(firstName: String) { | class Person(firstName: String) { | ||
} | } | ||
سطر 29: | سطر 28: | ||
} | } | ||
} | } | ||
</syntaxhighlight>ويٌسمَح باستخدام | </syntaxhighlight>ويٌسمَح باستخدام معاملات (parameters) الباني الأساسيّ في أجزاء التهيئة كما يُمكِن استخدامها أيضًا في تهيئة الخاصّيّات (properties) المُصرَّح عنها في بُنية الصنف (class body)، مثل:<syntaxhighlight lang="kotlin"> | ||
class Customer(name: String) { | class Customer(name: String) { | ||
val customerKey = name.toUpperCase() | val customerKey = name.toUpperCase() | ||
سطر 39: | سطر 38: | ||
</syntaxhighlight>وكما هو الحال في الخاصّيّات بشكلٍ عامّ، قد تُعرَّف الخاصّيّات في الباني الأساسيّ كخاصيّات متغيّرة (mutable) <code>var</code> أو خاصّيات للقراءة فقط (read-only) <code>val</code>. | </syntaxhighlight>وكما هو الحال في الخاصّيّات بشكلٍ عامّ، قد تُعرَّف الخاصّيّات في الباني الأساسيّ كخاصيّات متغيّرة (mutable) <code>var</code> أو خاصّيات للقراءة فقط (read-only) <code>val</code>. | ||
وفي حال وجود | وفي حال وجود توصيفٍ (annotation) أو مُحدِّد وصولٍ (visibility modifier) للباني فلا بُدَّ من وجود الكلمة المفتاحيّة <code>constructor</code> إذ يُوضع التوصيف أو المُحدِّدات قبلها بالصيغة:<syntaxhighlight lang="kotlin"> | ||
class Customer public @Inject constructor(name: String) { ... } | class Customer public @Inject constructor(name: String) { ... } | ||
</syntaxhighlight>المزيد عن [[Kotlin/visibility modifiers|مُحدِّدات الوصول (visibility modifiers)]]. | </syntaxhighlight>المزيد عن [[Kotlin/visibility modifiers|مُحدِّدات الوصول (visibility modifiers)]]. | ||
==== الباني | == الباني الثانويّ (Secondary Constructors) == | ||
يُصرَّح عن الباني الثانويّ في الصنف (class) بالبدء به عبر الكلمة المفتاحيّة <code>constructor</code>، مثل:<syntaxhighlight lang="kotlin"> | |||
class Person { | |||
constructor(parent: Person) { | |||
parent.children.add(this) | |||
} | |||
} | |||
</syntaxhighlight>فإذا احتوى الصنف بانيًا رئيسيًا (primary constructor) فإن كلَّ بانٍ ثانويّ سيُفضي في النهاية إليه إما مباشرةً أو بطريقةٍ غير مباشرة عبر بانٍ ثانويّ آخر، وبكلا الحالتين يكون الوصول للباني الرئيسيّ من خلال الكلمة المفتاحيّة <code>this</code>، مثل:<syntaxhighlight lang="kotlin"> | |||
class Person(val name: String) { | |||
constructor(name: String, parent: Person) : this(name) { | |||
parent.children.add(this) | |||
} | |||
} | |||
</syntaxhighlight>ويُلاحظ بأن الشيفرة في أجزاء التهيئة (initializer blocks) تصبح جزءًا من الباني الأساسيّ حيث يكون الانتقال للباني الرئيسيّ بمثابة التعليمة الأولى في الباني الثانويّ، وبالتالي فإن كلّ الشيفرات الموجودة في أجزاء التهيئة ستُنفَّذ قبل الباني الثانويّ، حتى وإن لم يكن هناك بانٍ رئيسيّ للصنف فإن هذا الانتقال سيكون ضمنيًا (implicit) وستُنفَّذ أجزاء التهيئة أيضًا، مثل:<syntaxhighlight lang="kotlin"> | |||
class Constructors { | |||
init { | |||
println("Init block") | |||
} | |||
constructor(i: Int) { | |||
println("Constructor") | |||
} | |||
} | |||
</syntaxhighlight>أما إذا لم يحتوِ الصنف (غير المُجرَّد non-abstract) على أي تصريحٍ لبانٍ (رئيسيّ أو ثانويّ) فسيُولَّد تلقائيًا بانٍ افتراضيّ بدون وسائط (arguments)، ويكون مُحدِّد الوصول له من النوع العامّ (public)، فإذا أردت ألا يكون عامًّا فعليك بإنشاء بانٍ رئيسيٍّ فارغٍ وبالمُحدِّد المناسب، مثل:<syntaxhighlight lang="kotlin"> | |||
class DontCreateMe private constructor () { | |||
} | |||
</syntaxhighlight>ويُلاحظ في بيئة JVM أنّه إن كان لكل معاملات (parameters) الباني قيمٌ افتراضيّةٌ فإن المُترجم (compiler) سيُولِّد بانيًا إضافيًا بدون معاملات (parameterless)، والذي سيستخدم القيم الافتراضيّة، وهذا ما يجعل لغة Kotlin سهلة الاستخدام مع المكتبات (libraries) مثل Jackson أو JPA اللتان تنشِئان الكائنات باستخدام البواني بدون المعاملات. مثل:<syntaxhighlight lang="kotlin"> | |||
class Customer(val customerName: String = "") | |||
</syntaxhighlight> | |||
=== إنشاء كائنات (Instances) من الصنف === | |||
لإنشاء كائنٍ من أحد الأصناف يُستدَعى الباني (constructor) فيه كما لو أنّه دالةٌ عاديّة، مثل:<syntaxhighlight lang="kotlin"> | |||
val invoice = Invoice() | |||
val customer = Customer("Joe Smith") | |||
</syntaxhighlight>إذ لا تعتمد لغة Kotlin على وجود الكلمة المفتاحيّة <code>new</code> كما هو الحال في لغة Java. | |||
أمّا لإنشاء الكائنات من الأصناف المتداخلة (nested) أو الأصناف الداخليّة (inner) أو الداخليّة المجهولة (anonymous inner) راجع [[Kotlin/nested classes|الأصناف المتداخلة]]. | |||
== عناصر الصنف (Class Members) == | |||
قد يحتوي الصنف على: | |||
* البواني وأجزاء التهيئة (constructors and initializer blocks) | |||
* [[Kotlin/functions|الدوال (functions)]] | |||
* [[Kotlin/properties|الخاصّيّات (properties)]] | |||
* [[Kotlin/nested classes|الأصناف المتداخلة والداخلية (nested and inner classes)]] | |||
* [[Kotlin/object declarations|التصريح عن الكائنات (object declarations)]] | |||
== الوراثة (Inheritance) == | |||
تشترك كل الأصناف في لغة Kotlin بالصنف <code>Any</code> باعتباره الصنفَ الأعلى (superclass) الافتراضيّ لأي صنفٍ لم يُصرَّح عن صنفٍ أعلى له، مثل:<syntaxhighlight lang="kotlin"> | |||
class Example // وراثة ضمنيّة من الصنف Any | |||
</syntaxhighlight>ملاحظة: إن الصنف <code>Any</code> ليس من <code>java.lang.Object;</code> إذ لا يحتوي إلا على الدوال الثلاثة: <code>equals()</code> و <code>hashCode()</code> و <code>toString()</code>. | |||
وللتصريح عن الصنف الأعلى (superclass) بشكلٍ واضحٍ يُوضَع النوع بعد الرمز <code>:</code> في ترويسة الصنف (class header)، مثل:<syntaxhighlight lang="kotlin"> | |||
open class Base(p: Int) | |||
class Derived(p: Int) : Base(p) | |||
</syntaxhighlight>تعبِّر الكلمة المفتاحيّة <code>open</code> (قبل اسم الصنف) عن المعنى المعاكس للكلمة المفتاحية <code>final</code> في لغة Java، إذ يُسمَح (بوجود <code>open</code>) بعملية الوراثة من هذا الصنف لأنّ الحالة الافتراضيّة للأصناف في لغة Kotlin هي <code>final</code>. | |||
وإذا احتوى الصنف المُشتق (derived) على بانٍ أساسيٍّ فتجب تهيئة الصنف الأساسي (base) هناك بالاعتماد على معاملات (parameters) هذا الباني، أما إن لم يكن للصنف بانٍ رئيسيّ فإن كلَّ بانٍ ثانويّ يجب أن يُهيِئ النوعَ الأساسيّ (base type) باستخدام الكلمة المفتاحيّة <code>super</code> أو أن ينقل تلك المهمة لبانٍ آخر غيره، وبالتالي فإنه من الممكن أن تستدعي عدة بوانٍ ثانويّة العديدَ من بواني النوع الأساسيّ، مثل:<syntaxhighlight lang="kotlin"> | |||
class MyView : View { | |||
constructor(ctx: Context) : super(ctx) | |||
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) | |||
} | |||
</syntaxhighlight> | |||
=== إعادة تعريف التوابع (Overriding Methods) === | |||
تتطلَّب لغة Kotlin -على عكس لغة Java- التصريح بشكلٍ واضحٍ عن إمكانيّة إعادة تعريف العنصر من خلال إضافة الكلمة المفتاحيّة <code>open</code> قبله وبإضافة الكلمة المفتاحيّة <code>override</code> عند إعادة تعريفه، مثل:<syntaxhighlight lang="kotlin"> | |||
open class Base { | |||
open fun v() {} | |||
fun nv() {} | |||
} | |||
class Derived() : Base() { | |||
override fun v() {} | |||
} | |||
</syntaxhighlight>إذ من الضرور يّ إضافة <code>override</code> إلى الدالة <code>Derived.v()</code>، وإذا لم تُضف سينجم خطأ في الترجمة (compilation)، أمّا إن لم تٌضف الكلمة المفتاحيّة <code>open</code> الموجودة عند الدالة <code>Base.nv()</code> فلا يُسمَح بإعادة تعريفها سواءً باستخدام <code>override</code> أو بدونها، لأنّ الوصول إلى العناصر (members) الموجودة في الأصناف من نوع <code>final</code> (بدون وجود <code>open</code>) غير مسموحٍ به، ويُعدُّ العنصر المسبوق بالكلمة المفتاحيّة <code>override</code> من نوع <code>open</code> بالحالة الافتراضيّة (أي من الممكن إعادةُ تعريفه أيضًا في الأصناف الفرعيّة (subclasses))، ولمنع ذلك تجب إضافة الكلمة المفتاحيّة <code>final</code> كما في الشيفرة:<syntaxhighlight lang="kotlin"> | |||
open class AnotherDerived() : Base() { | |||
final override fun v() {} | |||
} | |||
</syntaxhighlight> | |||
=== إعادة تعريف الخاصّيّات (Overriding Properties) === | |||
تماثل إعادة تعريف الخاصّيّات طريقة إعادة تعريف التوابع (methods) في الفقرة السابقة؛ إذ لا بُدّ من وجود الكلمة المفاحيّة <code>override</code> قبل الخاصيّات التي يُعاد تعريفها في الصنف المُشتقّ (derived) والموجودة بالأصل في الصنف الأعلى (superclass) بشرط أن تكون الخاصّيّتان متوافقتَين (compitable)، ويُعاد التعريف إمّا باستخدام التهيئة (initializer) أو من خلال تابع الوصول <code>get()</code>، مثل:<syntaxhighlight lang="kotlin"> | |||
open class Foo { | |||
open val x: Int get() { ... } | |||
} | |||
class Bar1 : Foo() { | |||
override val x: Int = ... | |||
} | |||
</syntaxhighlight>ويُسمح بإعادة تعريف الخاصّيّة من النوع <code>val</code> بخاصّيّةٍ من النوع <code>var</code> لكن تُمنَع الحالة العكسية؛ وذلك لأنّ الخاصيّة من النوع <code>val</code> تٌصرِّح بالأصل عن تابع getter وتعيد تعريفه كنوع <code>var</code> وكذلك تُصرِّح عن تابع setter في الصنف المُشتقّ(derived). | |||
وقد تُستخدَم الكلمة المفتاحيّة <code>override</code> كجزءٍ من تعريف الخاصّيّة في الباني الأساسيّ (primary constructor)، مثل:<syntaxhighlight lang="kotlin"> | |||
interface Foo { | |||
val count: Int | |||
} | |||
class Bar1(override val count: Int) : Foo | |||
class Bar2 : Foo { | |||
override var count: Int = 0 | |||
} | |||
</syntaxhighlight> | |||
=== ترتيب عمليات تهيئة الصنف المُشتقّ (Derived class initialization order) === | |||
عند إنشاء كائنات (instances) من الصنف المُشتقّ فإن عمليات التهيئة الموجودة في الصنف الأساسيّ (base class) ستُنفَّذ أولًا (تسبقها فقط عمليات الوسائط (arguments) في الباني الأساسيّ)، وبالتالي فإنها ستجري قبل أيّ من عمليات التهيئة الموجودة في الصنف المُشتقّ، مثل:<syntaxhighlight lang="kotlin"> | |||
open class Base(val name: String) { | |||
init { println("Initializing Base") } | |||
open val size: Int = | |||
name.length.also { println("Initializing size in Base: $it") } | |||
} | |||
class Derived( | |||
name: String, | |||
val lastName: String | |||
) : Base(name.capitalize().also { println("Argument for Base: $it") }) { | |||
init { println("Initializing Derived") } | |||
override val size: Int = | |||
(super.size + lastName.length).also { println("Initializing size in Derived: $it") } | |||
} | |||
</syntaxhighlight>وهذا يعني أنّ الخاصّيّات (properties) المُعرَّفة أو مُعادَة التعريف (overridden) في الصنف المُشتقّ لا تُهيَّأ في الوقت الذي تُنفَّذ فيه عمليات الباني (constructor) في الصنف الأساسيّ، وبالتالي قد يحدث خللٌ تنفيذيّ (runtime failure) إذا كانت أيًا من هذه الخاصّيّات في الصنف المُشتقّ مستخدمةً في عمليات التهيئة في الصنف الأساسيّ إما مباشرةً أو بشكلٍ غير مباشرٍ (عبر تعريف الاستخدام (implementation) لأيّ عنصرٍ آخر من نوع <code>open</code> مُعاد التعريف (overridden))، ولتجنُّب حدوث ذلك يجب ألّا تُعرَّف العناصر من نوع <code>open</code> في الصنف الأساسيّ في أيّ من البواني أو تهيئة الخاصيات أو أيّ من أجزاء التهيئة (blocks) المُعرَّفة بالكلمة المفتاحيّة <code>init</code>. | |||
=== | ===استدعاء تعريف الاستخدام (implementation) من الصنف الأعلى (superclass)=== | ||
بإمكان الشيفرة الموجودة في الصنف المٌشتقّ استدعاء الدوال من الصنف الأعلى ودوال الوصول إلى الخاصّيّات (accessors) باستخدام الكلمة المفتاحية <code>super</code>، مثل:<syntaxhighlight lang="kotlin"> | |||
open class Foo { | |||
open fun f() { println("Foo.f()") } | |||
open val x: Int get() = 1 | |||
} | |||
= | class Bar : Foo() { | ||
override fun f() { | |||
super.f() | |||
println("Bar.f()") | |||
} | |||
override val x: Int get() = super.x + 1 | |||
} | |||
</syntaxhighlight> أما للوصول إلى الصنف الأعلى (superclass) للصنف الخارجيّ (outer) من الصنف الداخليّ (inner) فتُستخدَم الكلمة المفتاحيّة <code>super</code> مقيّدةً باسم الصنف الخارجي بالصيغة <code>super@Outer</code> كما في الشيفرة الآتية:<syntaxhighlight lang="kotlin"> | |||
class Bar : Foo() { | |||
override fun f() { /* ... */ } | |||
override val x: Int get() = 0 | |||
inner class Baz { | |||
fun g() { | |||
super@Bar.f() // استدعاء تعريف الاستخدام للدالة f() | |||
println(super@Bar.x) // استخدام تعريف الاستخدام لدالة الوصول إلى الخاصية x | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
== | === إعادة تعريف القواعد (Overriding Rules) === | ||
تخضع وراثة تعريف الاستخدام (implementation inheritance) في Kotlin للقاعدة الآتية: | |||
"إذا ورِث صنفٌ ما عدة تعريفاتٍ للاستخدام (implementations) من نفس العنصر (member) من الأصناف الأعلى المباشرة (immediate superclasses) فيجب أن يحتوي على إعادة تعريفٍ (override) لهذا العنصر ليكون له تعريف الاستخدام الخاصِّ به (ربما باستخدام إحدى التعريفات التي يرثها)"، وتستخدم الكلمة المفتاحية <code>super</code> مقيّدة باسم النوع الأعلى (supertype) ومحاطةً بالأقواس <code><></code> بالصيغة: <code>super<Base></code> للإشارة إلى النوع الأعلى الذي يُؤخَذ منه تعريف الاستخدام، مثل:<syntaxhighlight lang="kotlin"> | |||
open class A { | |||
open fun f() { print("A") } | |||
fun a() { print("a") } | |||
} | |||
interface B { | |||
fun f() { print("B") } // عناصر الواجهة تكون بالحالة الافتراضيّة مفتوحةً open | |||
fun b() { print("b") } | |||
} | |||
=== | class C() : A(), B { | ||
// يتطلب المترجم وجود إعادة تعريف للدالة f() | |||
override fun f() { | |||
super<A>.f() // استدعاء A.f() | |||
super<B>.f() // استدعاء B.f() | |||
} | |||
} | |||
</syntaxhighlight>إذ لا مشكلة في وراثة الدالتين <code>a()</code> و <code>b()</code> من كلٍّ من الصنفين <code>A</code> و <code>B</code> (بالترتيب) لأنّ الصنف <code>C</code> يرِث تعريفًا واحدًا فقط من كلٍّ من هذه الدوال، أمّا بالنسبة للدالة <code>f()</code> فيرث الصنف <code>C</code> تعريفين لاستخدامها وبالتالي فعليه أن يحتوي على إعادة تعريف (override) للدالة <code>f()</code> ليكون له تعريف الاستخدام الخاصّ به لإزالة ذلك الالتباس. | |||
== الأصناف المُجرَّدة (Abstract Classes) == | |||
<span> قد يُصرَّح عن الصنف أو بعضٍ من عناصره (members) بالكلمة المفتاحيّة <code>abstract</code> (مُجرَّد) بحيث تكون هذه العناصر دون تعريفٍ للاستخدام (implementation) ضمن الصنف الموجودة فيه، ولا حاجة لإضافة الكلمة المفتاحيّة <code>open</code> للصنف أو العناصر المجرَّدة فهي موجودةٌ بالحالة الافتراضيّة.</span> | |||
وبالإمكان إعادة تعريف (override) العنصر المفتوح غير المُجرَّد في عنصرٍ آخر مُجرَّد، مثل:<syntaxhighlight lang="kotlin"> | |||
open class Base { | |||
open fun f() {} | |||
} | |||
abstract class Derived : Base() { | |||
override abstract fun f() | |||
} | |||
</syntaxhighlight> | |||
== | == الكائنات المرافقة (Companion Objects) == | ||
لا وجود للتوابع الستاتيكيّة (static methods) في Kotlin -على عكس لغتيّ البرمجة Java وC#- ويُستعاض عنها بالدوال على مستوى الحزمة (package-level functions). | |||
وعند الحاجة إلى كتابة دالةٍ قابلةٍ للاستدعاء دون إنشاء كائنٍ (instance) من الصنف ولكن لها صلاحيّة الوصول (access) إلى ما داخل الصنف (للتابع المُنتِج (factory method) مثلًا) فيُمكن جعلها كعنصرٍ (member) من [[Kotlin/object declarations|تصريح الكائن]] نفسه داخل ذلك الصنف. | |||
وبشكل أكثر تفصيلًا؛ إذا صُرِّح عن [[Kotlin/object declarations|الكائن المرافق]] داخل الصنف فيمكن حينئذٍ استدعاء عناصره بنفس صيغة استدعاء التوابع الستاتيكيّة في لغتيّ Java وC# بالاعتماد على اسم الصنف فقط. | |||
== مصادر == | == مصادر == |
المراجعة الحالية بتاريخ 15:49، 4 يوليو 2018
تُستخدم الكلمة المفتاحيّة 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)
قد يحتوي الصنف على:
- البواني وأجزاء التهيئة (constructors and initializer blocks)
- الدوال (functions)
- الخاصّيّات (properties)
- الأصناف المتداخلة والداخلية (nested and inner classes)
- التصريح عن الكائنات (object declarations)
الوراثة (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) هناك بالاعتماد على معاملات (parameters) هذا الباني، أما إن لم يكن للصنف بانٍ رئيسيّ فإن كلَّ بانٍ ثانويّ يجب أن يُهيِئ النوعَ الأساسيّ (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
}
ترتيب عمليات تهيئة الصنف المُشتقّ (Derived class initialization order)
عند إنشاء كائنات (instances) من الصنف المُشتقّ فإن عمليات التهيئة الموجودة في الصنف الأساسيّ (base class) ستُنفَّذ أولًا (تسبقها فقط عمليات الوسائط (arguments) في الباني الأساسيّ)، وبالتالي فإنها ستجري قبل أيّ من عمليات التهيئة الموجودة في الصنف المُشتقّ، مثل:
open class Base(val name: String) {
init { println("Initializing Base") }
open val size: Int =
name.length.also { println("Initializing size in Base: $it") }
}
class Derived(
name: String,
val lastName: String
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {
init { println("Initializing Derived") }
override val size: Int =
(super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}
وهذا يعني أنّ الخاصّيّات (properties) المُعرَّفة أو مُعادَة التعريف (overridden) في الصنف المُشتقّ لا تُهيَّأ في الوقت الذي تُنفَّذ فيه عمليات الباني (constructor) في الصنف الأساسيّ، وبالتالي قد يحدث خللٌ تنفيذيّ (runtime failure) إذا كانت أيًا من هذه الخاصّيّات في الصنف المُشتقّ مستخدمةً في عمليات التهيئة في الصنف الأساسيّ إما مباشرةً أو بشكلٍ غير مباشرٍ (عبر تعريف الاستخدام (implementation) لأيّ عنصرٍ آخر من نوع open
مُعاد التعريف (overridden))، ولتجنُّب حدوث ذلك يجب ألّا تُعرَّف العناصر من نوع open
في الصنف الأساسيّ في أيّ من البواني أو تهيئة الخاصيات أو أيّ من أجزاء التهيئة (blocks) المُعرَّفة بالكلمة المفتاحيّة init
.
استدعاء تعريف الاستخدام (implementation) من الصنف الأعلى (superclass)
بإمكان الشيفرة الموجودة في الصنف المٌشتقّ استدعاء الدوال من الصنف الأعلى ودوال الوصول إلى الخاصّيّات (accessors) باستخدام الكلمة المفتاحية super
، مثل:
open class Foo {
open fun f() { println("Foo.f()") }
open val x: Int get() = 1
}
class Bar : Foo() {
override fun f() {
super.f()
println("Bar.f()")
}
override val x: Int get() = super.x + 1
}
أما للوصول إلى الصنف الأعلى (superclass) للصنف الخارجيّ (outer) من الصنف الداخليّ (inner) فتُستخدَم الكلمة المفتاحيّة super
مقيّدةً باسم الصنف الخارجي بالصيغة super@Outer
كما في الشيفرة الآتية:
class Bar : Foo() {
override fun f() { /* ... */ }
override val x: Int get() = 0
inner class Baz {
fun g() {
super@Bar.f() // استدعاء تعريف الاستخدام للدالة f()
println(super@Bar.x) // استخدام تعريف الاستخدام لدالة الوصول إلى الخاصية x
}
}
}
إعادة تعريف القواعد (Overriding Rules)
تخضع وراثة تعريف الاستخدام (implementation inheritance) في Kotlin للقاعدة الآتية:
"إذا ورِث صنفٌ ما عدة تعريفاتٍ للاستخدام (implementations) من نفس العنصر (member) من الأصناف الأعلى المباشرة (immediate superclasses) فيجب أن يحتوي على إعادة تعريفٍ (override) لهذا العنصر ليكون له تعريف الاستخدام الخاصِّ به (ربما باستخدام إحدى التعريفات التي يرثها)"، وتستخدم الكلمة المفتاحية super
مقيّدة باسم النوع الأعلى (supertype) ومحاطةً بالأقواس <>
بالصيغة: super<Base>
للإشارة إلى النوع الأعلى الذي يُؤخَذ منه تعريف الاستخدام، مثل:
open class A {
open fun f() { print("A") }
fun a() { print("a") }
}
interface B {
fun f() { print("B") } // عناصر الواجهة تكون بالحالة الافتراضيّة مفتوحةً open
fun b() { print("b") }
}
class C() : A(), B {
// يتطلب المترجم وجود إعادة تعريف للدالة f()
override fun f() {
super<A>.f() // استدعاء A.f()
super<B>.f() // استدعاء B.f()
}
}
إذ لا مشكلة في وراثة الدالتين a()
و b()
من كلٍّ من الصنفين A
و B
(بالترتيب) لأنّ الصنف C
يرِث تعريفًا واحدًا فقط من كلٍّ من هذه الدوال، أمّا بالنسبة للدالة f()
فيرث الصنف C
تعريفين لاستخدامها وبالتالي فعليه أن يحتوي على إعادة تعريف (override) للدالة f()
ليكون له تعريف الاستخدام الخاصّ به لإزالة ذلك الالتباس.
الأصناف المُجرَّدة (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# بالاعتماد على اسم الصنف فقط.