Kotlin: الغوص العميق في أمان البطلان في كوتلن

آخر تحديث: 2026-08-26

أمان البطلان في كوتلن أكثر من مجرد سكر نحوي ? — إنه تصميم جذري لنظام الأنواع يحوّل NullPointerException من قنبلة وقت التشغيل إلى قيد وقت الترجمة. تشارلي يعبر ثلاث طبقات من المراجع القابلة للبطلان بأمان في سطر واحد.

1. ما ستتعلمه


2. قصة مهندس حقيقية

(1) نقطة الألم: NullPointerException — خطيرة جافا بمليار دولار

يتعطل OrderProcessor لتشارلي 15 مرة شهرياً بسبب NPE. الحادثة الكلاسيكية: استدعاء متسلسل مثل order.getCustomer().getAddress().getCity() — أي حلقة فارغة تنفجر، ومترجم جافا لا يحذر أبداً.

(2) حل الأنواع الآمنة للبطلان في كوتلن

KOTLIN
// جافا: قنبلة وقت التشغيل
String city = order.getCustomer().getAddress().getCity();  // NPE محتمل!

// كوتلن: أمان وقت الترجمة
val city: String = order.customer?.address?.city ?: "Unknown"  // آمن!

نظام الأنواع وقت الترجمة يقلل نسبة NPE بنسبة 95% — في مشاريع كوتلن، تحدث NPE بشكل أساسي فقط عند حدود التشغيل البيني مع جافا.


3. أساسيات الأنواع القابلة للبطلان

(1) T و T? نوعان مختلفان

KOTLIN
// غير قابل للبطلان: لا يمكنه حمل null
val orderId: String = "ORD-001"
// orderId = null  // خطأ ترجمة

// قابل للبطلان: يجب التصريح بـ ?
val customerName: String? = null
customerName = "Alice"  // صحيح

(2) الأنواع القابلة للبطلان لا يمكن استخدامها مباشرة

KOTLIN
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) العوامل الأربعة

KOTLIN
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) شجرة قرار العوامل

100%
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) مراجع قابلة للبطلان متعددة الطبقات

KOTLIN
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 المتسلسلة

KOTLIN
// 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 ليس مجرد قيم افتراضية — يمكنه دمج استراتيجيات متعددة:

KOTLIN
// الاستراتيجية 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!.

JAVA
// كود جافا - نوع الإرجاع غير معروف البطلان
public class JavaOrderService {
    public Order findOrder(String id) {  // يمكن أن يكون null!
        return orderMap.get(id);
    }
}
KOTLIN
// كوتلن: النوع المنصّ - أنت تقرر!
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

JAVA
// جافا مع تعليقات
public class JavaOrderService {
    @Nullable
    public Order findOrder(String id) { return null; }

    @NotNull
    public List<Order> findAll() { return orders; }
}
KOTLIN
// كوتلن الآن يعرف قابلية البطلان
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

KOTLIN
// ============================================
// 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}")
        }
    }
}

المخرجات:

TEXT 📖 للعرض فقط
=== 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

KOTLIN
// نوع غير 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

**المخرجات:

TEXT 📖 للعرض فقط
Nickname length: 0  (لو nickname = ""; null إذا null)
Forced: null  (لو فرض null)
Length: null

▶ مثال: Elvis operator

KOTLIN
// ?: قيمة افتراضية عند 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}")
}

**المخرجات:

TEXT 📖 للعرض فقط
Name: Default Name
Address: NYC
Length 'hello': 5
Length null: -1
Required: world

▶ مثال: safe calls و let/run/also/apply

KOTLIN
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}")

**المخرجات:

TEXT 📖 للعرض فقط
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

KOTLIN
// 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))

**المخرجات:

TEXT 📖 للعرض فقط
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

KOTLIN
// سلسلة عمليات آمنة
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")

**المخرجات:

TEXT 📖 للعرض فقط
City: NYC, ZIP: 10001
Version unknown
Updated: [1, 2, 3, 4, 5]

▶ مثال: !! و as!

KOTLIN
// !! - تأكيد 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")

**المخرجات:

TEXT 📖 للعرض فقط
Non-null: Hello
Cast: Hello
?: ALICE
!!: ALICE

▶ مثال: lateinit للأخصائص

KOTLIN
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) }

**المخرجات:

TEXT 📖 للعرض فقط
Connected to localhost:5432
Executing 'SELECT * FROM users' on Connected to localhost:5432
Port: 8080

❓ أسئلة شائعة

س هل كوتلن يلغي NPE تماماً؟
ج لا. كوتلن يقلل NPE بشكل كبير، لكنه لا يزال يمكن أن يحدث في هذه السيناريوهات: فشل تأكيد !!، أنواع المنصة في التشغيل البيني مع جافا، throw صريح، و lateinit غير المهيأ.
س هل يجب أن أستخدم !!؟
ج تجنبه قدر الإمكان. !! يعني "أنا متأكد أن هذا ليس null، تعطل خلاف ذلك" — لكن حكمك قد يكون خاطئاً. فضّل ?: مع قيمة افتراضية أو استثناء ذي معنى.
س ما هو بالضبط النوع المنصّ T!؟
ج الأنواع المنصّة توجد فقط عندما يستدعي كوتلن كود جافا، وتعني "كوتلن لا يعرف ما إذا كان قابلاً للبطلان." تختار أن تعامله كـ T? (آمن) أو T (مخاطر).
س ما الفرق بين if (x != null) و x?.let؟
ج متكافئان وظيفياً، لكن let ينشئ نطاقاً جديداً حيث it غير قابل للتغيير، بينما if يستخدم التحويل الذكي والمتغير قابل للتغيير. استخدم if لعمليات الفحص البسيطة، let عند الحاجة لعزل النطاق.
س كيف أرى تعليقات أمان البطلان من مكتبات جافا؟
ج العديد من مكتبات جافا الحديثة (Spring، Android SDK) أضافت بالفعل @Nullable / @NotNull. للمكتبات بدونها، يعامل كوتلن جميع الإرجاعات كأنواع منصّة.
س ما الفرق بين ?: throw و requireNotNull؟
ج متكافئان وظيفياً. requireNotNull أكثر دلالية (يعبر عن نية التحقق من المعاملات)، بينما ?: throw أكثر مرونة (استثناء مخصص). فضّل requireNotNull للتحقق من المعاملات.

📖 ملخص


📝 تمارين

  1. مبتدئ (⭐): عرّف متغير String? قابل للبطلان واحصل على طوله باستخدام ?. و ?: والتحويل الذكي. تلميح: str?.length، str?.length ?: 0، if (str != null)
  2. متوسط (⭐⭐): استخدم سلسلة استدعاء آمنة للحصول على اسم مدينة من Order، مع إرجاع "Unknown" إذا كان فارغاً. تلميح: order?.customer?.address?.city ?: "Unknown"
  3. متقدم (⭐⭐⭐): صمّم نظام تحقق بـ requireNotNull + Elvis يتحقق من جميع الحقول القابلة للبطلان في طلب، مع طرح استثناءات أعمال ذات معنى للقيم الفارغة. تلميح: ادمج requireNotNull مع أنواع استثناءات مخصصة

← السابق | التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%