Kotlin: شرح دوال الامتداد في كوتلن
آخر تحديث: 2026-08-26
دوال الامتداد تتيح لتشارلي إضافة isHighValue() إلى Order بدون تعديل كود مصدره — ليست سحراً، بل مجرد سكر نحوي للإرسال الثابت وقت الترجمة، لكن قيمتها العملية لا تُنكر.
1. ما ستتعلمه
- دوال الامتداد:
fun ReceiverType.extensionName() - خصائص الامتداد: خصائص محسوبة بدون حقول داعمة
- الإرسال الثابت: الامتدادات لا تكسر التغليف، لا تشارك في تعدد الأشكال
- التحكم في النطاق: امتدادات المستوى الأعلى مقابل امتدادات عضو الصنف
- تشارلي في العمل:
Order.isHighValue()/List<Order>.totalRevenue()
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) مخطط تسلسل الإرسال الثابت
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 تضمينها وتحسينها بسهولة. صفر تكلفة إضافية.
📖 ملخص
- دوال الامتداد
fun Type.name()تضيف توابع بدون تعديل كود المصدر - خصائص الامتداد
val Type.nameتضيف خصائص محسوبة، غير مسموح بالحقول الداعمة - الامتدادات تُرسل بشكل ثابت: النوع المصرح يحدد الاستدعاء، لا تعدد أشكال
- توابع العضو لها أولوية على الامتدادات ذات نفس الاسم — الامتدادات لا تكسر السلوك الموجود أبداً
- امتدادات المستوى الأعلى تتطلب استيراداً؛ امتدادات عضو الصنف مرئية فقط داخل الصنف
- دوال الامتداد تطوّر واجهات البرمجة من
Util.method(obj)إلىobj.method()
📝 تمارين
- مبتدئ (⭐): أضف دالة امتداد
containsPatternإلىStringتتحقق مما إذا كانت تحتوي على نمط regex معين. تلميح: استخدمRegex.containsMatchIn - متوسط (⭐⭐): أضف دوال امتداد
toOrderId()وisOrderId()إلىString. تلميح: أضف "ORD-" في البداية / تحقق بـ regex - متقدم (⭐⭐⭐): تحقق من الإرسال الثابت لدوال الامتداد: عرّف
OrderوBulkOrder، كل منهما بدالة امتداد تحمل نفس الاسم، استدعِ من خلال مرجع من نوعOrderولاحظ النتيجة. تلميح: النوع المصرح يحدد الاستدعاء