Kotlin: شرح أصناف البيانات والأصناف المختومة في كوتلن

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

data class هي الميزة القاتلة في كوتلن لإزالة الكود المتكرر — سطر واحد يولّد تلقائياً equals/hashCode/toString/copy/componentN. sealed class تبني تورينات أنواع محدودة تسمح للمترجم بفرض الشمولية — يستخدمها تشارلي لنمذجة آلة حالة الطلب بدون أي حالات فائتة.

1. ما ستتعلمه


2. قصة مطور حقيقية

(1) نقطة الألم: equals المكتوب يدوياً يسبب أخطاء

كتب بوب دالة equals يدوياً لصنف Order في جافا لكنه نسي مقارنة حقل status. نتيجةً لذلك، عاملت إزالة التكرار في Set الطلبات ذات الحالات المختلفة كطلب واحد، مما تسبب في 200 عملية معالجة مكررة.

(2) الحل بتوليد data class التلقائي

KOTLIN
// جافا: 30 سطراً من equals/hashCode المعرضة للأخطاء
// كوتلن: سطر واحد، توليد تلقائي من المترجم وصحيح
data class Order(val id: String, val total: Double, val status: String)

// equals يقارن جميع خصائص المشيد - لا يمكن نسيان حقل

data class يولّد تلقائياً equals/hashCode بناءً على جميع خصائص المشيد — من المستحيل نسيان حقل.


3. التوليد التلقائي في data class

(1) الاستخدام الأساسي

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

val o1 = Order("ORD-001", 299.99, "CONFIRMED")
val o2 = Order("ORD-001", 299.99, "CONFIRMED")

// توليد تلقائي لـ equals: يقارن جميع خصائص المشيد
println(o1 == o2)  // true

// توليد تلقائي لـ toString
println(o1)  // Order(id=ORD-001, total=299.99, status=CONFIRMED)

// توليد تلقائي لـ copy: إنشاء نسخة معدلة
val updated = o1.copy(status = "SHIPPED")
println(updated)  // Order(id=ORD-001, total=299.99, status=SHIPPED)

// توليد تلقائي لـ hashCode
println(o1.hashCode() == o2.hashCode())  // true

(2) 6 توابع مولّدة تلقائياً

التابع الغرض بناءً على
equals() فحص تساوي القيم جميع خصائص المشيد
hashCode() حساب التجزئة جميع خصائص المشيد
toString() تمثيل نصي Order(id=..., total=..., status=...)
copy() إنشاء نسخة معدلة قيم جديدة للخصائص المحددة
component1() تصريح تفكيكي بترتيب تصريح الخصائص
componentN() تصريح تفكيكي الخاصية N → componentN

(3) تحذير: copy هي نسخة سطحية

KOTLIN
data class Order(val id: String, val items: MutableList<String>)

val o1 = Order("ORD-001", mutableListOf("Widget", "Gadget"))
val o2 = o1.copy()  // نسخة سطحية!

o2.items.add("Doohickey")
println(o1.items)  // [Widget, Gadget, Doohickey] - مرجع مشترك!

// الحل: نسخ القائمة صراحةً
val o3 = o1.copy(items = o1.items.toMutableList())
⚠️ ملاحظة: copy() هي نسخة سطحية — خصائص النوع المرجعي لا تزال تشترك في نفس الكائن. يجب التعامل مع النسخ العميقة يدوياً عند الحاجة.


4. التصريحات التفكيكية

(1) التفكيك الأساسي

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

val order = Order("ORD-001", 299.99, "CONFIRMED")

// تصريح تفكيكي
val (id, total, status) = order
println("ID: $id, Total: \$$total USD, Status: $status")

// تفكيك جزئي باستخدام _
val (orderId, _, orderStatus) = order  // تخطي total

// في حلقة for
val orders = listOf(order, Order("ORD-002", 1_500.00, "PENDING"))
for ((oid, ototal, ostatus) in orders) {
    println("$oid: \$$ototal USD ($ostatus)")
}

(2) حالات استخدام التفكيك

KOTLIN
// مدخلات Map
val statusMap = mapOf("ORD-001" to "SHIPPED", "ORD-002" to "PENDING")
for ((id, status) in statusMap) { ... }

// قيم إرجاع الدوال
data class Result(val success: Boolean, val message: String)
val (ok, msg) = processOrder()

// معاملات lambda
orders.map { (id, total) -> "Order $id: \$$total USD" }

5. sealed class

(1) الصنف المختوم يبني تورينات أنواع محدودة

