الفرق بين المراجعتين لصفحة: «Kotlin/null safety»

من موسوعة حسوب
أنشأ الصفحة ب'<noinclude>{{DISPLAYTITLE:أمان القيم الفارغة (Null) في لغة Kotlin}}</noinclude> == الأنواع Nullable والأنواع Non-Null == يهدف ن...'
 
ط تعديل مصطلح متحول
 
(مراجعة متوسطة واحدة بواسطة مستخدم واحد آخر غير معروضة)
سطر 1: سطر 1:
<noinclude>{{DISPLAYTITLE:أمان القيم الفارغة (Null) في لغة Kotlin}}</noinclude>
<noinclude>{{DISPLAYTITLE:أمان القيم الفارغة (Null) في لغة Kotlin}}</noinclude>
== الأنواع Nullable والأنواع Non-Null ==
== الأنواع Nullable والأنواع Non-Null ==
يهدف نظام الأنواع في Kotlin إلى الحدِّ من أخطار القيمة الفارغة <code>null</code> في الشيفرات، إذ إنّ أحد الأخطاء الأكثر شيوعًا في لفات البرمجة -بما فيها لغة Java- هو أنّ محاولة الوصول إلى مرجعيّةٍ تحتوي على القيمة <code>null</code> سيؤدي إلى حدوث استثناءٍ مرجعيّ (reference exception)، ويُدعى هذا الاستثناء في لغة Java باسم <code>NullPointerException</code> أو NPE اختصارًا، أمّا Kotlin فهي تحدُّ من هذا الاستثناء ليقتصر على الحالات الآتية:
يهدف نظام الأنواع في Kotlin إلى الحدِّ من أخطار القيمة الفارغة <code>null</code> في الشيفرات، إذ إنّ أحد الأخطاء الأكثر شيوعًا في لغات البرمجة -بما فيها لغة Java- هو أنّ محاولة الوصول إلى مرجعيّةٍ تحتوي على القيمة <code>null</code> سيؤدي إلى حدوث استثناءٍ مرجعيّ (reference exception)، ويُدعى هذا الاستثناء في لغة Java باسم <code>NullPointerException</code> أو NPE اختصارًا، أمّا Kotlin فهي تحدُّ من هذا الاستثناء ليقتصر على الحالات الآتية:
* استدعاءٌ صريحٌ بالشكل: <code>throw NullPointerException()‎</code>
* استدعاءٌ صريحٌ بالشكل: <code>throw NullPointerException()‎</code>
* استخدام المعامل <code>!!</code> (كما سيُشرح لاحقًا)
* استخدام المعامل <code>!!</code> (كما سيُشرح لاحقًا)
سطر 11: سطر 11:
** الاستخدام الخاطئ لقابليّة القيمة الفارغة (nullability) في الأنواع المُعمَّمة (generic types) في التوافقيّة مع لغة Java ؛ كأن تحاول شيفرة Java إضافة قيمة <code>null</code> في النوع <code>MutableList<String></code>‎ الموجود في Kotlin بدلًا من النوع <code>MutableList<String?></code>‎ المُخصَّص للتعامل معها.
** الاستخدام الخاطئ لقابليّة القيمة الفارغة (nullability) في الأنواع المُعمَّمة (generic types) في التوافقيّة مع لغة Java ؛ كأن تحاول شيفرة Java إضافة قيمة <code>null</code> في النوع <code>MutableList<String></code>‎ الموجود في Kotlin بدلًا من النوع <code>MutableList<String?></code>‎ المُخصَّص للتعامل معها.
** بعض الحالات الأخرى الناتجة عن استخدام شيفرة Java خارجيّة.
** بعض الحالات الأخرى الناتجة عن استخدام شيفرة Java خارجيّة.
ويميّز نظام الأنواع في Kotlin بين المرجعيّة التي يمكن أن تقبل القيمة الفارغة <code>null</code> وتُدعى nullable وتلك التي لا يمكن أن تحتويها وتُدعى non-null، فلا يمكن مثلًا للمتحول من النوع <code>String</code> أن يحتوي على القيمة <code>null</code> كما في الشيفرة:<syntaxhighlight lang="kotlin">
ويميّز نظام الأنواع في Kotlin بين المرجعيّة التي يمكن أن تقبل القيمة الفارغة <code>null</code> وتُدعى nullable وتلك التي لا يمكن أن تحتويها وتُدعى non-null، فلا يمكن مثلًا للمتغيِّر من النوع <code>String</code> أن يحتوي على القيمة <code>null</code> كما في الشيفرة:<syntaxhighlight lang="kotlin">
var a: String = "abc"
var a: String = "abc"
a = null // سينتج خطأٌ بالترجمة
a = null // سينتج خطأٌ بالترجمة
</syntaxhighlight>وللسماح بذلك يجب التصريح عن المتحوّل بالنوع <code>String?‎</code> (أي النوع nullable String)، كما في الشيفرة:<syntaxhighlight lang="kotlin">
</syntaxhighlight>وللسماح بذلك يجب التصريح عن المتغيِّر بالنوع <code>String?‎</code> (أي النوع nullable String)، كما في الشيفرة:<syntaxhighlight lang="kotlin">
var b: String? = "abc"
var b: String? = "abc"
b = null // يُسمح بهذا
b = null // يُسمح بهذا
</syntaxhighlight>وعند استدعاء تابعٍ (method) أو الوصول إلى خاصّيّةٍ في المتحول <code>a</code> فمن المؤكَّد عدم حدوث استثناءٍ من النوع NPE (المشروح سابقًا)، وبالتالي فمن الآمن كتابة الشيفرة بالشكل:<syntaxhighlight lang="kotlin">
</syntaxhighlight>وعند استدعاء تابعٍ (method) أو الوصول إلى خاصّيّةٍ في المتغيِّر <code>a</code> فمن المؤكَّد عدم حدوث استثناءٍ من النوع NPE (المشروح سابقًا)، وبالتالي فمن الآمن كتابة الشيفرة بالشكل:<syntaxhighlight lang="kotlin">
val l = a.length
val l = a.length
</syntaxhighlight>أما عند الوصول إلى نفس الخاصّيّة في المتحول <code>b</code> فسينتج خطأٌ كما في الشيفرة:<syntaxhighlight lang="kotlin">
</syntaxhighlight>أما عند الوصول إلى نفس الخاصّيّة في المتغيِّر <code>b</code> فسينتج خطأٌ كما في الشيفرة:<syntaxhighlight lang="kotlin">
val l = b.length // سينتج خطأ لأنه من الممكن أن يكون المتحول بقيمة فارغة
val l = b.length // سينتج خطأ لأنه من الممكن أن يكون المتغيِّر بقيمة فارغة
</syntaxhighlight>وتتيح Kotlin عدّة طرقٍ للقيام بذلك دون حدوث أيّة أخطاء.
</syntaxhighlight>وتتيح Kotlin عدّة طرقٍ للقيام بذلك دون حدوث أيّة أخطاء.


