Kotlin: الغوص العميق في أمان البطلان في كوتلن
آخر تحديث: 2026-08-26
أمان البطلان في كوتلن أكثر من مجرد سكر نحوي ? — إنه تصميم جذري لنظام الأنواع يحوّل NullPointerException من قنبلة وقت التشغيل إلى قيد وقت الترجمة. تشارلي يعبر ثلاث طبقات من المراجع القابلة للبطلان بأمان في سطر واحد.
1. ما ستتعلمه
- الأنواع القابلة للبطلان
T?مقابل الأنواع غير الباطلةT: ضمانات وقت الترجمة - الاستدعاء الآمن
?.، Elvis?:، تأكيد عدم البطلان!!، وسلاسلlet - الأنواع المنصّة
T!: المنطقة الرمادية للتشغيل البيني مع جافا - الربط مع تعليقات
@Nullable/@NotNull - تشارلي في العمل: سلاسل آمنة + تحقق معاملات
requireNotNull
2. قصة مهندس حقيقية
(1) نقطة الألم: NullPointerException — خطيرة جافا بمليار دولار
يتعطل OrderProcessor لتشارلي 15 مرة شهرياً بسبب NPE. الحادثة الكلاسيكية: استدعاء متسلسل مثل order.getCustomer().getAddress().getCity() — أي حلقة فارغة تنفجر، ومترجم جافا لا يحذر أبداً.
(2) حل الأنواع الآمنة للبطلان في كوتلن
// جافا: قنبلة وقت التشغيل
String city = order.getCustomer().getAddress().getCity(); // NPE محتمل!
// كوتلن: أمان وقت الترجمة
val city: String = order.customer?.address?.city ?: "Unknown" // آمن!
نظام الأنواع وقت الترجمة يقلل نسبة NPE بنسبة 95% — في مشاريع كوتلن، تحدث NPE بشكل أساسي فقط عند حدود التشغيل البيني مع جافا.
3. أساسيات الأنواع القابلة للبطلان
(1) T و T? نوعان مختلفان
// غير قابل للبطلان: لا يمكنه حمل null
val orderId: String = "ORD-001"
// orderId = null // خطأ ترجمة
// قابل للبطلان: يجب التصريح بـ ?
val customerName: String? = null
customerName = "Alice" // صحيح
(2) الأنواع القابلة للبطلان لا يمكن استخدامها مباشرة
val name: String? = getInput()
// println(name.length) // خطأ: مستقبل قابل للبطلان
// يجب التعامل مع حالة null صراحةً
println(name?.length) // استدعاء آمن: Int?
println(name?.length ?: 0) // Elvis: Int
println(name!!.length) // تأكيد عدم البطلان: Int (يطرح استثناء إذا كان null)
if (name != null) println(name.length) // تحويل ذكي: Int
4. عوامل الأمان من البطلان بالتفصيل
(1) العوامل الأربعة
val order: Order? = fetchOrder()
// 1. الاستدعاء الآمن ?. - يرجع null إذا كان المستقبل فارغاً
val id = order?.id // String?
// 2. Elvis ?: - توفير قيمة افتراضية عند null
val total = order?.total ?: 0.0 // Double
// 3. تأكيد عدم البطلان !! - يطرح NPE إذا كان null
val status = order!!.status // String (خطر - استخدم باعتدال)
// 4. سلسلة let - تنفيذ الكتلة فقط إذا لم يكن فارغاً
order?.let {
println("Processing ${it.id}") // 'it' مضمون عدم البطلان
}
(2) شجرة قرار العوامل
flowchart TD
A[قيمة قابلة للبطلان] --> B{تحتاج نتيجة غير باطلة؟}
B -->|لا| C["?." استدعاء آمن]
B -->|نعم| D{لديك قيمة افتراضية؟}
D -->|نعم| E["?: عامل Elvis"]
D -->|لا| F{متأكد من عدم البطلان؟}
F -->|نعم| G["!! تأكيد"]
F -->|لا| H{تحتاج تنفيذ كتلة؟}
H -->|نعم| I["?.let {}"]
H -->|لا| J["if (x != null) تحويل ذكي"]
(3) جدول مقارنة العوامل
| العامل | نوع الإرجاع | سلوك البطلان | تكرار الاستخدام | مستوى الأمان |
|---|---|---|---|---|
?. |
T? |
يرجع null | ⭐⭐⭐⭐⭐ | آمن |
?: |
T |
يستخدم القيمة الافتراضية | ⭐⭐⭐⭐ | آمن |
!! |
T |
يطرح NPE | ⭐ (تجنب) | خطر |
?.let |
متغير | يتخطى التنفيذ | ⭐⭐⭐ | آمن |
| تحويل ذكي | T |
ضمان وقت الترجمة | ⭐⭐⭐⭐ | آمن |
5. سلاسل الاستدعاء الآمنة
(1) مراجع قابلة للبطلان متعددة الطبقات
data class Address(val city: String, val country: String)
data class Customer(val name: String, val address: Address?)
data class Order(val id: String, val customer: Customer?)
// سلسلة آمنة عبر مراجع قابلة للبطلان متعددة
val order: Order? = fetchOrder()
val city = order?.customer?.address?.city ?: "Unknown"
val country = order?.customer?.address?.country ?: "N/A"
(2) let المتسلسلة
// let المتداخلة يمكن أن تكون صعبة القراءة
order?.let { o ->
o.customer?.let { c ->
c.address?.let { a ->
println("${a.city}, ${a.country}")
}
}
}
// أفضل: استخدم سلسلة استدعاء آمنة
val addressInfo = order?.customer?.address?.let {
"${it.city}, ${it.country}"
} ?: "Address unavailable"
6. أنماط استراتيجية عامل Elvis
Elvis ليس مجرد قيم افتراضية — يمكنه دمج استراتيجيات متعددة:
// الاستراتيجية 1: قيمة افتراضية
val name = customer?.name ?: "Anonymous"
// الاستراتيجية 2: طرح استثناء
val order = findOrder(id) ?: throw OrderNotFoundException(id)
// الاستراتيجية 3: إرجاع مبكر
fun process(order: Order?) {
val confirmed = order ?: return
// confirmed تم تحويله ذكياً إلى Order غير باطل
println(confirmed.id)
}
// الاستراتيجية 4: requireNotNull للتحقق من المعاملات
fun createInvoice(orderId: String, customer: Customer?) {
val cust = requireNotNull(customer) { "Customer is required for invoice" }
// cust تم تحويله ذكياً إلى Customer غير باطل
}
(1) مقارنة استراتيجيات Elvis
| الاستراتيجية | الصياغة | حالة الاستخدام |
|---|---|---|
| قيمة افتراضية | x ?: default |
قيمة بديلة مقبولة |
| طرح استثناء | x ?: throw Ex() |
null هي حالة خطأ |
| إرجاع مبكر | x ?: return |
null يمكنه تخطي المعالجة |
| requireNotNull | requireNotNull(x) |
تحقق من المعاملات |
| تسجيل وقيمة افتراضية | x ?: run { log(); default } |
تسجيل + بديل |
7. الأنواع المنصّة والتشغيل البيني مع جافا
(1) النوع المنصّ T!
عند استدعاء كود جافا، لا يمكن لكوتلن تحديد قابلية البطلان للإرجاع، مما يُدخل النوع المنصّ T!.
// كود جافا - نوع الإرجاع غير معروف البطلان
public class JavaOrderService {
public Order findOrder(String id) { // يمكن أن يكون null!
return orderMap.get(id);
}
}
// كوتلن: النوع المنصّ - أنت تقرر!
val order = javaService.findOrder("ORD-001")
// الخيار 1: معاملة كقابل للبطلان (آمن)
val safeOrder: Order? = javaService.findOrder("ORD-001")
// الخيار 2: معاملة كغير قابل للبطلان (مخاطر)
val riskyOrder: Order = javaService.findOrder("ORD-001") // NPE إذا كان null!
(2) الربط بـ @Nullable / @NotNull
// جافا مع تعليقات
public class JavaOrderService {
@Nullable
public Order findOrder(String id) { return null; }
@NotNull
public List<Order> findAll() { return orders; }
}
// كوتلن الآن يعرف قابلية البطلان
val order: Order? = javaService.findOrder("ORD-001") // معروف القابلية للبطلان
val all: List<Order> = javaService.findAll() // معروف عدم البطلان
(3) استراتيجيات أمان البطلان في التشغيل البيني مع جافا
| الاستراتيجية | النهج | مستوى الأمان |
|---|---|---|
| معاملة الكل كقابل للبطلان | val x: T? = javaMethod() |
⭐⭐⭐⭐⭐ |
| إضافة تعليقات | جانب جافا: @Nullable / @NotNull |
⭐⭐⭐⭐ |
| JSR-305 | @ParametersAreNonnullByDefault |
⭐⭐⭐ |
| افتراض عدم البطلان | val x: T = javaMethod() |
⭐ (خطر) |
8. مثال كامل: سلاسل آمنة في OrderProcessor
// ============================================
// OrderProcessor - عمليات آمنة للبطلان
// الميزة: سلاسل آمنة، استراتيجيات Elvis، requireNotNull
// ============================================
data class Address(val street: String, val city: String, val country: String)
data class Customer(val name: String, val email: String?, val address: Address?)
data class Order(val id: String, val total: Double, val customer: Customer?, val status: String)
class OrderNotFoundException(id: String) : RuntimeException("Order not found: $id")
// مساعد سلسلة آمنة
fun Order.getCity(): String = customer?.address?.city ?: "Unknown"
fun Order.getCountry(): String = customer?.address?.country ?: "N/A"
fun Order.getDisplayEmail(): String = customer?.email ?: "no-email"
// استراتيجيات Elvis
fun findOrderOrFail(id: String, orders: List<Order>): Order =
orders.find { it.id == id } ?: throw OrderNotFoundException(id)
fun processOrder(order: Order?) {
val confirmed = order ?: run {
println("Skipping: null order")
return
}
println("Processing: ${confirmed.id}")
}
fun validateOrder(order: Order): Order {
requireNotNull(order.customer) { "Customer is required for order ${order.id}" }
require(order.total > 0) { "Total must be positive" }
return order
}
fun main() {
val orders = listOf(
Order("ORD-001", 299.99, Customer("Alice", "alice@example.com",
Address("123 Main St", "New York", "US")), "CONFIRMED"),
Order("ORD-002", 1_500.00, Customer("Bob", null, null), "PENDING"),
Order("ORD-003", 8_900.00, null, "SHIPPED"),
Order("ORD-004", 45.50, Customer("Charlie", "charlie@example.com",
Address("456 Oak Ave", "London", "UK")), "CONFIRMED")
)
// سلاسل آمنة
println("=== Order Details ===")
orders.forEach { order ->
println("${order.id}: ${order.getCity()}, ${order.getCountry()} | Email: ${order.getDisplayEmail()}")
}
// Elvis: ابحث أو أفشل
println("\n=== Find Orders ===")
println("ORD-001: ${findOrderOrFail("ORD-001", orders).total} USD")
try {
findOrderOrFail("ORD-999", orders)
} catch (e: OrderNotFoundException) {
println("Error: ${e.message}")
}
// Elvis: إرجاع مبكر
println("\n=== Process Orders ===")
processOrder(orders[0]) // يعالج
processOrder(null) // يتخطى
// تحقق requireNotNull
println("\n=== Validation ===")
orders.forEach { order ->
try {
validateOrder(order)
println("${order.id}: Valid")
} catch (e: IllegalArgumentException) {
println("${order.id}: Invalid - ${e.message}")
} catch (e: IllegalStateException) {
println("${order.id}: Invalid - ${e.message}")
}
}
}
المخرجات:
=== Order Details ===
ORD-001: New York, US | Email: alice@example.com
ORD-002: Unknown, N/A | Email: no-email
ORD-003: Unknown, N/A | Email: no-email
ORD-004: London, UK | Email: charlie@example.com
=== Find Orders ===
ORD-001: 299.99 USD
Error: Order not found: ORD-999
=== Process Orders ===
Processing: ORD-001
Skipping: null order
=== Validation ===
ORD-001: Valid
ORD-002: Valid
ORD-003: Invalid - Customer is required for order ORD-003
ORD-004: Valid
9. أمثلة عملية سريعة
▶ مثال: أنواع nullable وغير nullable
// نوع غير nullable - لا يمكن تعيين null
val name: String = "Alice"
// name = null // خطأ ترجمة
// نوع nullable - يمكن أن يكون null
val nickname: String? = null
val maybeAge: Int? = 25
// التحقق من null
if (nickname != null) {
println("Nickname length: ${nickname.length}")
}
// مع !! - تأكيد عدم null (خطير)
val forced: String = nickname!!
println("Forced: $forced")
// مع ? - استدعاء آمن
val length: Int? = nickname?.length
println("Length: $length") // null
**المخرجات:
Nickname length: 0 (لو nickname = ""; null إذا null)
Forced: null (لو فرض null)
Length: null
▶ مثال: Elvis operator
// ?: قيمة افتراضية عند null
val userInput: String? = null
val name = userInput ?: "Default Name"
println("Name: $name")
// سلسلة Elvis operators
val address: String? = null
val city: String? = "NYC"
val country: String? = null
val finalAddress = address ?: city ?: country ?: "Unknown"
println("Address: $finalAddress")
// Elvis مع return/throw
fun getLength(s: String?): Int = s?.length ?: return -1
fun requireNotEmpty(s: String?): String = s ?: throw IllegalArgumentException("Empty")
println("Length 'hello': ${getLength("hello")}")
println("Length null: ${getLength(null)}")
val valid = "world"
try {
println("Required: ${requireNotEmpty(valid)}")
} catch (e: Exception) {
println("Error: ${e.message}")
}
**المخرجات:
Name: Default Name
Address: NYC
Length 'hello': 5
Length null: -1
Required: world
▶ مثال: safe calls و let/run/also/apply
val user: Map<String, Any?> = mapOf(
"name" to "Alice",
"email" to "alice@example.com",
"age" to 30
)
// Safe call chain
val emailLength: Int? = user["email"]?.toString()?.length
println("Email length: $emailLength")
// let - تنفيذ عند non-null
val name: String? = user["name"] as? String
val greeting = name?.let {
"Hello, $it!"
} ?: "Hello, Guest!"
println(greeting)
// let مع it غير قابل للتغيير
val numbers = listOf(1, 2, 3, 4, 5)
numbers.firstOrNull { it > 3 }?.let { first ->
println("First > 3: $first, doubled: ${first * 2}")
}
// run - تنفيذ وإرجاع قيمة
val config: String? = "production"
val configResult = config?.run {
"Running in $this mode"
} ?: "No config"
println("Config: $configResult")
// also - تنفيذ وإرجاع نفس الكائن
val items = mutableListOf(1, 2, 3)
val result = items.also {
it.add(4)
println("Items after add: $it")
}
println("Result same as items: ${result === items}")
**المخرجات:
Email length: 17
Hello, Alice!
First > 3: 4, doubled: 8
Config: Running in production mode
Items after add: [1, 2, 3, 4]
Result same as items: true
▶ مثال: smart cast
// smart cast - المُترجم يتتبع الفحوصات
fun describe(obj: Any?): String {
if (obj == null) return "null"
// obj أصبح Any (غير nullable) هنا
return when (obj) {
is String -> "String of length ${obj.length}: \"$obj\""
is Int -> if (obj >= 0) "Positive int: $obj" else "Negative int: $obj"
is List<*> -> "List of ${obj.size} items"
is Map<*, *> -> "Map of ${obj.size} entries"
else -> "Unknown: ${obj::class.simpleName}"
}
}
println(describe(null))
println(describe("Hello"))
println(describe(42))
println(describe(-5))
println(describe(listOf(1, 2, 3)))
println(describe(3.14))
// smart cast بعد فحص null
fun processValue(v: Any?): String {
if (v !is String) return "Not a string"
// v أصبح String هنا
return "Got string of length ${v.length}: $v"
}
println(processValue("test"))
println(processValue(42))
**المخرجات:
null
String of length 5: "Hello"
Positive int: 42
Negative int: -5
List of 3 items
Unknown: double
Got string of length 4: test
Not a string
▶ مثال: let لإجراء عمليات على non-null
// سلسلة عمليات آمنة
data class User(val name: String, val address: Address?)
data class Address(val city: String, val zip: String?)
val user = User("Alice", Address("NYC", "10001"))
val cityInfo = user.address?.let { addr ->
"City: ${addr.city}, ZIP: ${addr.zip ?: "Unknown"}"
} ?: "No address"
println(cityInfo)
// سلسلة nested let
val deepValue: Map<String, Map<String, String?>> = mapOf(
"config" to mapOf("name" to "MyApp", "version" to null)
)
val version = deepValue["config"]?.get("version")?.let { v ->
"Version: $v"
} ?: "Version unknown"
println(version)
// also لتنفيذ عملية مع إرجاع الكائن
val numbers = mutableListOf(1, 2, 3)
val updated = numbers.also {
it.add(4)
it.add(5)
it.sort()
}
println("Updated: $updated")
**المخرجات:
City: NYC, ZIP: 10001
Version unknown
Updated: [1, 2, 3, 4, 5]
▶ مثال: !! و as!
// !! - تأكيد non-null
val nullable: String? = "Hello"
val nonNull: String = nullable!!
println("Non-null: $nonNull")
// try { val x: String = (null as String?)!! } catch (e: Exception) { println("Error: ${e.message}") }
// as! - تأكيد نوع
val obj: Any = "Hello"
val str: String = obj as String
println("Cast: $str")
// try { val num: Int = obj as Int } catch (e: Exception) { println("Cast error: ${e.message}") }
// الجمع: chain of !!
// val reallyNull: String? = null
// val forced: String = reallyNull!! // يلقي NullPointerException
// مقارنة !! مع ?.let
val maybeName: String? = "Alice"
val upper1 = maybeName?.uppercase() ?: "DEFAULT"
val upper2 = maybeName!!.uppercase()
println("?: $upper1")
println("!!: $upper2")
**المخرجات:
Non-null: Hello
Cast: Hello
?: ALICE
!!: ALICE
▶ مثال: lateinit للأخصائص
class DatabaseConnection {
lateinit var connection: String
fun connect(url: String) {
// محاكاة اتصال
connection = "Connected to $url"
println(connection)
}
fun query(sql: String) {
// إذا لم تُستدعى connect() قبل query() → استثناء
println("Executing '$sql' on $connection")
}
}
val db = DatabaseConnection()
db.connect("localhost:5432")
db.query("SELECT * FROM users")
// lateinit لا يعمل مع الأنواع البدائية
class Settings {
// lateinit var port: Int // خطأ ترجمة!
// Int هو primitive - له قيمة افتراضية 0
}
// استخدام nullable للبدائيات
class SafeSettings {
var port: Int? = null
fun setPort(p: Int) { port = p }
fun getPort(): Int = port ?: throw IllegalStateException("Port not set")
}
val settings = SafeSettings()
settings.setPort(8080)
println("Port: ${settings.port}")
// try { println(SafeSettings().getPort()) } catch (e: Exception) { println(e.message) }
**المخرجات:
Connected to localhost:5432
Executing 'SELECT * FROM users' on Connected to localhost:5432
Port: 8080
❓ أسئلة شائعة
!!، أنواع المنصة في التشغيل البيني مع جافا، throw صريح، و lateinit غير المهيأ.!!؟!! يعني "أنا متأكد أن هذا ليس null، تعطل خلاف ذلك" — لكن حكمك قد يكون خاطئاً. فضّل ?: مع قيمة افتراضية أو استثناء ذي معنى.T? (آمن) أو T (مخاطر).if (x != null) و x?.let؟let ينشئ نطاقاً جديداً حيث it غير قابل للتغيير، بينما if يستخدم التحويل الذكي والمتغير قابل للتغيير. استخدم if لعمليات الفحص البسيطة، let عند الحاجة لعزل النطاق.@Nullable / @NotNull. للمكتبات بدونها، يعامل كوتلن جميع الإرجاعات كأنواع منصّة.?: throw و requireNotNull؟requireNotNull أكثر دلالية (يعبر عن نية التحقق من المعاملات)، بينما ?: throw أكثر مرونة (استثناء مخصص). فضّل requireNotNull للتحقق من المعاملات.📖 ملخص
TوT?نوعان مختلفان — المترجم يفرض أمان البطلان وقت الترجمة- أربعة عوامل أساسية:
?.(استدعاء آمن)،?:(Elvis)،!!(تأكيد)،?.let(تنفيذ مشروط) - سلاسل الاستدعاء الآمنة
a?.b?.c?.dتعبر طبقات متعددة من المراجع القابلة للبطلان في سطر واحد - استراتيجيات Elvis: قيمة افتراضية / طرح استثناء / إرجاع مبكر / requireNotNull
- الأنواع المنصّة
T!هي المنطقة الرمادية للتشغيل البيني مع جافا — فضّل معاملتها كقابلة للبطلان !!هو الملاذ الأخير؛ فضّل نهج Elvis الآمن أو?.let
📝 تمارين
- مبتدئ (⭐): عرّف متغير
String?قابل للبطلان واحصل على طوله باستخدام?.و?:والتحويل الذكي. تلميح:str?.length،str?.length ?: 0،if (str != null) - متوسط (⭐⭐): استخدم سلسلة استدعاء آمنة للحصول على اسم مدينة من
Order، مع إرجاع "Unknown" إذا كان فارغاً. تلميح:order?.customer?.address?.city ?: "Unknown" - متقدم (⭐⭐⭐): صمّم نظام تحقق بـ
requireNotNull+ Elvis يتحقق من جميع الحقول القابلة للبطلان في طلب، مع طرح استثناءات أعمال ذات معنى للقيم الفارغة. تلميح: ادمجrequireNotNullمع أنواع استثناءات مخصصة