KOTLIN
// الصنف المختوم: توريث محدود
// يجب أن تكون جميع الأصناف الفرعية في نفس الملف (كوتلن 1.5: نفس الحزمة)
sealed class OrderStatus {
    object Pending : OrderStatus()
    data class Processing(val step: Int, val totalSteps: Int) : OrderStatus()
    object Shipped : OrderStatus()
    data class Delivered(val deliveredAt: String) : OrderStatus()
    data class Cancelled(val reason: String) : OrderStatus()
}

(2) فروع when الشاملة

KOTLIN
fun handleStatus(status: OrderStatus): String = when (status) {
    is OrderStatus.Pending -> "Waiting for payment"
    is OrderStatus.Processing -> "Step ${status.step}/${status.totalSteps}"
    is OrderStatus.Shipped -> "In transit"
    is OrderStatus.Delivered -> "Delivered at ${status.deliveredAt}"
    is OrderStatus.Cancelled -> "Cancelled: ${status.reason}"
    // لا حاجة لـ else - المترجم يتحقق من تغطية جميع الحالات
}

(3) آلة حالة الصنف المختوم

100%
stateDiagram-v2
    [*] --> Pending
    Pending --> Processing : Pay
    Pending --> Cancelled : Cancel
    Processing --> Shipped : Ship
    Processing --> Cancelled : Fail
    Shipped --> Delivered : Deliver
    Delivered --> [*]
    Cancelled --> [*]

(4) مقارنة sealed class مع enum class و open class

البُعد enum class sealed class open class
عدد الأصناف الفرعية قيم ثابتة محدود (معروف وقت الترجمة) غير محدود (قابل للتوسيع وقت التشغيل)
حالة الصنف الفرعي أحادي حالة مستقلة لكل منهما حالة مستقلة لكل منهما
شمولية when ❌ (يتطلب else)
دقة الأصناف الفرعية نفس الصنف أصناف مختلفة أصناف مختلفة
قابلية التوسيع غير قابل للتوسيع توسيع محدود مفتوح بالكامل
حالة الاستخدام تعدادات بسيطة أنواع محدودة + حالة توريث مفتوح

6. تشارلي في العمل: مصدر حدث الطلب

KOTLIN
// أحداث النطاق لمصدر الحدث
sealed class OrderEvent {
    data class Created(val orderId: String, val total: Double, val customer: String) : OrderEvent()
    data class PaymentReceived(val orderId: String, val amount: Double) : OrderEvent()
    data class ItemAdded(val orderId: String, val sku: String, val qty: Int) : OrderEvent()
    data class Shipped(val orderId: String, val trackingCode: String) : OrderEvent()
    data class Cancelled(val orderId: String, val reason: String) : OrderEvent()
}

data class OrderState(val id: String, val total: Double, val status: String, val items: List<String>)

fun applyEvent(state: OrderState?, event: OrderEvent): OrderState = when (event) {
    is OrderEvent.Created -> OrderState(event.orderId, event.total, "PENDING", emptyList())
    is OrderEvent.PaymentReceived -> state!!.copy(status = "PAID")
    is OrderEvent.ItemAdded -> state!!.copy(items = state.items + "${event.sku}x${event.qty}")
    is OrderEvent.Shipped -> state!!.copy(status = "SHIPPED")
    is OrderEvent.Cancelled -> state!!.copy(status = "CANCELLED")
}

7. مثال كامل: آلة حالة OrderProcessor

KOTLIN
// ============================================
// OrderProcessor - آلة حالة مع الأصناف المختومة
// الميزة: دورة حياة الطلب مع when الشامل
// ============================================

sealed class OrderStatus {
    object Pending : OrderStatus()
    data class Processing(val step: Int, val totalSteps: Int) : OrderStatus()
    object Shipped : OrderStatus()
    data class Delivered(val date: String) : OrderStatus()
    data class Cancelled(val reason: String, val refundAmount: Double) : OrderStatus()
}

data class Order(val id: String, val total: Double, var status: OrderStatus = OrderStatus.Pending)

fun describeStatus(status: OrderStatus): String = when (status) {
    is OrderStatus.Pending -> "Waiting for payment"
    is OrderStatus.Processing -> "Processing step ${status.step}/${status.totalSteps}"
    is OrderStatus.Shipped -> "In transit"
    is OrderStatus.Delivered -> "Delivered on ${status.date}"
    is OrderStatus.Cancelled -> "Cancelled: ${status.reason} (refund: \$${status.refundAmount} USD)"
}

