الأخطاء التي لا يعرفها معظم مطوري البرامج السريعة أنهم يرتكبونها
نشرت: 2022-03-11قادمة من خلفية Objective-C ، في البداية ، شعرت أن Swift كان يعيقني. لم يكن Swift يسمح لي بإحراز تقدم بسبب طبيعته شديدة التصنيف ، والتي كانت تثير الغضب في بعض الأحيان.
على عكس Objective-C ، يفرض Swift العديد من المتطلبات في وقت الترجمة. الأشياء التي يتم تخفيفها في Objective-C ، مثل نوع id
والتحويلات الضمنية ، ليست شيئًا في Swift. حتى إذا كان لديك Int
و Double
، وتريد إضافتهما ، فسيتعين عليك تحويلهما إلى نوع واحد بشكل صريح.
تعد الاختيارات أيضًا جزءًا أساسيًا من اللغة ، وعلى الرغم من كونها مفهومًا بسيطًا ، إلا أنها تستغرق بعض الوقت لتعتاد عليها.
في البداية ، قد ترغب في فك كل شيء بالقوة ، ولكن هذا سيؤدي في النهاية إلى وقوع حوادث. عندما تتعرف على اللغة ، تبدأ في حب الطريقة التي لا تكاد تواجه فيها أخطاء وقت التشغيل حيث يتم اكتشاف العديد من الأخطاء في وقت الترجمة.
يتمتع معظم مبرمجي Swift بخبرة سابقة كبيرة في Objective-C ، والتي ، من بين أشياء أخرى ، قد تقودهم إلى كتابة تعليمات برمجية Swift باستخدام نفس الممارسات المألوفة لديهم في اللغات الأخرى. وهذا يمكن أن يسبب بعض الأخطاء السيئة.
في هذه المقالة ، نوضح الأخطاء الأكثر شيوعًا التي يرتكبها مطورو Swift وطرق تجنبها.
1. القوة الاختيارية بفك التفاف
متغير من النوع الاختياري (مثل String?
) قد يحتوي أو لا يحتوي على قيمة. عندما لا تكون لها قيمة ، فإنها تساوي nil
. للحصول على قيمة اختيارية ، عليك أولاً فكها ، ويمكن عمل ذلك بطريقتين مختلفتين.
إحدى الطرق هي الربط الاختياري باستخدام if let
أو guard let
، وهذا هو:
var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }
الثاني هو إجبار الفك باستخدام !
عامل التشغيل ، أو استخدام نوع اختياري غير مغلف ضمنيًا (مثل String!
). إذا كان الخيار الاختياري nil
، فإن فرض إلغاء التفاف سيؤدي إلى حدوث خطأ في وقت التشغيل وإنهاء التطبيق. علاوة على ذلك ، ستؤدي محاولة الوصول إلى قيمة اختياري غير مغلف ضمنيًا إلى حدوث نفس الشيء.
لدينا أحيانًا متغيرات لا يمكننا (أو لا نريد) تهيئتها في مُهيئ الفئة / الهيكل. وبالتالي ، علينا أن نعلن أنها اختيارية. في بعض الحالات ، نفترض أنها لن تكون nil
في أجزاء معينة من التعليمات البرمجية الخاصة بنا ، لذلك نفرضها أو نعلن أنها اختيارية غير مغلفة ضمنيًا لأن هذا أسهل من الاضطرار إلى القيام بربط اختياري طوال الوقت. يجب أن يتم ذلك بعناية.
يشبه هذا العمل مع IBOutlet
s ، وهي متغيرات تشير إلى كائن في المنقار أو لوحة العمل. لن يتم تهيئتها عند تهيئة الكائن الأصل (عادةً وحدة تحكم عرض أو UIView
مخصصة) ، ولكن يمكننا التأكد من أنها لن تكون nil
عند viewDidLoad
(في وحدة تحكم العرض) أو awakeFromNib
(في عرض) ، وحتى نتمكن من الوصول إليها بأمان.
بشكل عام ، أفضل الممارسات هي تجنب الإجبار على فك الغطاء واستخدام الاختيارات غير المغلفة ضمنيًا. ضع في اعتبارك دائمًا أن الخيار الاختياري يمكن أن يكون nil
والتعامل معه بشكل مناسب إما باستخدام الربط الاختياري ، أو التحقق مما إذا لم يكن nil
قبل فرض فك التغليف ، أو الوصول إلى المتغير في حالة وجود اختياري غير مغلف ضمنيًا.
2. عدم معرفة مزالق الدورات المرجعية القوية
توجد دورة مرجعية قوية عندما يحتفظ زوج من العناصر بمرجع قوي لبعضهما البعض. هذا ليس شيئًا جديدًا على Swift ، نظرًا لأن Objective-C لديه نفس المشكلة ، ومن المتوقع أن يدير مطورو Objective-C المخضرمون هذا بشكل صحيح. من المهم الانتباه إلى المراجع القوية وما يشير إلى ما. تحتوي وثائق Swift على قسم مخصص لهذا الموضوع.
من المهم بشكل خاص إدارة مراجعك عند استخدام الإغلاق. بشكل افتراضي ، تحتفظ الإغلاق (أو الكتل) بمرجع قوي لكل كائن مشار إليه بداخلها. إذا كان لأي من هذه العناصر إشارة قوية إلى الإغلاق نفسه ، فلدينا دورة مرجعية قوية. من الضروري الاستفادة من قوائم الالتقاط لإدارة كيفية التقاط مراجعك بشكل صحيح.
إذا كان هناك احتمال أن يتم إلغاء تخصيص المثيل الذي تم التقاطه بواسطة الكتلة قبل استدعاء الكتلة ، فيجب عليك التقاطها كمرجع ضعيف ، والذي سيكون اختياريًا لأنه يمكن أن يكون nil
. الآن ، إذا كنت متأكدًا من أنه لن يتم إلغاء تخصيص المثيل الذي تم التقاطه خلال عمر الكتلة ، فيمكنك التقاطه كمرجع غير مملوك . ميزة استخدام unowned
بدلاً من weak
هو أن المرجع لن يكون اختياريًا ويمكنك استخدام القيمة مباشرة دون الحاجة إلى فكها.
في المثال التالي ، الذي يمكنك تشغيله في Xcode Playground ، تحتوي فئة Container
على مصفوفة وإغلاق اختياري يتم استدعاؤه كلما تغيرت المصفوفة (يستخدم مراقبو الخصائص للقيام بذلك). Whatever
الصنف لديه مثيل Container
، وفي مُهيئته ، فإنه يعين إغلاقًا إلى arrayDidChange
وهذا الإغلاق يشير إلى self
، وبالتالي يُنشئ علاقة قوية بين مثيل Whatever
والإغلاق.
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
إذا قمت بتشغيل هذا المثال ، w
أنه لا يتم طباعة deinit whatever
، مما يعني أنه لا يتم إلغاء تخصيص المثيل الخاص بنا من الذاكرة. لإصلاح ذلك ، يتعين علينا استخدام قائمة الالتقاط لعدم التقاط self
بقوة:
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
في هذه الحالة ، يمكننا استخدام unowned
، لأن self
لن تكون nil
أبدًا خلال فترة الإغلاق.
من الممارسات الجيدة استخدام قوائم الالتقاط دائمًا تقريبًا لتجنب الدورات المرجعية ، مما يقلل من تسرب الذاكرة ، ويؤدي إلى رمز أكثر أمانًا في النهاية.
3. استخدام self
في كل مكان
على عكس Objective-C ، مع Swift ، لسنا مطالبين باستخدام self
للوصول إلى خصائص فئة أو بنية داخل طريقة. نحن مطالبون فقط بالقيام بذلك في الإغلاق لأنه يحتاج إلى التقاط self
. استخدام self
حيث لا يكون ذلك مطلوبًا ليس خطأً بالضبط ، فهو يعمل بشكل جيد ، ولن يكون هناك أخطاء ولا تحذيرات. ومع ذلك ، لماذا تكتب رمزًا أكثر مما يجب عليك؟ أيضًا ، من المهم الحفاظ على اتساق التعليمات البرمجية الخاصة بك.
4. عدم معرفة نوع أنواعك
يستخدم Swift أنواع القيم وأنواع المراجع . علاوة على ذلك ، تُظهر مثيلات نوع القيمة سلوكًا مختلفًا قليلاً عن مثيلات أنواع المراجع. إن عدم معرفة الفئة التي تناسبها كل حالة من حالاتك سيؤدي إلى توقعات خاطئة بشأن سلوك الكود.
في معظم اللغات الموجهة للكائنات ، عندما نقوم بإنشاء مثيل لفئة ونقوم بتمريرها إلى أمثلة أخرى وكحجة للأساليب ، فإننا نتوقع أن يكون هذا المثال هو نفسه في كل مكان. هذا يعني أن أي تغيير يطرأ عليه سينعكس في كل مكان ، لأنه في الواقع ، ما لدينا هو مجرد مجموعة من المراجع لنفس البيانات بالضبط. الكائنات التي تُظهر هذا السلوك هي أنواع مرجعية ، وفي Swift ، جميع الأنواع المُعلنة class
هي أنواع مرجعية.
بعد ذلك ، لدينا أنواع القيم التي يتم التصريح عنها باستخدام struct
أو enum
. يتم نسخ أنواع القيم عند تعيينها إلى متغير أو تمريرها كوسيطة إلى دالة أو طريقة. إذا قمت بتغيير شيء ما في النسخة المنسوخة ، فلن يتم تعديل النسخة الأصلية. أنواع القيم غير قابلة للتغيير . إذا قمت بتعيين قيمة جديدة لخاصية مثيل من نوع القيمة ، مثل CGPoint
أو CGSize
، يتم إنشاء مثيل جديد بالتغييرات. لهذا السبب يمكننا استخدام مراقبي الخصائص على مصفوفة (كما في المثال أعلاه في فئة Container
) لإعلامنا بالتغييرات. ما يحدث بالفعل ، هو أنه يتم إنشاء مصفوفة جديدة مع التغييرات ؛ يتم تعيينه للممتلكات ، ثم يتم استدعاء didSet
.

