الفرق بين المراجعتين لصفحة: «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> في الشيفرات، إذ إنّ أحد الأخطاء الأكثر شيوعًا في | يهدف نظام الأنواع في 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، فلا يمكن مثلًا | ويميّز نظام الأنواع في 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>وللسماح بذلك يجب التصريح عن | </syntaxhighlight>وللسماح بذلك يجب التصريح عن المتغيِّر بالنوع <code>String?</code> (أي النوع nullable String)، كما في الشيفرة:<syntaxhighlight lang="kotlin"> | ||
var b: String? = "abc" | var b: String? = "abc" | ||
b = null // يُسمح بهذا | b = null // يُسمح بهذا | ||
</syntaxhighlight>وعند استدعاء تابعٍ (method) أو الوصول إلى خاصّيّةٍ في | </syntaxhighlight>وعند استدعاء تابعٍ (method) أو الوصول إلى خاصّيّةٍ في المتغيِّر <code>a</code> فمن المؤكَّد عدم حدوث استثناءٍ من النوع NPE (المشروح سابقًا)، وبالتالي فمن الآمن كتابة الشيفرة بالشكل:<syntaxhighlight lang="kotlin"> | ||
val l = a.length | val l = a.length | ||
</syntaxhighlight>أما عند الوصول إلى نفس الخاصّيّة في | </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"> | ||
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>وهذا يصلح فقط عندما يكون | </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> إن كان | </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> ، وهذا يساعد عند التحقُّق من | وبما أنّ التعليمتين <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 | وهو الطريقة الثالثة لتجنُّب الاستثناء 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) كما في الحالات الآتية:
- تمرير الكلمة المفتاحيّة
this
غير المُهيَّأة (uninitialized) والمُتاحة في الباني (constructor) لاستخدامها في مكانٍ آخر - استدعاء الباني في صنفٍ أعلى (superclass) لعنصرٍ مفتوحٍ (open) يكون تعريف استخدامه (implementation) في الصنف المشتقّ (derived class) بحالةٍ غير مُهيَّأة (uninitialized)
- تمرير الكلمة المفتاحيّة
- أثناء توافقية التبديل مع لغة 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()