== التحقُّق من القيمة الفارغة Null في الشرط ==
== التحقُّق من القيمة الفارغة Null في الشرط ==
تتلخَّص الطريقة الأولى بالتأكُّد بشكلٍ صريحٍ من أنّ المتحول <code>b</code> لا يحتوي على القيمة <code>null</code> ومعالجة الحالتين بشكلٍ منفصلٍ، كما يلي:<syntaxhighlight lang="kotlin">
تتلخَّص الطريقة الأولى بالتأكُّد بشكلٍ صريحٍ من أنّ المتغيِّر <code>b</code> لا يحتوي على القيمة <code>null</code> ومعالجة الحالتين بشكلٍ منفصلٍ، كما يلي:<syntaxhighlight lang="kotlin">
val l = if (b != null) b.length else -1
val l = if (b != null) b.length else -1
</syntaxhighlight>إذ يتتبَّع المترجِم (compiler) المعلومات الناتجة عن التحقُّق المُجرى، ويَسمح باستدعاء <code>length</code> الموجودة في الشرط <code>if</code>، كما تدعم Kotlin صيغةً أخرى أكثر تعقيدًا كما يلي:<syntaxhighlight lang="kotlin">
</syntaxhighlight>إذ يتتبَّع المترجِم (compiler) المعلومات الناتجة عن التحقُّق المُجرى، ويَسمح باستدعاء <code>length</code> الموجودة في الشرط <code>if</code>، كما تدعم Kotlin صيغةً أخرى أكثر تعقيدًا كما يلي:<syntaxhighlight lang="kotlin">
سطر 32: سطر 32:
     print("Empty string")
     print("Empty string")
}
}
</syntaxhighlight>وهذا يصلح فقط عندما يكون المتحوِّل <code>b</code> غير متغيّرٍ (immutable)؛ أي قد يكون متحولًا محليًّا لا يتغيّر ما بين عملية التحقُّق والاستخدام أو متحولًا من النوع <code>val</code> له حقلٌ مساعدٌ (backing field) ولا يُسمَح بإعادة تعريف استخدامه (not overridable)، لأنه ما لم يكن كذلك فمن الممكن أن تتغيَّر قيمته لتصبح <code>null</code> من بعد عملية التحقُّق.
</syntaxhighlight>وهذا يصلح فقط عندما يكون المتغيِّر <code>b</code> غير متغيّرٍ (immutable)؛ أي قد يكون متغيِّرًا محليًّا لا يتغيّر ما بين عملية التحقُّق والاستخدام أو متحولًا من النوع <code>val</code> له حقلٌ مساعدٌ (backing field) ولا يُسمَح بإعادة تعريف استخدامه (not overridable)، لأنه ما لم يكن كذلك فمن الممكن أن تتغيَّر قيمته لتصبح <code>null</code> من بعد عملية التحقُّق.


