Kotlin: شرح دوال الامتداد في كوتلن

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

دوال الامتداد تتيح لتشارلي إضافة isHighValue() إلى Order بدون تعديل كود مصدره — ليست سحراً، بل مجرد سكر نحوي للإرسال الثابت وقت الترجمة، لكن قيمتها العملية لا تُنكر.

1. ما ستتعلمه


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

(1) نقطة الألم: تكاثر أصناف Util

مشروع تشارلي في جافا يحتوي على 15 صنف Util: OrderUtil، StringUtil، DateUtil... كل منها كومة من التوابع الثابتة، مما يجعل الاستدعاءات مثل OrderUtil.isHighValue(order) مطولة وغير بديهية.

(2) حل دوال الامتداد

KOTLIN
// بدلاً من OrderUtil.isHighValue(order)
fun Order.isHighValue() = total > 10_000

// الآن استدعِها مثل تابع عضو!
if (order.isHighValue()) {
    routeToVipPipeline()
}

دوال الامتداد تحوّل استدعاءات API من Util.method(obj) إلى obj.method()، قفزة نوعية في مقروئية الكود.


3. أساسيات دوال الامتداد

(1) الصياغة الأساسية

KOTLIN
// توسيع String بتنسيق معرف الطلب
fun String.toOrderId() = "ORD-$this"

// توسيع Double بتنسيق الدولار
fun Double.toUSD() = "\$$this USD"

// توسيع List بمنطق الأعمال
fun List<Order>.totalRevenue() = this.sumOf { it.total }

// الاستخدام
println("001".toOrderId())          // ORD-001
println(299.99.toUSD())             // $299.99 USD
println(orders.totalRevenue())      // 12345.67

(2) دوال الامتداد مع الأدوية

KOTLIN
// امتداد عام
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null

// امتداد مع قيد نوع
fun <T : Comparable<T>> List<T>.secondLargest(): T? {
    return this.sortedDescending().secondOrNull()
}

(3) امتدادات المستقبل القابل للبطلان

KOTLIN
// توسيع نوع قابل للبطلان - التعامل مع null بلباقة
fun String?.orDefault(default: String = "N/A"): String = this ?: default

val name: String? = null
println(name.orDefault("Unknown"))  // Unknown
println("Alice".orDefault())        // Alice

4. خصائص الامتداد

KOTLIN
// خاصية امتداد للقراءة فقط
val BigDecimal.inMillions: Double
    get() = this.toDouble() / 1_000_000

val String.isOrderId: Boolean
    get() = startsWith("ORD-")

// الاستخدام
val revenue = BigDecimal("2_500_000")
println("${revenue.inMillions}M")  // 2.5M

println("ORD-001".isOrderId)  // true
println("ABC-001".isOrderId)  // false

// ملاحظة: خصائص الامتداد لا يمكن أن يكون لها حقول داعمة
// خصائص امتداد var ممكنة لكنها تتطلب setter صريح

(1) خصائص الامتداد مقابل خصائص العضو

البُعد خاصية العضو خاصية الامتداد
موقع التعريف داخل الصنف خارج الصنف
الحقل الداعم يمتلك واحداً لا يوجد
تخزين الحالة مخزن في الكائن لا يمكنه التخزين
القابلية للتغيير يمكن أن تكون var var محسوبة فقط
مستوى الوصول خاضع للرؤية يصل فقط للواجهة العامة

5. الإرسال الثابت — جوهر الامتدادات

دوال الامتداد تُرسل بشكل ثابت: أي تطبيق يُستدعى يُحدد بـ النوع المصرح به، وليس النوع وقت التشغيل.

(1) مثال على الإرسال الثابت

KOTLIN
open class Order(val id: String, val total: Double)
class BulkOrder(id: String, total: Double, val minQty: Int) : Order(id, total)

// امتداد على Order
fun Order.summary() = "Order $id: \$$total USD"

// امتداد على BulkOrder
fun BulkOrder.summary() = "Bulk $id: \$$total USD (min: $minQty)"

fun printSummary(order: Order) {
    println(order.summary())  // يستدعي دائماً Order.summary()!
}

val bulk = BulkOrder("ORD-001", 5_000.0, 100)
printSummary(bulk)       // "Order ORD-001: $5000.0 USD" - ليس نسخة BulkOrder!
bulk.summary()           // "Bulk ORD-001: $5000.0 USD (min: 100)" - الاستدعاء المباشر صحيح

