الخاصّيّات (Properties) وحقول البيانات (Fields) في لغة Kotlin

من موسوعة حسوب
اذهب إلى: تصفح، ابحث

التصريح عن الخاصّيّات (Declaring Properties)

قد تحتوي الأصناف في لغة Kotlin على الخاصّيّات المعرَّفة إما كقيمٍ متغيّرةٍ عبر الكلمة المفتاحيّة var أو كقيمٍ ثابتةٍ للقراءة فقط (read-only) عبر الكلمة المفتاحيّة val، مثل:
class Address {
    var name: String = ...
    var street: String = ...
    var city: String = ...
    var state: String? = ...
    var zip: String = ...
}
إذ يُمكن الوصول للخصائص عبر اسمها (كما لو كانت حقلًا [field] في لغة Java)، مثل:
fun copyAddress(address: Address): Address {
    val result = Address() // لا وجود هنا للكلمة المفتاحيّة new
    result.name = address.name // تُستدعى دوال الوصول (accessors)
    result.street = address.street
    // ...
    return result
}

الوصول للخاصّيات باستخدام Getter و Setter

إنّ الصيغة الكاملة للتصريح عن الخاصيّات هي:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]
إذ يُعدُّ وجودُ كلّ من التهيئة وعمليات الوصول (getter و setter) أمرًا اختياريًا، ويصبح وجود النوع اختياريًا أيضًا إذا أمكن تحديده من خلال التهيئة (أو من النوع المُعاد في دالة getter) كما في الأمثلة الآتية:
var allByDefault: Int? // تحدث مشكلة: يجب وجود التهيئة بشكل واضح، ودوال الوصول 
// الافتراضيّة ضمنيّة 
var initialized = 1 // النوع هو نوع الأعداد الصحيحة ودوال الوصول افتراضيّة
أمّا الصيغة الكاملة للتصريح عن خاصّيّات القراءة فقط فهي مختلفةٌ عن سابقتها من ناحيتين: الأولى أنّها تبدأ بالكلمة المفتاحيّة val بدلًا من var، والثانية أنه لا وجود لدالة الوصول setter، وهي بالصيغة:
val simple: Int? // للخاصية نوع الأعداد الصحيحة ويجب القيام بعملية التهيئة في الباني ودالة الوصول الافتراضية getter

val inferredType = 1 // لها نوع الأعداد الصحيحة ودالة وصول افتراضية getter
ويُمكن تخصيص دوال الوصول (accessors) مثل عملية تعديل أيّ دالة أخرى وذلك في تعريف الخاصّيّة، مثل تخصيص getter في الشيفرة الآتية:
val isEmpty: Boolean
    get() = this.size == 0
أمّا تخصيص الدالة setter فهو كما في الشيفرة:
var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // تحويل السلسلة النصية وإسناد القيمة إلى خاصّيّة أخرى
    }
إذ تعبِّر value -اصطلاحًا- عن اسم المعامل في setter ويمكن تعديله. وبدءًا من الإصدار 1.1 يمكن حذف نوع الخاصّيّة إذا أمكن تحديده من خلال getter مثل:
val isEmpty get() = this.size == 0  // لها النوع Boolean
ويمكن في بعض الأحيان تعريفُ (define) أيّ من دوال الوصول (accessors) بدون تعريف بُنيتها (body) وذلك عند الحاجة لتغيير مرئيّة الوصول لها (visibility) أو إضافة التوصيف (annotation) دون تغيير تعريف الاستخدام (implementation) الافتراضيّ، كما هو الحال في الشيفرة الآتية:
var setterVisibility: String = "abc"
    private set // محدد الوصول هو من النوع الخاص وله تعريف الاستخدام الافتراضيّ

var setterWithAnnotation: Any? = null
    @Inject set // إضافة التوصيف إليه باسم Inject

الحقول المُساعدة (Backing Fields)

لا تُعرَّف الحقول مباشرةً في الأصناف (classes) في لغة Kotlin لذلك فإنه عند الحاجة لحقلٍ مساعدٍ فإن Kotlin تزوِّد به تلقائيًا، ويُشار إليه في دوال الوصول عبر المُحدِّد field، مثل:
var counter = 0 // تُسند القيمة أيضًا للحقل المساعد مباشرة
    set(value) {
        if (value >= 0) field = value
    }
إذ إن استخدام المُحدِّد field متاحٌ فقط في دوال الوصول (accessors) لتلك الخاصية. ويُولَّد الحقل المُساعد للخاصية إذا ما استخدَمت تعريف الاستخدام (implementation) الافتراضيّ لدوال الوصول ولو لمرةٍ واحدةٍ أو في حالة الإشارة لها عبر المُحدِّد field عند تخصيص أيّ من دوال الوصول، فلن يكون هناك مثلًا حقلٌ مساعدٌ في الشيفرة الآتية:
val isEmpty: Boolean
    get() = this.size == 0