== الاستدعاء الآمن (Safe Call) ==
== الاستدعاء الآمن (Safe Call) ==
الخيار المتاح الثاني لتجنُّب حدوث خطأٍ ناتجٍ عن القيمة <code>null</code> هو استخدام معامل الاستدعاء الآمن <code>.?</code> بالشكل:<syntaxhighlight lang="kotlin">
الخيار المتاح الثاني لتجنُّب حدوث خطأٍ ناتجٍ عن القيمة <code>null</code> هو استخدام معامل الاستدعاء الآمن <code>.?</code> بالشكل:<syntaxhighlight lang="kotlin">
b?.length
b?.length
</syntaxhighlight>والذي يُعيد <code>null</code> إن كان المتحول <code>b</code> يحتوي على القيمة <code>null</code> ويعيد <code>b.length</code> إن لم يكن كذلك، ويكون نوع التعبير  <code>Int?</code>‎.
</syntaxhighlight>والذي يُعيد <code>null</code> إن كان المتغيِّر <code>b</code> يحتوي على القيمة <code>null</code> ويعيد <code>b.length</code> إن لم يكن كذلك، ويكون نوع التعبير  <code>Int?</code>‎.


يُفيد استخدام الاستدعاء الآمن في حالة السلاسل (chains)؛ فإن كان الموظَّف Bob مثلًا يتبع لقسمٍ (department) ما (وربما لا يتبع)، والذي بدوره يحتوي على موظفٍ آخر باعتباره مديرًا للقسم (head)، وبالتالي فإنه للحصول على اسم رئيس القسم الذي يعمل به الموظَّف Bob (إن وُجد) تكتب الشيفرة:<syntaxhighlight lang="kotlin">
يُفيد استخدام الاستدعاء الآمن في حالة السلاسل (chains)؛ فإن كان الموظَّف Bob مثلًا يتبع لقسمٍ (department) ما (وربما لا يتبع)، والذي بدوره يحتوي على موظفٍ آخر باعتباره مديرًا للقسم (head)، وبالتالي فإنه للحصول على اسم رئيس القسم الذي يعمل به الموظَّف Bob (إن وُجد) تكتب الشيفرة:<syntaxhighlight lang="kotlin">
سطر 63: سطر 63:
</syntaxhighlight>فإن لم يكن التعبير على يسار المعامل ‎<code>?:</code>‎  بالقيمة <code>null</code> فسيُعيده المعامل، أمّا إن كان بالقيمة <code>null</code> فسيُعيد التعبيرَ  في الجانب الأيمن، والذي سيُحسَب فقط عندما يكون الجانب الأيسر بالقيمة <code>null</code>.
</syntaxhighlight>فإن لم يكن التعبير على يسار المعامل ‎<code>?:</code>‎  بالقيمة <code>null</code> فسيُعيده المعامل، أمّا إن كان بالقيمة <code>null</code> فسيُعيد التعبيرَ  في الجانب الأيمن، والذي سيُحسَب فقط عندما يكون الجانب الأيسر بالقيمة <code>null</code>.