(2) مخطط تسلسل الإرسال الثابت

100%
sequenceDiagram
    participant Caller
    participant Order
    participant BulkOrder

    Caller->>Order: order.summary() (النوع المصرح: Order)
    Note over Order: يُحل وقت الترجمة
    Order-->>Caller: نتيجة Order.summary()

    Caller->>BulkOrder: bulk.summary() (النوع المصرح: BulkOrder)
    Note over BulkOrder: يُحل وقت الترجمة
    BulkOrder-->>Caller: نتيجة BulkOrder.summary()

(3) الإرسال الثابت مقابل الإرسال الافتراضي

البُعد تابع العضو (الإرسال الافتراضي) دالة الامتداد (الإرسال الثابت)
وقت الحل وقت التشغيل وقت الترجمة
الأساس النوع الفعلي النوع المصرح
تعدد الأشكال مدعوم غير مدعوم
التجاوز الصنف الفرعي يمكنه التجاوز لا يمكن التجاوز
الميزة مرونة ديناميكية آمن ومتوقع

6. التحكم في النطاق

(1) امتدادات المستوى الأعلى

KOTLIN
// الملف: OrderExtensions.kt
package com.order.extensions

fun Order.isHighValue() = total > 10_000
fun List<Order>.totalRevenue() = sumOf { it.total }

(2) امتدادات عضو الصنف

KOTLIN
class OrderService {
    // امتداد معرّف داخل صنف - مرئي فقط داخل هذا الصنف
    fun Order.needsReview(): Boolean = total > 5_000 && status == "PENDING"

    fun process(order: Order) {
        if (order.needsReview()) {  // يمكن الوصول هنا
            routeToReview(order)
        }
    }
}

// order.needsReview()  // خطأ: غير قابل للوصول خارج OrderService

(3) مقارنة نطاق الامتداد

موقع التعريف الرؤية حالة الاستخدام
المستوى الأعلى (مستوى الملف) على مستوى المشروع (بعد الاستيراد) امتدادات الأدوات العامة
داخل عضو صنف داخل الصنف فقط امتدادات مرتبطة بحالة الصنف
نفس الملف نفس الملف فقط امتدادات مساعدة

7. مثال كامل: مجموعة أدوات امتداد OrderProcessor

KOTLIN
// ============================================
// OrderProcessor - مجموعة أدوات الامتداد
// الميزة: منطق الأعمال كدوال امتداد
// ============================================

import java.math.BigDecimal
import java.math.RoundingMode

data class Order(val id: String, val total: Double, val status: String, val customer: String)

// امتدادات String
fun String.toOrderId() = if (startsWith("ORD-")) this else "ORD-$this"
fun String.isOrderId() = matches(Regex("ORD-\\d{3,}"))

// امتدادات Double
fun Double.toUSD(): String = "\$${"%.2f".format(this)} USD"
fun Double.inMillions(): Double = this / 1_000_000

// امتدادات BigDecimal
fun BigDecimal.toUSD(): String = "\$${setScale(2, RoundingMode.HALF_UP)} USD"

// امتدادات Order
fun Order.isHighValue() = total > 10_000
fun Order.isPending() = status == "PENDING"
fun Order.summary() = "$id | ${total.toUSD()} | $status | $customer"

// امتدادات List<Order>
fun List<Order>.totalRevenue() = sumOf { it.total }
fun List<Order>.highValueOrders() = filter { it.isHighValue() }
fun List<Order>.byCustomer() = groupBy { it.customer }
fun List<Order>.revenueByCustomer() = byCustomer().mapValues { (_, orders) -> orders.totalRevenue() }

// امتداد قابل للبطلان
fun String?.orDefault(default: String = "UNKNOWN") = this ?: default