الخاصّيّات المُساعدِة (Backing Properties)

عند القيام بما يتعارض مع آليّة الحقول المُساعدة الضمنيّة (المشروحة في الفقرة السابقة) فيُعمَد لاستخدام الخاصيّات المُساعدِة كما هو الحال في الشيفرة الآتية:
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // تُحدد معاملات النوع تلقائيًا
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }
وهذا يماثل -من كافّة النواحي- ما هو مُعتمدٌ في لغة Java، لأن الوصول إلى الخاصيات من النوع الخاصّ (private) باستخدام الدوال الافتراضيّة للوصول (accessor) مُصمَّمٌ بحيث لا يمكن تجاوزه من قِبل أيّ استدعاءٍ للدوال.

ثوابت الترجمة (Compile-Time Constants)

تٌحدَّد الخاصّيّات التي تُعرَف قيمتها (known) أثناء عملية الترجمة باعتبارها "ثوابت الترجمة" وذلك باستخدام المُحدِّد const، ولها المتطلَّبات الآتية:

  • أن تكون أحدَ العناصر (members) في object أو واقعة بمستوى أعلى (top-level)
  • مُهيَّئةٌ بالقيمة من النوع String أو أيّ نوع أساسي (primitive) آخر
  • لا يوجد لها getter مُخصَّص
وتستخدم مثل هذه الخاصّيّات في التوصيفات (annotations) مثل:
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }

التهيئة اللاحقة (Late-Initializing) للخاصيات (Properties) والمتغيرات (Variables)

تُهيَّأ الخاصّيّات المُعرَّفة بالحالة الطبيعيّة من النوع بدون قيم فارغة (non-null) في الباني (constructor) ولكنّ ذلك لا يكون مناسبًا دائمًا، إذ يُمكن مثلًا أن تُهيَّأ خلال إضافة اعتمادية (dependency injection) أو بتابع الإعداد (setup method) لاختبار البُنية (unit test)، ولا يُمكن حينئذٍ الاعتماد على تهيئةٍ non-null في الباني على الرغم من الرغبة بتجنُّب عمليات التحقُّق من القيم الفارغة عند الإشارة (reference) إلى الخاصّيّات داخل بنية الصنف (class body).

ولمعالجة هذه الحالة تُعتمَد التهيئة اللاحقة عبر المُحدِّد lateinit مثل:
public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // ٌإسنادٌ مؤشريّ مباشر
    }
}
ويُستخدَم هذا المُحدِّد مع الخاصّيّات من النوع var المُعرَّفة داخل بُنية الصنف (وليس في الباني الأساسي [primary constructor] وفقط عندما لا يكون هناك تخصيص في دوال الوصول [accessors])، واعتبارًا من الإصدار Kotlin 1.2 يُستخدَم أيضًا للخاصيّات بمستوى أعلى (top-level) وللمتغيِّرات المحلية (local variables)، إذ يجب أن يكون النوع فيها نوعًا أساسيًا (primitive) ولا يقبل القيم الفارغة (non-null).

و ينتُج عن محاولة الوصول إلى الخاصّيّة قبل تهيئتها اللاحقة استثناءٌ (exception) خاصّ يُفيد بأنّه قد تمّ الوصول إلى الخاصّيّة قبل تهيئتها.

التأكد من التهيئة اللاحقة للمتغير var (بدءًا من الإصدار 1.2)

تُستخدَم ‎.‎isInitialized‎ مع مرجعيّة (reference) الخاصّيّة المُحدَّدة بالمُعرِّفين lateinit var للتحقُّق من إتمام عملية التهيئة اللاحقة لها، مثل:
if (foo::bar.isInitialized) {
    println(foo.bar)
}
ويُسمَح بهذا التحقق فقط عندما يكون الوصول لهذه الخاصيّة مُتاحًا (accessible)، أي أنها مُعرَّفةٌ من نفس النوع أو أحد الأنواع الخارجية (outer) أو بمستوى أعلى (top-level) في نفس الملف (file).

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

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

الخاصّيّات المُعمَّمة (Delegated Properties)

إنّ النوع الأكثر شيوعًا للخاصّيّات مُعدٌّ للقراءة من (وربما الكتابة في) الحقل المُساعِد، أمّا في حالة تخصيص دوال الوصول (getter أو setter) فيُمكن تعريف استخدام (implement) أيّ سلوكٍ للخاصّيّة، وهناك أنماطٌ مُحدَّدةٌ لكيفية عمل الخاصّيّة، مثل: القيم الكسولة (lazy value) أو القراءة من map من خلال المفتاح (key)، أو الوصول إلى قاعدة البيانات أو الإعلام عند محاولة الوصول أو ...إلخ، ومثلُ هذه الحالات يُمكن تعريف استخدامها كمكتباتٍ (libraries) باستخدام الخاصّيّات المُعمَّمة (delegated properties).

مصادر