fun canTransitionTo(current: OrderStatus, next: OrderStatus): Boolean = when {
    current is OrderStatus.Pending && next is OrderStatus.Processing -> true
    current is OrderStatus.Processing && next is OrderStatus.Shipped -> true
    current is OrderStatus.Shipped && next is OrderStatus.Delivered -> true
    current is OrderStatus.Pending && next is OrderStatus.Cancelled -> true
    current is OrderStatus.Processing && next is OrderStatus.Cancelled -> true
    else -> false
}

fun main() {
    val orders = listOf(
        Order("ORD-001", 299.99, OrderStatus.Pending),
        Order("ORD-002", 1_500.00, OrderStatus.Processing(2, 5)),
        Order("ORD-003", 8_900.00, OrderStatus.Shipped),
        Order("ORD-004", 45.50, OrderStatus.Delivered("2026-01-15")),
        Order("ORD-005", 2_500.00, OrderStatus.Cancelled("Customer request", 2_500.00))
    )

    println("=== Order Status Report ===")
    orders.forEach { order ->
        println("${order.id}: ${describeStatus(order.status)} (Total: \$${order.total} USD)")
    }

    // اختبار الانتقالات
    val testOrder = Order("ORD-099", 100.00)
    println("\n=== Transition Tests ===")
    println("Pending -> Processing(1,3): ${canTransitionTo(testOrder.status, OrderStatus.Processing(1, 3))}")
    println("Pending -> Shipped: ${canTransitionTo(testOrder.status, OrderStatus.Shipped)}")
    println("Pending -> Cancelled: ${canTransitionTo(testOrder.status, OrderStatus.Cancelled("Error", 100.0))}")

    // تجميع حسب نوع الحالة
    val statusCounts = orders.groupBy { it.status::class.simpleName }
    println("\n=== Status Distribution ===")
    statusCounts.forEach { (status, list) -> println("  $status: ${list.size} orders") }
}

المخرجات:

TEXT 📖 للعرض فقط
=== Order Status Report ===
ORD-001: Waiting for payment (Total: $299.99 USD)
ORD-002: Processing step 2/5 (Total: $1500.0 USD)
ORD-003: In transit (Total: $8900.0 USD)
ORD-004: Delivered on 2026-01-15 (Total: $45.5 USD)
ORD-005: Cancelled: Customer request (refund: $2500.0 USD) (Total: $2500.0 USD)

=== Transition Tests ===
Pending -> Processing(1,3): true
Pending -> Shipped: false
Pending -> Cancelled: true

=== Status Distribution ===
  Pending: 1 orders
  Processing: 1 orders
  Shipped: 1 orders
  Delivered: 1 orders
  Cancelled: 1 orders

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

▶ مثال: data class أساسي مع copy

KOTLIN
data class Customer(val id: String, val name: String, val email: String, val tier: String = "BRONZE")

val alice = Customer("C001", "Alice", "alice@example.com")
val aliceGold = alice.copy(tier = "GOLD")  // نسخ مع تعديل حقل واحد

println("Original: $alice")
println("Upgraded: $aliceGold")
println("Same id: ${alice.id == aliceGold.id}")

// تدمير (destructuring)
val (id, name, email, tier) = alice
println("Destructured: id=$id, name=$name, tier=$tier")

// في قائمة
val customers = listOf(alice, alice.copy(name = "Alice2"), aliceGold)
customers.forEach { println("${it.name} (${it.tier})") }

**المخرجات:

TEXT 📖 للعرض فقط
Original: Customer(id=C001, name=Alice, email=alice@example.com, tier=BRONZE)
Upgraded: Customer(id=C001, name=Alice, email=alice@example.com, tier=GOLD)
Same id: true
Destructured: id=C001, name=Alice, tier=BRONZE
Alice (BRONZE)
Alice2 (BRONZE)
Alice (GOLD)

▶ مثال: copy مع تعديل متعدد

KOTLIN
data class Product(val id: String, val name: String, val price: Double, val stock: Int)

val original = Product("P001", "Laptop", 1299.99, 50)

// خصومات متعددة
val discounted = original.copy(price = 999.99)
val restocked = original.copy(stock = 100)
val onSale = original.copy(price = 799.99, stock = 200, name = "Laptop (SALE)")

println("Original: $original")
println("Discounted: $discounted")
println("Restocked: $restocked")
println("On Sale: $onSale")

**المخرجات:

TEXT 📖 للعرض فقط
Original: Product(id=P001, name=Laptop, price=1299.99, stock=50)
Laptop (SALE)