fun main() {
    val orders = listOf(
        Order("ORD-001", 299.99, "CONFIRMED", "Alice"),
        Order("ORD-002", 15_000.00, "PENDING", "Bob"),
        Order("ORD-003", 2_500.00, "SHIPPED", "Charlie"),
        Order("ORD-004", 8_900.00, "CONFIRMED", "Bob"),
        Order("ORD-005", 45.50, "CANCELLED", "Alice")
    )

    // امتداد String
    println("001".toOrderId())       // ORD-001
    println("ORD-001".toOrderId())   // ORD-001
    println("ORD-001".isOrderId())   // true

    // امتدادات Order
    println("\n=== Order Summaries ===")
    orders.forEach { println(it.summary()) }

    println("\n=== High Value Orders ===")
    orders.highValueOrders().forEach { println(it.summary()) }

    // امتدادات List
    println("\n=== Revenue ===")
    println("Total: ${orders.totalRevenue().toUSD()}")
    println("In millions: ${orders.totalRevenue().inMillions()}M")

    println("\n=== Revenue by Customer ===")
    orders.revenueByCustomer().forEach { (customer, revenue) ->
        println("  $customer: ${revenue.toUSD()}")
    }

    // امتداد قابل للبطلان
    val name: String? = null
    println("\nDefault name: ${name.orDefault("Guest")}")
}

المخرجات:

TEXT 📖 للعرض فقط
ORD-001
ORD-001
true

=== Order Summaries ===
ORD-001 | $299.99 USD | CONFIRMED | Alice
ORD-002 | $15000.00 USD | PENDING | Bob
ORD-003 | $2500.00 USD | SHIPPED | Charlie
ORD-004 | $8900.00 USD | CONFIRMED | Bob
ORD-005 | $45.50 USD | CANCELLED | Alice

=== High Value Orders ===
ORD-002 | $15000.00 USD | PENDING | Bob

=== Revenue ===
Total: $26745.49 USD
In millions: 0.02674549M

=== Revenue by Customer ===
  Alice: $345.49 USD
  Bob: $23900.00 USD
  Charlie: $2500.00 USD

Default name: Guest

8. أمثلة عملية سريعة

▶ مثال: دوال امتداد بسيطة

KOTLIN
// دالة امتداد على String
fun String.addExcitement(): String = "$this!"

val greeting = "Hello"
println(greeting.addExcitement())

// دالة امتداد مع معامل
fun String.repeat(times: Int): String = this.repeat(times)

println("Ha".repeat(3))

// دالة امتداد ترجع قيمة محسوبة
fun List<Int>.product(): Int {
    var result = 1
    for (n in this) result *= n
    return result
}

println("Product of 1..5: ${(1..5).toList().product()}")
println("Product of [2,3,7]: ${listOf(2, 3, 7).product()}")

**المخرجات:

TEXT 📖 للعرض فقط
Hello!
HaHaHa
Product of 1..5: 120
Product of [2,3,7]: 42

▶ مثال: خصائص امتداد

KOTLIN
// خاصية امتداد محسوبة
val String.wordCount: Int
    get() = split("\\s+".toRegex()).size

val Long.isEven: Boolean
    get() = this % 2 == 0

val <T> List<T>.secondLast: T?
    get() = if (size >= 2) this[size - 2] else null

// استخدام
val sentence = "Kotlin is a modern programming language"
println("Words: ${sentence.wordCount}")

println("10 is even: ${10L.isEven}")
println("7 is even: ${7L.isEven}")

val items = listOf("a", "b", "c", "d")
println("Second last: ${items.secondLast}")
println("Empty: ${emptyList<String>().secondLast}")

**المخرجات:

TEXT 📖 للعرض فقط
Words: 5
10 is even: true
7 is even: false
Second last: d
Empty: null

▶ مثال: دوال امتداد على String

KOTLIN
// تحويل String إلى camelCase
fun String.toCamelCase(): String {
    return split("_", "-").mapIndexed { i, part ->
        if (i == 0) part.lowercase()
        else part.lowercase().replaceFirstChar { it.uppercase() }
    }.joinToString("")
}

println("hello_world".toCamelCase())
println("kotlin-is-fun".toCamelCase())

// التحقق إذا كان String رقم
fun String.isNumeric(): Boolean = this.toDoubleOrNull() != null

println("'123' is numeric: ${"123".isNumeric()}")
println("'12.5' is numeric: ${"12.5".isNumeric()}")
println("'abc' is numeric: ${"abc".isNumeric()}")

// تكرار String N مرة
fun String.times(n: Int): String = repeat(n)

println("ab".times(3))

// عكس String
fun String.reverse(): String = this.reversed()

println("Kotlin".reverse())

