Kotlin: شرح الأدوية في كوتلن
آخر تحديث: 2026-08-26
الأدوية في كوتلن أكثر أماناً من جافا — تباين موقع التصريح يتيح لتشارلي تحديد التباين المشترك وقت التعريف بدلاً من تكرار الرموز البديلة في كل موقع استخدام.
1. ما ستتعلمه
- التباين:
out(التباين المشترك) مقابلin(التباين العكسي) - تباين موقع الاستخدام: إسقاط النوع
- الإسقاط النجمي: وصول آمن لـ
*إلى الأدوية غير المعروفة reified: الدوال المضمنة تحافظ على معلومات النوع العام- تشارلي في العمل:
ReadOnlyRepository<out T>/OrderFilter<in T>
2. قصة مهندس حقيقية
(1) نقطة الألم: جحيم الرموز البديلة للأدوية في جافا
يكتب تشارلي List<? extends Order> لقوائم الطلبات المقروءة فقط و List<? super Order> لقوائم الكتابة فقط في جافا. كل موقع استخدام يتطلب تكرار الرموز البديلة، مما يجعل الكود غير مقروء تقريباً.
(2) حل تباين موقع التصريح في كوتلن
JAVA
// جافا: تكرار الرمز البديل في كل موقع استخدام
void process(List<? extends Order> orders) { }
void addAll(List<? super Order> target) { }
// كوتلن: تصريح التباين مرة واحدة في موقع التعريف
class Repository<out T : Order> { // تباين مشترك في التعريف
fun getAll(): List<T> // T يظهر فقط في موقع 'out'
}
// لا حاجة للرموز البديلة في موقع الاستخدام!
تباين موقع التصريح: عرّف مرة، استخدم بلا حدود. تباين موقع الاستخدام في جافا: كرر في كل موقع استدعاء.
3. أساسيات الأدوية
(1) الأصناف والدوال العامة
KOTLIN
// صنف عام
class Box<T>(val value: T) {
fun unwrap(): T = value
}
val intBox = Box(42) // T يستنتج كـ Int
val strBox = Box("Hello") // T يستنتج كـ String
// دالة عامة
fun <T> singletonList(item: T): List<T> = listOf(item)
// عام مع قيد نوع
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a > b) a else b
// قيود متعددة
fun <T> process(item: T) where T : CharSequence, T : Comparable<T> {
// T هو CharSequence و Comparable في نفس الوقت
}
(2) مشكلة التباين: لماذا نحتاج out/in
KOTLIN
// هذا يبدو معقولاً لكنه خطأ نوع
val strings: List<String> = listOf("Hello", "World")
// val objects: List<Any> = strings // خطأ في كوتلن (صحيح في مصفوفات جافا)
// لماذا؟ إذا سُمح:
// objects.add(42) // سيؤدي لفساد قائمة النصوص!
// strings[0].length // ClassCastException وقت التشغيل
4. تباين موقع التصريح
(1) out (التباين المشترك): منتج فقط، لا مستهلك أبداً
KOTLIN
// المنتج: T يظهر فقط في موقع 'out' (نوع الإرجاع)
interface Repository<out T> {
fun findById(id: String): T
fun findAll(): List<T>
// fun save(item: T) // خطأ: T في موقع 'in'
}
// الآن هذا يعمل:
val orderRepo: Repository<Order> = OrderRepositoryImpl()
val anyRepo: Repository<Any> = orderRepo // صحيح! تباين مشترك
(2) in (التباين العكسي): مستهلك فقط، لا منتج أبداً
KOTLIN
// المستهلك: T يظهر فقط في موقع 'in' (نوع المعامل)
interface Filter<in T> {
fun test(item: T): Boolean
fun filter(items: List<T>): List<T>
// fun getResult(): T // خطأ: T في موقع 'out'
}
// الآن هذا يعمل:
val orderFilter: Filter<Order> = object : Filter<Order> {
override fun test(item: Order) = item.total > 1_000
override fun filter(items: List<Order>) = items.filter { test(it) }
}
val anyFilter: Filter<Any> = orderFilter // لا يمكن التعيين - اتجاه التباين العكسي
(3) قواعد التباين نظرة سريعة
| المُعدِّل | المعنى | T يظهر في | اتجاه النوع الفرعي | التشبيه |
|---|---|---|---|---|
out |
تباين مشترك | فقط في موقع الإرجاع | Producer<Sub> نوع فرعي من Producer<Base> |
جافا ? extends |
in |
تباين عكسي | فقط في موقع المعامل | Consumer<Base> نوع فرعي من Consumer<Sub> |
جافا ? super |
| بلا | ثابت | أي موقع | لا علاقة نوع فرعي | جافا T |
(4) مخطط تدفق التباين
flowchart TD
subgraph "التباين المشترك (out)"
A1[منتج من Sub] -->|"IS-A"| A2[منتج من Base]
end
subgraph "التباين العكسي (in)"
B1[مستهلك من Base] -->|"IS-A"| B2[مستهلك من Sub]
end
subgraph "ثابت"
C1[صندوق من Sub] -.->|"NOT IS-A"| C2[صندوق من Base]
end
5. تباين موقع الاستخدام (إسقاط النوع)
عندما لا يمكنك تحديد التباين في موقع التصريح، اسقطه في موقع الاستخدام.
KOTLIN
// إسقاط موقع الاستخدام: عندما يكون موقع التصريح ثابتاً
fun copyFrom(source: Array<out Any>, dest: Array<in Any>) {
for (i in source.indices) {
dest[i] = source[i]
}
}
// مكافئ لجافا:
// void copyFrom(Object[] source, Object[] dest)
(1) مقارنة تباين موقع الاستخدام
| السيناريو | جافا | كوتلن |
|---|---|---|
| معامل للقراءة فقط | <? extends T> |
<out T> |
| معامل للكتابة فقط | <? super T> |
<in T> |
| تباين مشترك في التصريح | غير مدعوم | out T |
| تباين عكسي في التصريح | غير مدعوم | in T |
6. الإسقاط النجمي
عندما يكون النوع العام غير معروف، استخدم الإسقاط النجمي للوصول الآمن.
KOTLIN
// List<*> = قائمة من نوع غير معروف (آمن للقراءة، ليس للكتابة)
val unknownList: List<*> = listOf("Hello", 42, 3.14)
val first: Any? = unknownList[0] // صحيح - يرجع Any?
// unknownList.add("New") // خطأ - لا يمكن الكتابة
// مقارنة
fun printSize(list: List<*>) {
println("Size: ${list.size}") // صحيح - الحجم لا يعتمد على T
}
// Array<*> مختلف
val array: Array<*> = arrayOf("A", "B")
// array[0] = "C" // خطأ - لا يمكن الكتابة في Array<*>
val elem: Any? = array[0] // صحيح - يمكن القراءة
(1) قواعد الإسقاط النجمي
| النوع | مكافئ الإسقاط النجمي | قابل للقراءة | قابل للكتابة |
|---|---|---|---|
Foo<out T> |
Foo<out Any?> |
✅ (Any?) | ❌ |
Foo<in T> |
Foo<in Nothing> |
❌ | ✅ (Nothing = غير قابل للكتابة) |
Foo<T> (ثابت، T حد أعلى) |
Foo<out Any?> |
✅ (Any?) | ❌ |
Foo<T> (T حد أعلى T : Upper) |
Foo<out Upper> |
✅ (Upper) | ❌ |
7. معاملات النوع reified
(1) محو النوع و reified
KOTLIN
// العام العادي: النوع يُمحى وقت التشغيل
fun <T> isA(value: Any): Boolean = value is T // خطأ: لا يمكن الفحص لنوع ممحو
// reified: الدالة المضمنة تحافظ على النوع في موقع الاستدعاء
inline fun <reified T> isA(value: Any): Boolean = value is T // صحيح!
// الاستخدام
isA<String>("Hello") // true
isA<Int>(42) // true
isA<Int>("42") // false
// عملي: تصفية حسب النوع
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
return filter { it is T }.map { it as T }
}
(2) حالات استخدام reified العملية
KOTLIN
// إلغاء تسلسل JSON بدون معامل Class
inline fun <reified T> Json.decodeFromString(json: String): T
// بدلاً من:
// json.decodeFromString(Order::class.java, jsonString)
// فقط:
// json.decodeFromString<Order>(jsonString)
8. مثال كامل: مستودع طلبات آمن الأنواع
KOTLIN
// ============================================
// OrderProcessor - مستودع آمن الأنواع مع الأدوية
// الميزة: مستودع عام مع التباين
// ============================================
open class Order(val id: String, val total: Double)
class BulkOrder(id: String, total: Double, val minQty: Int) : Order(id, total)
class PriorityOrder(id: String, total: Double, val priority: String) : Order(id, total)
// مستودع التباين المشترك: ينتج T فقط
interface ReadOnlyRepository<out T : Order> {
fun findById(id: String): T?
fun findAll(): List<T>
}
// كاتب التباين العكسي: يستهلك T فقط
interface WriteRepository<in T : Order> {
fun save(item: T)
fun saveAll(items: List<T>)
}
// المستودع الكامل: ثابت (يقرأ ويكتب)
interface OrderRepository<T : Order> : ReadOnlyRepository<T>, WriteRepository<T>
// تطبيق في الذاكرة
class InMemoryOrderRepository<T : Order> : OrderRepository<T> {
private val storage = mutableMapOf<String, T>()
override fun findById(id: String): T? = storage[id]
override fun findAll(): List<T> = storage.values.toList()
override fun save(item: T) { storage[item.id] = item }
override fun saveAll(items: List<T>) { items.forEach { save(it) } }
}
// تصفية عامة مع التباين العكسي
class OrderFilter<in T : Order>(private val predicate: (T) -> Boolean) {
fun test(item: T): Boolean = predicate(item)
fun filter(items: List<T>): List<T> = items.filter(predicate)
}
inline fun <reified T : Order> List<Order>.filterByType(): List<T> {
return this.filterIsInstance<T>()
}
fun main() {
val orders = listOf(
Order("ORD-001", 299.99),
BulkOrder("ORD-002", 5_000.00, 100),
PriorityOrder("ORD-003", 15_000.00, "VIP"),
BulkOrder("ORD-004", 8_000.00, 50),
PriorityOrder("ORD-005", 2_500.00, "GOLD")
)
// التباين المشترك: مستودع BulkOrder قابل للتعيين لمستودع Order
val bulkRepo: ReadOnlyRepository<BulkOrder> = InMemoryOrderRepository()
val orderRepo: ReadOnlyRepository<Order> = bulkRepo // صحيح: تباين مشترك
// reified: تصفية حسب النوع وقت التشغيل
val bulkOrders: List<BulkOrder> = orders.filterByType<BulkOrder>()
println("Bulk orders: ${bulkOrders.map { it.id }}")
val priorityOrders: List<PriorityOrder> = orders.filterByType<PriorityOrder>()
println("Priority orders: ${priorityOrders.map { it.id }}")
// تصفية عامة
val highValueFilter = OrderFilter<Order> { it.total > 1_000 }
val highValue = highValueFilter.filter(orders)
println("High value orders: ${highValue.map { it.id }}")
}
المخرجات:
TEXT
📖 للعرض فقط
Bulk orders: [ORD-002, ORD-004]
Priority orders: [ORD-003, ORD-005]
High value orders: [ORD-002, ORD-003, ORD-004, ORD-005]
9. أمثلة عملية سريعة
▶ مثال: دالة عامة بسيطة
KOTLIN
// دالة عامة - تعمل مع أي نوع
fun <T> identity(value: T): T = value
println("Int: ${identity(42)}")
println("String: ${identity("Hello")}")
println("Double: ${identity(3.14)}")
println("List: ${identity(listOf(1, 2, 3))}")
// دالة تأخذ قائمة وتعيد العنصر الأول
fun <T> firstOrNull(list: List<T>, default: T? = null): T? =
if (list.isEmpty()) default else list[0]
println("First: ${firstOrNull(listOf(1, 2, 3))}")
println("Empty default: ${firstOrNull(emptyList(), default = -1)}")
// دالة عامة مع قيود
fun <T : Number> sumList(list: List<T>): Double {
return list.sumOf { it.toDouble() }
}
println("Sum Int: ${sumList(listOf(1, 2, 3, 4, 5))}")
println("Sum Double: ${sumList(listOf(1.5, 2.5, 3.0))}")
**المخرجات:
TEXT
📖 للعرض فقط
Int: 42
String: Hello
Double: 3.14
List: [1, 2, 3]
First: 1
Empty default: -1
Sum Int: 15.0
Sum Double: 7.0
▶ مثال: فئة عامة (Generic Class)
KOTLIN
// Box فئة عامة تحتفظ بقيمة من أي نوع
class Box<T>(val content: T) {
fun describe(): String = "Box contains: $content (${content!!::class.simpleName})"
fun <R> map(transform: (T) -> R): Box<R> = Box(transform(content))
}
val intBox = Box(42)
val strBox = Box("Hello Kotlin")
println(intBox.describe())
println(strBox.describe())
// map يحول النوع
val stringifiedBox = intBox.map { "Number: $it" }
val doubledBox = intBox.map { it * 2 }
println(stringifiedBox.describe())
println("Doubled: ${doubledBox.content}")
// Pair عام
class Pair<A, B>(val first: A, val second: B) {
fun <C> combine(other: Pair<A, C>): Triple<A, B, C> = Triple(first, second, other.second)
}
val pair = Pair("name", 42)
val combined = pair.combine(Pair("name", "extra"))
println("Combined: $combined")
**المخرجات:
TEXT
📖 للعرض فقط
Box contains: 42 (Int)
Box contains: Hello Kotlin (String)
Box contains: Number: 42 (String)
Doubled: 84
Combined: (name, 42, extra)
▶ مثال: قيوود النوع (Type Bounds)
KOTLIN
// قيد T : Number - يقبل فقط أنواع الأرقام
fun <T : Number> average(numbers: List<T>): Double {
if (numbers.isEmpty()) return 0.0
var sum = 0.0
for (n in numbers) sum += n.toDouble()
return sum / numbers.size
}
println("Int avg: ${average(listOf(1, 2, 3, 4, 5))}")
println("Double avg: ${average(listOf(1.5, 2.5, 3.5))}")
// قيد مركب: T يجب أن يكون Number و Comparable
fun <T> findMax(items: List<T>): T? where T : Number, T : Comparable<T> {
return items.maxOrNull()
}
println("Max int: ${findMax(listOf(3, 1, 4, 1, 5, 9, 2, 6))}")
println("Max double: ${findMax(listOf(1.5, 3.14, 2.71))}")
// قيد مع interface
interface Describable {
fun describe(): String
}
data class User(val name: String, val age: Int) : Describable {
override fun describe() = "$name ($age)"
}
fun <T> describeAll(items: List<T>) where T : Describable {
items.forEach { println(it.describe()) }
}
describeAll(listOf(User("Alice", 30), User("Bob", 25)))
**المخرجات:
TEXT
📖 للعرض فقط
Int avg: 3.0
Double avg: 2.5
Max int: 9
Max double: 3.14
Alice (30)
Bob (25)
▶ مثال: التباين out و in
KOTLIN
// Producer<out T>: ينتج T فقط (مثل List<T>)
interface Producer<out T> {
fun produce(): T
}
// Consumer<in T>: يستهلك T فقط (مثل Comparable<T>)
interface Consumer<in T> {
fun consume(value: T)
}
class StringProducer : Producer<String> {
override fun produce(): String = "Hello"
}
class AnyConsumer : Consumer<Any> {
override fun consume(value: Any) = println("Consumed: $value")
}
// Producer<Cat> يمكن استخدامه كـ Producer<Animal> (covariance)
val stringProducer: Producer<String> = StringProducer()
val anyProducerFromString: Producer<Any> = stringProducer // OK بسبب out
// Consumer<Animal> يمكن استخدامه كـ Consumer<Cat> (contravariance)
val anyConsumer: Consumer<Any> = AnyConsumer()
val stringConsumer: Consumer<String> = anyConsumer // OK بسبب in
println(anyProducerFromString.produce())
stringConsumer.consume("Test value")
**المخرجات:
TEXT
📖 للعرض فقط
Hello
Consumed: Test value
▶ مثال: reified type parameters
KOTLIN
// reified يحافظ على معلومات النوع في وقت التشغيل (مع inline فقط)
inline fun <reified T> isType(value: Any): Boolean = value is T
println("42 is Int: ${isType<Int>(42)}")
println("'Hello' is String: ${isType<String>("Hello")}")
println("42 is String: ${isType<String>(42)}")
// دالة لفحص النوع و cast آمن
inline fun <reified T> safeCast(value: Any): T? = value as? T
val maybeInt: Any = "42" // String, not Int
val result: Int? = safeCast<Int>(maybeInt)
println("Cast result: $result")
// استخدام عملي: EventBus بسيط
inline fun <reified T> handle(event: T) {
println("Handling event of type ${T::class.simpleName}: $event")
}
handle(42) // Int
handle("Hello") // String
handle(listOf(1, 2, 3)) // List
**المخرجات:
TEXT
📖 للعرض فقط
42 is Int: true
'Hello' is String: true
42 is String: false
Cast result: null
Handling event of type Int: 42
Handling event of type String: Hello
Handling event of type List: [1, 2, 3]
▶ مثال: دوال الامتداد العامة
KOTLIN
// دالة امتداد عامة على List
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
fun <T> List<T>.thirdOrNull(): T? = if (size >= 3) this[2] else null
val numbers = listOf(10, 20, 30, 40)
println("Second: ${numbers.secondOrNull()}")
println("Third: ${numbers.thirdOrNull()}")
println("Empty: ${emptyList<Int>().secondOrNull()}")
// دالة عامة تأخذ تحويل
fun <T, R> List<T>.mapToPairs(transform: (T) -> Pair<R, T>): Map<R, T> {
val result = mutableMapOf<R, T>()
for (item in this) {
val (key, value) = transform(item)
result[key] = value
}
return result
}
val users = listOf("Alice", "Bob", "Charlie")
val firstCharMap = users.mapToPairs { it.first() to it }
println("First char map: $firstCharMap")
// دالة عامة على Map
fun <K, V> Map<K, V>.swapKeysValues(): Map<V, K> {
return entries.associate { (k, v) -> v to k }
}
val original = mapOf("a" to 1, "b" to 2, "c" to 3)
val swapped = original.swapKeysValues()
println("Swapped: $swapped")
**المخرجات:
TEXT
📖 للعرض فقط
Second: 20
Third: 30
Empty: null
First char map: {A=Alice, B=Bob, C=Charlie}
Swapped: {1=a, 2=b, 3=c}
▶ مثال: الأنواع العامة المتداخلة
KOTLIN
// فئة عامة بأكثر من معامل نوع
class KeyValueStore<K, V> {
private val data = mutableMapOf<K, V>()
fun put(key: K, value: V) {
data[key] = value
}
fun get(key: K): V? = data[key]
fun getOrDefault(key: K, default: V): V = data.getOrDefault(key, default)
fun keys(): Set<K> = data.keys
fun values(): Collection<V> = data.values
fun entries(): Set<Map.Entry<K, V>> = data.entries
inline fun <R> mapValues(transform: (V) -> R): KeyValueStore<K, R> {
val result = KeyValueStore<K, R>()
for ((k, v) in data) {
result.put(k, transform(v))
}
return result
}
}
// استخدام
val store: KeyValueStore<String, Int> = KeyValueStore()
store.put("age-alice", 30)
store.put("age-bob", 25)
println("Alice age: ${store.get("age-alice")}")
println("Charlie: ${store.getOrDefault("age-charlie", 0)}")
println("Keys: ${store.keys()}")
// mapValues تُرجع نوعًا جديدًا
val doubledStore = store.mapValues { it * 2 }
println("Doubled Bob: ${doubledStore.get("age-bob")}")
**المخرجات:
TEXT
📖 للعرض فقط
Alice age: 30
Charlie: 0
Keys: [age-alice, age-bob]
Doubled Bob: 50
❓ أسئلة شائعة
س هل يمكن استخدام out و in على نفس معامل النوع في وقت واحد؟
ج لا. معامل النوع يمكن أن يكون فقط out أو in أو ثابتاً. إذا ظهر T في كل من موقع الإدخال والإخراج، يجب أن يكون ثابتاً.
س هل الأدوية في كوتلن لا تزال موجودة وقت التشغيل؟
ج مُمحاة في الغالب (مثل جافا). لكن inline + reified يمكنه الحفاظ على معلومات النوع. الدوال المضمنة تتوسع في موقع الاستدعاء، لذا معلومات النوع متاحة وقت الترجمة.
س ما الفرق بين الإسقاط النجمي
* و Any؟ج
List<*> تعني "نوع العنصر غير معروف"، قابل للقراءة فقط كـ Any؟؛ List<Any> تعني "نوع العنصر هو Any"، يمكنك إضافة أي قيمة. الأول أكثر أماناً.س متى أستخدم تباين موقع التصريح مقابل موقع الاستخدام؟
ج فضّل تباين موقع التصريح — قرر وقت تصميم API ما إذا كان T للقراءة فقط (out) أو للكتابة فقط (in). تباين موقع الاستخدام هو الملاذ الأخير.
س هل لـ reified تكلفة أداء إضافية؟
ج لا تكلفة إضافية وقت التشغيل. بما أن الدوال المضمنة تتوسع وقت الترجمة، فحوصات نوع reified تحدث وقت الترجمة بدون عمل إضافي وقت التشغيل.
س ما الفرق بين
<T : Upper> و where T : Upper؟ج القيد المفرد يستخدم اختصار
<T : Upper>؛ القيود المتعددة يجب أن تستخدم صياغة where.📖 ملخص
out(التباين المشترك): T يظهر فقط في موقع الإرجاع،Producer<Sub>نوع فرعي منProducer<Base>in(التباين العكسي): T يظهر فقط في موقع المعامل،Consumer<Base>نوع فرعي منConsumer<Sub>- تباين موقع التصريح يُعرّف مرة؛ تباين موقع الاستخدام (الإسقاط) هو تجاوز عند الطلب
- الإسقاط النجمي
*لسيناريوهات الأدوية غير المعروفة — آمن لكن محدود reifiedيحافظ على معلومات النوع العام في الدوال المضمنة، مما يتيح فحوصات النوع وقت التشغيل- تذكير التباين: المنتج → out، المستهلك → in
📝 تمارين
- مبتدئ (⭐): نفّذ
Box<T>عام واختبر توافق التعيين مع معاملات أنواع مختلفة. تلميح: هل يمكن تعيينBox<Int>إلىBox<Any>؟ - متوسط (⭐⭐): عرّف
ReadOnlyRepository<out T>وWriteRepository<in T>، أظهر علاقات تعيين التباين المشترك والتباين العكسي. تلميح:List<BulkOrder>→List<out Order> - متقدم (⭐⭐⭐): نفّذ دالة
spotNull<reified Any>تصفّي حسب النوع ثم تحوّله. تلميح: استخدمfilterIsInstance<T>