▶ مثال: data class في المجموعات (equals/hashCode)

KOTLIN
data class Point(val x: Int, val y: Int)

val p1 = Point(1, 2)
val p2 = Point(1, 2)
val p3 = Point(1, 3)

// equals يعتمد على القيم
println("p1 == p2: ${p1 == p2}")      // true
println("p1 == p3: ${p1 == p3}")      // false
println("p1.hashCode == p2.hashCode: ${p1.hashCode() == p2.hashCode()}")

// استخدام في Set (يحتاج hashCode متسق)
val pointSet = setOf(p1, p2, p3)
println("Set size: ${pointSet.size}")  // 2 (p1 == p2)

// استخدام كمفتاح خريطة
val pointMap = mapOf(
    p1 to "origin-near",
    p3 to "shifted-right"
)
println(pointMap[Point(1, 2)])  // "origin-near" (يستخدم equals/hashCode)

**المخرجات:

TEXT 📖 للعرض فقط
p1 == p2: true
p1 == p3: false
p1.hashCode == p2.hashCode: true
Set size: 2
origin-near

▶ مثال: sealed class لأنواع محدودة

KOTLIN
// sealed class - جميع الفئات الفرعية يجب أن تكون في نفس الحزمة
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Failure(val message: String, val code: Int) : Result<Nothing>()
    data class Loading(val progress: Int = 0) : Result<Nothing>()
}

// معالجة شاملة - المُترجم يضمن عدم وجود فروع مفقودة
fun handle(result: Result<String>): String = when (result) {
    is Result.Success -> "✓ ${result.data}"
    is Result.Failure -> "✗ [${result.code}] ${result.message}"
    is Result.Loading -> "⏳ Loading... ${result.progress}%"
}

println(handle(Result.Success("User loaded")))
println(handle(Result.Failure("Network timeout", 504)))
println(handle(Result.Loading(45)))

**المخرجات:

TEXT 📖 للعرض فقط
✓ User loaded
✗ [504] Network timeout
⏳ Loading... 45%

▶ مثال: sealed class لحالة التطبيق

KOTLIN
sealed class UiState {
    object Loading : UiState()
    object Empty : UiState()
    data class Success(val data: List<String>) : UiState()
    data class Error(val message: String, val retryable: Boolean) : UiState()
}

fun render(state: UiState): String = when (state) {
    UiState.Loading -> "Spinner visible"
    UiState.Empty -> "Show 'No items' message"
    is UiState.Success -> "Render ${state.data.size} items: ${state.data.take(3)}"
    is UiState.Error -> if (state.retryable) {
        "Show error: ${state.message} (with Retry button)"
    } else {
        "Show fatal error: ${state.message}"
    }
}

println(render(UiState.Loading))
println(render(UiState.Empty))
println(render(UiState.Success(listOf("A", "B", "C", "D"))))
println(render(UiState.Error("Network down", retryable = true)))
println(render(UiState.Error("Auth failed", retryable = false)))

**المخرجات:

TEXT 📖 للعرض فقط
Spinner visible
Show 'No items' message
Render 4 items: [A, B, C]
Show error: Network down (with Retry button)
Show fatal error: Auth failed

▶ مثال: sealed class مع when كتعبير

KOTLIN
sealed class HttpResponse {
    data class Success(val body: String, val statusCode: Int) : HttpResponse()
    data class ClientError(val statusCode: Int, val message: String) : HttpResponse()
    data class ServerError(val statusCode: Int, val message: String) : HttpResponse()
    data class NetworkError(val exception: String) : HttpResponse()
}

// استخدام كقيمة مُرجعة
fun fetchUser(id: String): HttpResponse {
    return when {
        id.isBlank() -> HttpResponse.ClientError(400, "Invalid ID")
        id == "timeout" -> HttpResponse.NetworkError("Connection timed out")
        id.startsWith("5") -> HttpResponse.ServerError(500, "Internal server error")
        else -> HttpResponse.Success("User data for $id", 200)
    }
}

// when كتعبير - يُرجع String
fun processResponse(response: HttpResponse): String = when (response) {
    is HttpResponse.Success -> "OK [${response.statusCode}]: ${response.body}"
    is HttpResponse.ClientError -> "Client error [${response.statusCode}]: ${response.message}"
    is HttpResponse.ServerError -> "Server error [${response.statusCode}]: ${response.message}"
    is HttpResponse.NetworkError -> "Network: ${response.exception}"
}