وبالتالي ، إذا كنت لا تعرف أن الكائن الذي تتعامل معه هو نوع مرجعي أو قيمة ، فقد تكون توقعاتك حول ما ستفعله التعليمات البرمجية الخاصة بك خاطئة تمامًا.
5. عدم استخدام الإمكانات الكاملة لـ Enums
عندما نتحدث عن التعداد ، فإننا نفكر عمومًا في تعداد C الأساسي ، وهو مجرد قائمة من الثوابت ذات الصلة التي توجد تحتها أعداد صحيحة. في Swift ، تعد الأرقام أكثر قوة. على سبيل المثال ، يمكنك إرفاق قيمة بكل حالة تعداد. تحتوي Enums أيضًا على طرق وخصائص للقراءة فقط / محسوبة يمكن استخدامها لإثراء كل حالة بمزيد من المعلومات والتفاصيل.
يعد التوثيق الرسمي الخاص بالتعدادات بديهيًا للغاية ، وتقدم وثائق معالجة الأخطاء بعض حالات الاستخدام لقوة التعداد الإضافية في Swift. تحقق أيضًا من متابعة الاستكشاف المكثف للتعدادات في Swift لتتعلم إلى حد كبير كل ما يمكنك القيام به معهم.
6. عدم استخدام الميزات الوظيفية
توفر مكتبة Swift القياسية العديد من الطرق الأساسية في البرمجة الوظيفية وتتيح لنا القيام بالكثير باستخدام سطر واحد فقط من التعليمات البرمجية ، مثل الخريطة والتقليل والتصفية ، من بين أمور أخرى.
دعونا نفحص بعض الأمثلة.
لنفترض أنه عليك حساب ارتفاع عرض الجدول. نظرًا لأن لديك فئة فرعية UITableViewCell
مثل ما يلي:
class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }
ضع في اعتبارك ، لدينا مجموعة من الأمثلة النموذجية modelArray
؛ يمكننا حساب ارتفاع عرض الجدول بسطر واحد من التعليمات البرمجية:
let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)
ستخرج map
مصفوفة من CGFloat
، تحتوي على ارتفاع كل خلية ، وسيؤدي reduce
إلى جمعها.
إذا كنت تريد إزالة عناصر من مصفوفة ، فقد ينتهي بك الأمر للقيام بما يلي:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }
هذا المثال لا يبدو أنيقًا ولا فعالًا جدًا لأننا indexOf
لكل عنصر. ضع في اعتبارك المثال التالي:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }
الآن ، أصبحت الشفرة أكثر فاعلية ، ولكن يمكن تحسينها باستخدام filter
:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)
يوضح المثال التالي كيف يمكنك إزالة جميع العروض الفرعية لـ UIView
التي تفي بمعايير معينة ، مثل تقاطع الإطار مع مستطيل معين. يمكنك استخدام شيء مثل:
for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }
ومع ذلك ، يجب أن نكون حذرين ، لأنك قد تميل إلى ربط بضع مكالمات بهذه الطرق لإنشاء تصفية وتحويل خياليين ، والذي قد ينتهي بسطر واحد من شفرة السباغيتي غير القابلة للقراءة.
7. البقاء في منطقة الراحة وعدم تجربة البرمجة المبنية على البروتوكول
يُزعم أن Swift هي أول لغة برمجة موجهة نحو البروتوكول ، كما هو مذكور في WWDC Protocol-Oriented Programming في جلسة Swift. يعني هذا في الأساس أنه يمكننا نمذجة برامجنا حول البروتوكولات وإضافة سلوك للأنواع ببساطة عن طريق التوافق مع البروتوكولات وتوسيعها. على سبيل المثال ، نظرًا لأن لدينا بروتوكول Shape
، يمكننا توسيع CollectionType
(الذي يتوافق مع أنواع مثل Array
و Set
و Dictionary
) ، وإضافة طريقة إليه تحسب إجمالي المساحة المحاسبية للتقاطعات
protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }
العبارة where Generator.Element: Shape
عبارة عن قيد ينص على أن الطرق في الامتداد ستكون متاحة فقط في حالات الأنواع التي تتوافق مع CollectionType
، والتي تحتوي على عناصر من الأنواع التي تتوافق مع Shape
. على سبيل المثال ، يمكن استدعاء هذه الطرق في مثيل Array<Shape>
، ولكن ليس في مثيل Array<String>
. إذا كان لدينا فئة Polygon
تتوافق مع بروتوكول Shape
، فستكون هذه الطرق متاحة أيضًا لمثيل Array<Polygon>
.
باستخدام امتدادات البروتوكول ، يمكنك إعطاء تنفيذ افتراضي للطرق المعلنة في البروتوكول ، والتي ستكون متاحة بعد ذلك في جميع الأنواع التي تتوافق مع هذا البروتوكول دون الحاجة إلى إجراء أي تغييرات على هذه الأنواع (الفئات أو الهياكل أو التعدادات). يتم ذلك على نطاق واسع في جميع أنحاء مكتبة Swift القياسية ، على سبيل المثال ، يتم تحديد map
reduce
في امتداد CollectionType
، ويتم مشاركة هذا التطبيق نفسه بواسطة أنواع مثل Array
and Dictionary
بدون أي كود إضافي.
يشبه هذا السلوك مزيجًا من لغات أخرى ، مثل Ruby أو Python. من خلال التوافق ببساطة مع بروتوكول مع تطبيقات الطريقة الافتراضية ، يمكنك إضافة وظائف إلى النوع الخاص بك.
قد تبدو البرمجة الموجهة نحو البروتوكول محرجة جدًا وليست مفيدة جدًا للوهلة الأولى ، مما قد يجعلك تتجاهلها ولا تجعلها مجربة. يقدم هذا المنشور فهمًا جيدًا لاستخدام البرمجة الموجهة نحو البروتوكول في التطبيقات الحقيقية.
كما تعلمنا ، فإن Swift ليست لغة لعبة
تم استقبال Swift في البداية بالكثير من الشك ؛ بدا أن الناس يعتقدون أن شركة Apple ستستبدل Objective-C بلغة ألعاب للأطفال أو بشيء لغير المبرمجين. ومع ذلك ، فقد أثبتت Swift أنها لغة جادة وقوية تجعل البرمجة ممتعة للغاية. نظرًا لأنه مكتوب بشدة ، فمن الصعب ارتكاب الأخطاء ، وعلى هذا النحو ، من الصعب سرد الأخطاء التي يمكن أن ترتكبها في اللغة.
عندما تعتاد على Swift وتعود إلى Objective-C ، ستلاحظ الفرق. ستفوت الميزات الرائعة التي يقدمها Swift وسيتعين عليك كتابة تعليمات برمجية مملة في Objective-C لتحقيق نفس التأثير. في أوقات أخرى ، ستواجه أخطاء وقت تشغيل كان من الممكن أن يكتشفها Swift أثناء التجميع. إنها ترقية رائعة لمبرمجي Apple ، ولا يزال هناك الكثير في المستقبل مع نضوج اللغة.