تخطَّ إلى المحتوى

الوراثة والبروتوكولات في Swift: protocol و extension

الدرس 9 من 11 · Swift

في درس الهياكل والكلاسات ورثت Cat من Animal، وعرفت أن الوراثة للكلاسات فقط. فكيف تتشارك الهياكل سلوكاً موحداً إذن؟ الجواب هو البروتوكولات (Protocols)، وهي ما يسمى في لغات أخرى «الواجهات». في هذا الدرس ستتعمق في الوراثة، ثم تتعلم البروتوكولات والإضافات، وهي الأسلوب المفضل في Swift لتنظيم الكود.

الكلاس الوارث يستطيع إضافة خصائص جديدة، وأن يبني على دوال أبيه بدل استبدالها:

class Employee {
let name: String
let salary: Double
init(name: String, salary: Double) {
self.name = name
self.salary = salary
}
func bonus() -> Double {
salary * 0.10
}
func describe() -> String {
"\(name) - المكافأة: \(bonus())"
}
}
class Manager: Employee {
let teamSize: Int
init(name: String, salary: Double, teamSize: Int) {
self.teamSize = teamSize
super.init(name: name, salary: salary)
}
override func bonus() -> Double {
super.bonus() + 500
}
}
let staff: [Employee] = [
Employee(name: "هدى", salary: 4000),
Manager(name: "كريم", salary: 6000, teamSize: 5),
]
for person in staff {
print(person.describe())
}
الناتج
هدى - المكافأة: 400.0
كريم - المكافأة: 1100.0
  • super.init(...): استدعاء مُنشئ الأب. في Swift يجب أن تهيئ خصائص الابن أولاً ثم تستدعي super.init.
  • super.bonus(): نسخة الأب من الدالة، ونضيف عليها.
  • describe مكتوبة مرة واحدة في الأب، ومع ذلك استخدمت مكافأة المدير الصحيحة. هذا هو تعدد الأشكال (Polymorphism): الكائن الحقيقي يحدد أي نسخة تعمل.

البروتوكول قائمة متطلبات: خصائص ودوال يتعهد النوع بتوفيرها. وميزته الكبرى أنه يعمل مع struct و class معاً:

protocol Shape {
var name: String { get }
func area() -> Double
}
struct Rectangle: Shape {
let name = "مستطيل"
let width: Double
let height: Double
func area() -> Double {
width * height
}
}
struct Circle: Shape {
let name = "دائرة"
let radius: Double
func area() -> Double {
3.14 * radius * radius
}
}
let shapes: [any Shape] = [
Rectangle(width: 4, height: 5),
Circle(radius: 10),
]
for shape in shapes {
print("\(shape.name): \(shape.area())")
}
الناتج
مستطيل: 20.0
دائرة: 314.0
  • var name: String { get }: البروتوكول يطلب خاصية قابلة للقراءة، ولا يهمه كيف توفرها.
  • struct Rectangle: Shape: الهيكل يطابق البروتوكول، والمترجم يرفضه إن نسي أي متطلب.
  • [any Shape]: مصفوفة تقبل أي نوع يطابق Shape. الكلمة any تعني «قيمة من نوع ما يطابق هذا البروتوكول».

البروتوكول مثل رخصة القيادة: لا يهم من أنت ولا من أي عائلة، المهم أنك أثبتّ قدرتك على القيادة.

والنوع الواحد يطابق أي عدد من البروتوكولات، بعكس الوراثة التي تسمح بأب واحد فقط:

protocol Playable {
func play()
}
protocol Shareable {
func share()
}
struct Song: Playable, Shareable {
let title: String
func play() {
print("تشغيل \(title)")
}
func share() {
print("مشاركة \(title)")
}
}
let song = Song(title: "نشيد الصباح")
song.play()
song.share()
الناتج
تشغيل نشيد الصباح
مشاركة نشيد الصباح

extension تضيف دوالاً إلى نوع موجود، حتى لو لم تكتبه أنت، مثل Int و String:

extension Int {
var isEven: Bool {
self % 2 == 0
}
func squared() -> Int {
self * self
}
}
print(7.isEven)
print(7.squared())
الناتج
false
49

والأقوى: الإضافة على بروتوكول تعطي كل الأنواع المطابقة له دالة جاهزة دون أن تكتبها في كل نوع:

protocol Greeter {
var name: String { get }
}
extension Greeter {
func greet() {
print("مرحباً، أنا \(name)")
}
}
struct Teacher: Greeter {
let name: String
}
struct Robot: Greeter {
let name: String
func greet() {
print("بيب بوب. الاسم: \(name)")
}
}
Teacher(name: "منى").greet()
Robot(name: "R2").greet()
الناتج
مرحباً، أنا منى
بيب بوب. الاسم: R2

Teacher حصل على greet مجاناً، و Robot كتب نسخته الخاصة فاستُخدمت بدلاً من الافتراضية. بهذا الأسلوب تتشارك الهياكل الكود دون وراثة.

بروتوكولات جاهزة من Swift

Section titled “بروتوكولات جاهزة من Swift”

Swift نفسها مبنية على بروتوكولات، وهذه ثلاثة ستستخدمها كثيراً:

struct Point: Equatable, CustomStringConvertible {
let x: Int
let y: Int
var description: String {
"(\(x), \(y))"
}
}
let p1 = Point(x: 2, y: 3)
let p2 = Point(x: 2, y: 3)
print(p1 == p2)
print(p1)
الناتج
true
(2, 3)
البروتوكول ماذا يمنحك
Equatable المقارنة بـ ==، وتكتبها Swift عنك تلقائياً للهياكل البسيطة
Comparable المقارنة بـ < و >، فتعمل sorted() مع نوعك
CustomStringConvertible شكل الطباعة عبر الخاصية description

مصفوفة من نوع الأب أو البروتوكول قد تحمل أنواعاً مختلفة. is تفحص النوع، و as? تحاول التحويل وتُرجع قيمة اختيارية:

for person in staff {
if let manager = person as? Manager {
print("\(manager.name) يدير \(manager.teamSize) موظفين")
} else {
print("\(person.name) موظف")
}
}
print(staff[1] is Manager)
الناتج
هدى موظف
كريم يدير 5 موظفين
true

بعد نجاح as? Manager أصبح بإمكاننا قراءة teamSize التي لا يعرفها النوع Employee.

الوراثة البروتوكول
يعمل مع class فقط struct و class و enum
العدد المسموح أب واحد أي عدد
يحمل بيانات مخزنة نعم لا، متطلبات فقط
مشاركة الكود من الأب من extension على البروتوكول

توصية Apple: ابدأ بالبروتوكول. واختر الوراثة فقط عندما تحتاج كلاسات تتشارك بيانات مخزنة، أو حين يفرضها إطار العمل مثل UIKit.

  1. أنشئ protocol Vehicle فيه name و wheels، وهيكلين Car و Bike يطابقانه.
  2. أضف extension Vehicle فيها دالة info() تطبع الاسم وعدد العجلات، وجرّبها على مصفوفة [any Vehicle].
  3. أنشئ protocol Electric فيه func charge()، وهيكل ElectricCar يطابق البروتوكولين معاً.
  4. تحدٍّ: أضف extension String فيها خاصية محسوبة wordCount تُرجع عدد الكلمات في النص.
  • نسيان متطلب: ستقرأ Type 'Circle' does not conform to protocol 'Shape'. زر الإصلاح في Xcode يضيف الدوال الناقصة فارغة.
  • super.init قبل تهيئة خصائص الابن: خطأ ترجمة في Swift. هيّئ خصائص الكلاس الوارث أولاً.
  • نسيان override: إعادة كتابة دالة الأب بدون override خطأ ترجمة، وليست اختيارية.
  • خصائص مخزنة في extension: الإضافات تقبل الدوال والخصائص المحسوبة فقط، ولا تقبل خصائص مخزنة جديدة.

سؤال

ما الميزة الأساسية للبروتوكول مقارنة بالوراثة في Swift؟

سؤال

ماذا تُرجع person as? Manager إذا لم يكن الكائن من النوع Manager؟

  • super.init و super.method() للبناء على الكلاس الأب، و final تمنع الوراثة.
  • protocol قائمة متطلبات تطابقها الهياكل والكلاسات، وبأي عدد.
  • extension تضيف دوالاً لأي نوع، وعلى البروتوكول تمنح تنفيذاً افتراضياً.
  • Equatable و Comparable و CustomStringConvertible بروتوكولات جاهزة مفيدة.
  • is تفحص النوع، و as? تحوّل بأمان. ابدأ دائماً بالبروتوكول.