**المخرجات:

TEXT 📖 للعرض فقط
helloWorld
kotlinIsFun
'123' is numeric: true
'12.5' is numeric: true
'abc' is numeric: false
ababab
niltoK

▶ مثال: دوال امتداد على List

KOTLIN
// تقسيم القائمة إلى أجزاء
fun <T> List<T>.chunked(size: Int): List<List<T>> {
    if (size <= 0) return listOf(this)
    val chunks = mutableListOf<List<T>>()
    var i = 0
    while (i < size) {
        chunks.add(subList(i, minOf(i + size, this.size)))
        i += size
    }
    return chunks
}

val nums = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
println("Chunks of 3: ${nums.chunked(3)}")

// الحصول على عنصر عشوائي
fun <T> List<T>.random(): T = this[(0 until size).random()]

println("Random from list: ${listOf("A", "B", "C").random()}")

// مجموع مخصص مع مبدئ
fun List<Int>.sumFrom(start: Int): Int = filter { it >= start }.sum()

println("Sum >= 3: ${listOf(1, 2, 3, 4, 5, 6).sumFrom(3)}")

// تصفية حسب الفهرس الزوجي
fun <T> List<T>.evenIndexed(): List<T> =
    filterIndexed { index, _ -> index % 2 == 0 }

println("Even indexed: ${(1..6).toList().evenIndexed()}")

// سلسلة من التحويلات
fun <T, R> List<T>.pipe(initial: R, transform: (R, T) -> R): R {
    var result = initial
    for (item in this) result = transform(result, item)
    return result
}

val sum = (1..5).toList().pipe(0) { acc, n -> acc + n }
val product = (1..5).toList().pipe(1) { acc, n -> acc * n }
println("Sum: $sum, Product: $product")

**المخرجات:

TEXT 📖 للعرض فقط
Chunks of 3: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Random from list: B
Sum >= 3: 12
Even indexed: [1, 3, 5]
Sum: 15, Product: 120

▶ مثال: دوال امتداد nullable

KOTLIN
// دالة امتداد على نوع nullable
fun String?.orDefault(default: String): String = this ?: default

val nullString: String? = null
val emptyString: String? = ""

println(nullString.orDefault("fallback"))
println(emptyString.orDefault("fallback"))
println("hello".orDefault("fallback"))

// سلسلة استدعاء آمنة
fun String?.toIntOrZero(): Int = this?.toIntOrNull() ?: 0

println("42".toIntOrZero())
println("abc".toIntOrZero())
println(nullString.toIntOrZero())

// التحقق إذا كان فارغ أو null
fun String?.isNullOrEmptyOrBlank(): Boolean =
    this.isNullOrEmpty() || this.isBlank()

println("null isEmpty: ${nullString.isNullOrEmptyOrBlank()}")
println("'' isEmpty: ${emptyString.isNullOrEmptyOrBlank()}")
println("'  ' isEmpty: ${"  ".isNullOrEmptyOrBlank()}")
println("'hello' isEmpty: ${"hello".isNullOrEmptyOrBlank()}")

**المخرجات:

TEXT 📖 للعرض فقط
fallback
fallback
hello
42
0
0
null isEmpty: true
'' isEmpty: true
'  ' isEmpty: true
'hello' isEmpty: false

▶ مثال: دوال امتداد للأرقام والتواريخ

KOTLIN
// دالة امتداد على Int
fun Int.factorial(): Long {
    var result = 1L
    for (i in 2..this) result *= i
    return result
}

println("5! = ${5.factorial()}")
println("10! = ${10.factorial()}")

// تحويل إلى ساعات:دقائق:ثواني
fun Int.toHMS(): String {
    val h = this / 3600
    val m = (this % 3600) / 60
    val s = this % 60
    return "${h}h ${m}m ${s}s"
}

println("3661 seconds: ${3661.toHMS()}")
println("7200 seconds: ${7200.toHMS()}")

// تقريب إلى مضاعفات
fun Double.roundTo(multiples: Double): Double {
    return Math.round(this / multiples) * multiples
}

println("3.7 rounded to 0.5: ${3.7.roundTo(0.5)}")
println("3.2 rounded to 0.5: ${3.2.roundTo(0.5)}")