وبما أنّ التعليمتين <code>return</code> و <code>throw</code> تُعدَّان تعابيرًا (expressions) في لغة Kotlin فيمكن استخدامهما على الجانب الأيمن من المعامل ‎<code>?:</code>‎ ، وهذا يساعد عند التحقُّق من متحولات (arguments) الدالة كما في الشيفرة:<syntaxhighlight lang="kotlin">
وبما أنّ التعليمتين <code>return</code> و <code>throw</code> تُعدَّان تعابيرًا (expressions) في لغة Kotlin فيمكن استخدامهما على الجانب الأيمن من المعامل ‎<code>?:</code>‎ ، وهذا يساعد عند التحقُّق من وسائط (arguments) الدالة كما في الشيفرة:<syntaxhighlight lang="kotlin">
fun foo(node: Node): String? {
fun foo(node: Node): String? {
     val parent = node.getParent() ?: return null
     val parent = node.getParent() ?: return null
سطر 72: سطر 72:


== المعامل <code>!!</code> ==
== المعامل <code>!!</code> ==
وهو الطريقة الثالثة لتجنُّب الاستثناء NPE (المشروح سابقًا)، حيث يحوِّل المعامل <code>!!</code> أيَّ قيمةٍ إلى النوع non-null وسيَنتُج (throw) استثناءٌ إن كانت القيمة <code>null</code>، يستخدم المعامل بالصيغة <code>b!!‎</code> ، وهذا يعيد القيمة غير الفارغة من المتحول <code>b</code> وهي السلسلة النصّيّة String، أو سينتج الاستثناء NPE إن كان المتحول <code>b</code> بالقيمة <code>null</code>، مثل:<syntaxhighlight lang="kotlin">
وهو الطريقة الثالثة لتجنُّب الاستثناء NPE (المشروح سابقًا)، حيث يحوِّل المعامل <code>!!</code> أيَّ قيمةٍ إلى النوع non-null وسيرمى (throw) استثناءٌ إن كانت القيمة <code>null</code>، يستخدم المعامل بالصيغة <code>b!!‎</code> ، وهذا يعيد القيمة غير الفارغة من المتغيِّر <code>b</code> وهي السلسلة النصّيّة String، أو سيرمى الاستثناء NPE إن كان المتغيِّر <code>b</code> بالقيمة <code>null</code>، مثل:<syntaxhighlight lang="kotlin">
val l = b!!.length
val l = b!!.length
</syntaxhighlight>وبالتالي عند الحاجة لوجود الاستثناء NPE فهو متاحٌ.
</syntaxhighlight>وبالتالي عند الحاجة لوجود الاستثناء NPE فهو متاحٌ.

المراجعة الحالية بتاريخ 17:38، 4 يوليو 2018

الأنواع Nullable والأنواع Non-Null

يهدف نظام الأنواع في Kotlin إلى الحدِّ من أخطار القيمة الفارغة null في الشيفرات، إذ إنّ أحد الأخطاء الأكثر شيوعًا في لغات البرمجة -بما فيها لغة Java- هو أنّ محاولة الوصول إلى مرجعيّةٍ تحتوي على القيمة null سيؤدي إلى حدوث استثناءٍ مرجعيّ (reference exception)، ويُدعى هذا الاستثناء في لغة Java باسم NullPointerException أو NPE اختصارًا، أمّا Kotlin فهي تحدُّ من هذا الاستثناء ليقتصر على الحالات الآتية:

  • استدعاءٌ صريحٌ بالشكل: throw NullPointerException()‎
  • استخدام المعامل !! (كما سيُشرح لاحقًا)
  • التعارض في البيانات أثناء التهيئة (initialization) كما في الحالات الآتية:
  • أثناء توافقية التبديل مع لغة Java:
    • محاولة الوصول إلى عنصرٍ من الصنف عبر مرجعيّة null من نوع المنصة (platform type)
    • الاستخدام الخاطئ لقابليّة القيمة الفارغة (nullability) في الأنواع المُعمَّمة (generic types) في التوافقيّة مع لغة Java ؛ كأن تحاول شيفرة Java إضافة قيمة null في النوع MutableList<String>‎ الموجود في Kotlin بدلًا من النوع MutableList<String?>‎ المُخصَّص للتعامل معها.
    • بعض الحالات الأخرى الناتجة عن استخدام شيفرة Java خارجيّة.

ويميّز نظام الأنواع في Kotlin بين المرجعيّة التي يمكن أن تقبل القيمة الفارغة null وتُدعى nullable وتلك التي لا يمكن أن تحتويها وتُدعى non-null، فلا يمكن مثلًا للمتغيِّر من النوع String أن يحتوي على القيمة null كما في الشيفرة:

var a: String = "abc"
a = null // سينتج خطأٌ بالترجمة

وللسماح بذلك يجب التصريح عن المتغيِّر بالنوع String?‎ (أي النوع nullable String)، كما في الشيفرة:

var b: String? = "abc"
b = null // يُسمح بهذا

وعند استدعاء تابعٍ (method) أو الوصول إلى خاصّيّةٍ في المتغيِّر a فمن المؤكَّد عدم حدوث استثناءٍ من النوع NPE (المشروح سابقًا)، وبالتالي فمن الآمن كتابة الشيفرة بالشكل:

val l = a.length

أما عند الوصول إلى نفس الخاصّيّة في المتغيِّر b فسينتج خطأٌ كما في الشيفرة:

val l = b.length // سينتج خطأ لأنه من الممكن أن يكون المتغيِّر بقيمة فارغة

وتتيح Kotlin عدّة طرقٍ للقيام بذلك دون حدوث أيّة أخطاء.

التحقُّق من القيمة الفارغة Null في الشرط

تتلخَّص الطريقة الأولى بالتأكُّد بشكلٍ صريحٍ من أنّ المتغيِّر b لا يحتوي على القيمة null ومعالجة الحالتين بشكلٍ منفصلٍ، كما يلي:

val l = if (b != null) b.length else -1

إذ يتتبَّع المترجِم (compiler) المعلومات الناتجة عن التحقُّق المُجرى، ويَسمح باستدعاء length الموجودة في الشرط if، كما تدعم Kotlin صيغةً أخرى أكثر تعقيدًا كما يلي:

if (b != null && b.length > 0) {
    print("String of length ${b.length}")
} else {
    print("Empty string")
}

وهذا يصلح فقط عندما يكون المتغيِّر b غير متغيّرٍ (immutable)؛ أي قد يكون متغيِّرًا محليًّا لا يتغيّر ما بين عملية التحقُّق والاستخدام أو متحولًا من النوع val له حقلٌ مساعدٌ (backing field) ولا يُسمَح بإعادة تعريف استخدامه (not overridable)، لأنه ما لم يكن كذلك فمن الممكن أن تتغيَّر قيمته لتصبح null من بعد عملية التحقُّق.

الاستدعاء الآمن (Safe Call)

الخيار المتاح الثاني لتجنُّب حدوث خطأٍ ناتجٍ عن القيمة null هو استخدام معامل الاستدعاء الآمن .? بالشكل:

b?.length

والذي يُعيد null إن كان المتغيِّر b يحتوي على القيمة null ويعيد b.length إن لم يكن كذلك، ويكون نوع التعبير Int?‎. يُفيد استخدام الاستدعاء الآمن في حالة السلاسل (chains)؛ فإن كان الموظَّف Bob مثلًا يتبع لقسمٍ (department) ما (وربما لا يتبع)، والذي بدوره يحتوي على موظفٍ آخر باعتباره مديرًا للقسم (head)، وبالتالي فإنه للحصول على اسم رئيس القسم الذي يعمل به الموظَّف Bob (إن وُجد) تكتب الشيفرة:

bob?.department?.head?.name

وستعيد مثل هذه السلسلة القيمة null إن كانت إحدى هذه الخاصّيّات بالقيمة null (أي لا تحتوي على أيّ قيمة). كما ويمكن أيضًا استخدام معامل الاستدعاء الآمن مع الكلمة المفتاحيّة let لضمان تنفيذ العمليّة على القيم غير الفارغة (non-null) فقط، مثل:

val listWithNulls: List<String?> = listOf("A", null)
for (item in listWithNulls) {
     item?.let { println(it) } // سيطبع A 
                               // ويتجاهل القيمة null
}

كما ويمكن أن يكون الاستدعاء الآمن على الجانب الأيسر من الإسناد، وبالتالي فإن كان أحد المستقبلين (receivers) في السلسلة بالقيمة null فلن تتمّ عملية الإسناد ولن يُحسب التعبير (expression) بالجانب الأيمن أبدًا، مثل:

// لن تُستدعى الدالة إن كانت
// person أو person.department
// بقيمة null
person?.department?.head = managersPool.getManager()

المعامل Elvis‏ (‎?:‎)

عند وجود مرجعيّة nullable ولتكن r فيمكن القول حينئذٍ بأنّه إن لم تكن r بالقيمة null فيمكن استخدامها، وإلّا فيجب استخدام قيمةٍ غير فارغةٍ (non-null) ولتكن x، كما في الشيفرة الآتية:

val l: Int = if (b != null) b.length else -1

ويمكن التعبير عن ذلك بصيغةٍ أخرى عبر استخدام المعامل ‎?:‎ كما في الشيفرة:

val l = b?.length ?: -1

فإن لم يكن التعبير على يسار المعامل ‎?:‎ بالقيمة null فسيُعيده المعامل، أمّا إن كان بالقيمة null فسيُعيد التعبيرَ في الجانب الأيمن، والذي سيُحسَب فقط عندما يكون الجانب الأيسر بالقيمة null. وبما أنّ التعليمتين return و throw تُعدَّان تعابيرًا (expressions) في لغة Kotlin فيمكن استخدامهما على الجانب الأيمن من المعامل ‎?:‎ ، وهذا يساعد عند التحقُّق من وسائط (arguments) الدالة كما في الشيفرة:

fun foo(node: Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllegalArgumentException("name expected")
    // ...
}

المعامل !!

وهو الطريقة الثالثة لتجنُّب الاستثناء NPE (المشروح سابقًا)، حيث يحوِّل المعامل !! أيَّ قيمةٍ إلى النوع non-null وسيرمى (throw) استثناءٌ إن كانت القيمة null، يستخدم المعامل بالصيغة b!!‎ ، وهذا يعيد القيمة غير الفارغة من المتغيِّر b وهي السلسلة النصّيّة String، أو سيرمى الاستثناء NPE إن كان المتغيِّر b بالقيمة null، مثل:

val l = b!!.length

وبالتالي عند الحاجة لوجود الاستثناء NPE فهو متاحٌ.

التحويل الآمن (Safe Cast)

قد تحدث بعض الاستثناءات من النوع ClassCastException إن لم يكن الكائن (object) من النوع الهدف (target type)، ويُستخدَم التحويل الآمن حينئذٍ والذي يُعيد القيمة null إن لم تكن محاولة التحويل ناجحةً، وتكون الصيغة بالشكل:

val aInt: Int? = a as? Int

المجموعات (Collections) من النوع Nullable

إن كانت المجموعة تحتوي على عددٍ من العناصر من النوع nullable فبالإمكان استخدام الدالة filterNotNull لترشيح (filter) العناصر من النوع non-null كما في الشيفرة:

val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()

مصادر