println(processResponse(fetchUser("user-123")))
println(processResponse(fetchUser("")))
println(processResponse(fetchUser("timeout")))
println(processResponse(fetchUser("503-error")))

**المخرجات:

TEXT 📖 للعرض فقط
OK [200]: User data for user-123
Client error [400]: Invalid ID
Network: Connection timed out
Server error [500]: Internal server error

▶ مثال: data class و sealed class معًا

KOTLIN
// sealed class يحتوي على data classes
sealed class PaymentEvent {
    data class Initiated(val orderId: String, val amount: Double) : PaymentEvent()
    data class Completed(val orderId: String, val transactionId: String) : PaymentEvent()
    data class Failed(val orderId: String, val reason: String) : PaymentEvent()
    data class Refunded(val transactionId: String, val refundAmount: Double) : PaymentEvent()
}

fun processEvent(event: PaymentEvent): String = when (event) {
    is PaymentEvent.Initiated -> "[INIT] Order ${event.orderId}: \$${event.amount}"
    is PaymentEvent.Completed -> "[DONE] Order ${event.orderId}, txn=${event.transactionId}"
    is PaymentEvent.Failed -> "[FAIL] Order ${event.orderId}: ${event.reason}"
    is PaymentEvent.Refunded -> "[REFUND] txn=${event.transactionId}: \$${event.refundAmount}"
}

val events: List<PaymentEvent> = listOf(
    PaymentEvent.Initiated("ORD-001", 299.99),
    PaymentEvent.Completed("ORD-001", "TXN-12345"),
    PaymentEvent.Failed("ORD-002", "Card declined"),
    PaymentEvent.Refunded("TXN-12345", 299.99)
)

events.forEach { println(processEvent(it)) }

**المخرجات:

TEXT 📖 للعرض فقط
[INIT] Order ORD-001: $299.99
[DONE] Order ORD-001, txn=TXN-12345
[FAIL] Order ORD-002: Card declined
[REFUND] txn=TXN-12345: $299.99

❓ أسئلة شائعة

س هل يمكن لصنف بيانات أن يرث من أصناف أخرى؟
ج يمكنه الوراثة من أصناف غير بيانات لكن لا يمكنه الوراثة من صنف بيانات آخر. يمكن لأصناف البيانات تطبيق واجهات.
س هل copy في صنف البيانات نسخة عميقة؟
ج لا. copy() هي نسخة سطحية — خصائص النوع المرجعي لا تزال تشترك في نفس الكائن. يجب التعامل مع النسخ العميقة يدوياً.
س هل يجب أن تكون الأصناف الفرعية للصنف المختوم في نفس الملف؟
ج قبل كوتلن 1.5، كان يجب أن تكون في نفس الملف. من 1.5+، تحتاج فقط إلى أن تكون في نفس الحزمة. هذا يسمح بمرونة تنظيمية أكبر.
س ما الفرق بين sealed interface و sealed class؟
ج sealed interface (كوتلن 1.5+) يسمح بتطبيقات متعددة، بينما sealed class يدعم الوراثة الفردية فقط. sealed interface أكثر مرونة.
س لماذا يمكن للأصناف المختومة حذف else في when؟
ج المترجم يعرف جميع الأصناف الفرعية للصنف المختوم، لذلك يمكنه التحقق مما إذا كانت جميع الحالات مغطاة في when. إضافة صنف فرعي جديد ستسبب خطأ ترجمة، تذكيراً لك بإضافة الفرع.
س هل يجب أن تكون جميع معاملات المشيد الأساسي في صنف البيانات من نوع val/var؟
ج لا، لكن خصائص val/var فقط تشارك في equals/hashCode/copy/toString. المعاملات العادية لا تشارك.

📖 ملخص


📝 تمارين

  1. مبتدئ (⭐): استخدم data class لتعريف Product واختبر copy() و equals() والتصريحات التفكيكية. تلميح: data class Product(val name: String, val price: Double)
  2. متوسط (⭐⭐): استخدم sealed class لنمذجة Result كـ Success/Failure، واكتب handleResult للتعامل مع كلتا الحالتين. تلميح: sealed class Result { data class Success(...) ... }
  3. متقدم (⭐⭐⭐): نمذجة آلة حالة طلب كاملة باستخدام الأصناف المختومة (Pending→Processing→Shipped→Delivered / Cancelled)، نفذ transition() التي تطرح استثناء عند الانتقالات غير القانونية. تلميح: فحص when الشامل + require()

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

Web-Tutorial.com

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

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

100%