// التحقق إذا كان زوجي
fun Int.isEven(): Boolean = this % 2 == 0
fun Int.isOdd(): Boolean = this % 2 != 0

println("4 is even: ${4.isEven()}")
println("7 is odd: ${7.isOdd()}")

**المخرجات:

TEXT 📖 للعرض فقط
5! = 120
10! = 3628800
3661 seconds: 1h 1m 1s
7200 seconds: 2h 0m 0s
3.7 rounded to 0.5: 3.5
3.2 rounded to 0.5: 3.0
4 is even: true
7 is odd: true

▶ مثال: دوال امتداد عامة لـ DSL

KOTLIN
// بناء DSL بسيط باستخدام دوال الامتداد
class HtmlBuilder {
    private val content = StringBuilder()

    fun tag(name: String, block: HtmlBuilder.() -> Unit) {
        content.append("<$name>")
        this.block()
        content.append("</$name>")
    }

    fun text(content: String) {
        this.content.append(content)
    }

    override fun toString() = content.toString()
}

fun html(block: HtmlBuilder.() -> Unit): String {
    val builder = HtmlBuilder()
    builder.block()
    return builder.toString()
}

val page = html {
    tag("html") {
        tag("body") {
            tag("h1") {
                text("مرحبا بك في Kotlin DSL")
            }
            tag("p") {
                text("هذا مثال على DSL باستخدام دوال الامتداد")
            }
        }
    }
}

println(page)

**المخرجات:

TEXT 📖 للعرض فقط
<html><body><h1>مرحبا بك في Kotlin DSL</h1><p>هذا مثال على DSL باستخدام دوال الامتداد</p></body></html>

❓ أسئلة شائعة

س هل يمكن لدوال الامتداد الوصول للأعضاء الخاصين؟
ج لا. دوال الامتداد معرّفة خارج الصنف ولا يمكنها الوصول إلا للأعضاء العامين. هذا مفتاح عدم كسر الامتدادات للتغليف.
س ماذا يحدث إذا كانت دالة الامتداد تحمل نفس اسم تابع عضو؟
ج توابع العضو لها الأولوية. إذا كان الصنف يمتلك بالفعل تابعاً بنفس الاسم والتوقيع، تُتجاهل دالة الامتداد. لهذا الامتدادات آمنة — لا تتجاوز أبداً سلوكاً موجوداً عن طريق الخطأ.
س هل يمكن وراثة دوال الامتداد وتجاوزها؟
ج لا. دوال الامتداد تُرسل بشكل ثابت ولا تشارك في الإرسال الافتراضي. الأصناف الفرعية لا يمكنها تجاوز دوال امتداد الصنف الأب.
س لماذا لا يمكن لخصائص الامتداد أن يكون لها حقول داعمة؟
ج لأن خصائص الامتداد لا تُخزن في الكائنات — إنها مجرد منطق حساب. إذا احتجت لتخزين حالة، يجب استخدام آليات أخرى (مثل ارتباطات Map).
س هل تلوث امتدادات المستوى الأعلى مساحة الأسماء؟
ج تتطلب استيراداً للاستخدام، لذا لا تلوث تلقائياً. يُنصح بتجميع الامتدادات ذات الصلة في نفس الملف واستيرادها حسب الحاجة.
س هل أداء دوال الامتداد مماثل للتوابع العادية؟
ج شبه متطابق. دوال الامتداد تُترجم إلى استدعاءات توابع ثابتة، والتي يمكن لـ JVM تضمينها وتحسينها بسهولة. صفر تكلفة إضافية.

📖 ملخص


📝 تمارين

  1. مبتدئ (⭐): أضف دالة امتداد containsPattern إلى String تتحقق مما إذا كانت تحتوي على نمط regex معين. تلميح: استخدم Regex.containsMatchIn
  2. متوسط (⭐⭐): أضف دوال امتداد toOrderId() و isOrderId() إلى String. تلميح: أضف "ORD-" في البداية / تحقق بـ regex
  3. متقدم (⭐⭐⭐): تحقق من الإرسال الثابت لدوال الامتداد: عرّف Order و BulkOrder، كل منهما بدالة امتداد تحمل نفس الاسم، استدعِ من خلال مرجع من نوع Order ولاحظ النتيجة. تلميح: النوع المصرح يحدد الاستدعاء

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

Web-Tutorial.com

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

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

100%