استخدام النطاق المعجمي والإغلاقات
لدى العديد من المطورين سوء الفهم هذا، معتقدين أن استخدام تعبيرات لامدا سيؤدي إلى تكرار التعليمات البرمجية وتقليل جودة التعليمات البرمجية. على العكس من ذلك، مهما أصبح الكود معقدًا، فإننا لن نقدم أي تنازلات بشأن جودة الكود من أجل البساطة، كما سنرى أدناه.
لقد تمكنا من إعادة استخدام تعبير لامدا في المثال السابق، ولكن إذا قمنا بمطابقة حرف آخر، فستعود مشكلة تكرار التعليمات البرمجية بسرعة. دعونا نحلل هذه المشكلة بشكل أكبر أولاً، ثم نستخدم النطاق المعجمي والإغلاقات لحلها.
التكرار الناجم عن تعبيرات لامدا
دعونا نقوم بتصفية تلك الحروف التي تبدأ بـ N أو B من الأصدقاء. بالاستمرار في المثال أعلاه، قد يبدو الكود الذي نكتبه كما يلي:
انسخ رمز الكود كما يلي:
المسند النهائي<String> beginWithN = name -> name.startsWith("N");
المسند النهائي<String> beginWithB = name -> name.startsWith("B");
العد الطويل النهائي FriendsStartN =
friends.stream()
.filter(startsWithN).count();
العد الطويل النهائي FriendsStartB =
friends.stream()
.filter(startsWithB).count();
يحدد المسند الأول ما إذا كان الاسم يبدأ بـ N، ويحدد المسند الثاني ما إذا كان الاسم يبدأ بـ B. نقوم بتمرير هاتين الحالتين إلى استدعاءات أسلوب التصفية على التوالي. يبدو هذا معقولًا، لكن المسندين زائدان عن الحاجة، فهما مجرد أحرف مختلفة في الشيك. دعونا نرى كيف يمكننا تجنب هذا التكرار.
استخدم النطاق المعجمي لتجنب التكرار
في الحل الأول يمكننا استخراج الحروف كمعلمات للدالة وتمرير هذه الوظيفة إلى طريقة التصفية. هذه طريقة جيدة، لكن التصفية غير مقبولة في جميع الوظائف. يقبل فقط الوظائف ذات معلمة واحدة فقط. تتوافق هذه المعلمة مع العنصر الموجود في المجموعة وترجع قيمة منطقية.
نأمل أن يكون هناك مكان حيث يمكن تخزين هذه الرسالة مؤقتًا حتى يتم تمرير المعلمة (في هذه الحالة، معلمة الاسم). لنقم بإنشاء وظيفة جديدة مثل هذه.
انسخ رمز الكود كما يلي:
المسند الثابت العام <String> checkIfStartsWith(final String letter) {
اسم الإرجاع -> name.startsWith(letter);
}
لقد حددنا وظيفة ثابتة checkIfStartsWith، والتي تتلقى معلمة سلسلة وترجع كائنًا مسندًا، والذي يمكن تمريره إلى طريقة التصفية لاستخدامها لاحقًا. على عكس الدوال ذات الترتيب الأعلى التي رأيناها سابقًا، والتي تأخذ الدوال كمعلمات، تقوم هذه الطريقة بإرجاع دالة. ولكنها أيضًا وظيفة ذات ترتيب أعلى، وقد ذكرناها سابقًا في كتاب التطور، وليس التغيير، في الصفحة 12.
يختلف الكائن المسند الذي يتم إرجاعه بواسطة طريقة checkIfStartsWith إلى حد ما عن تعبيرات lambda الأخرى. في عبارة الإرجاع -> name.startsWith(letter)، نعرف بالضبط ما هو الاسم، وهو المعلمة التي تم تمريرها إلى تعبير lambda. ولكن ما هو بالضبط الحرف المتغير؟ إنه خارج مجال الوظيفة المجهولة، حيث تجد Java المجال حيث يتم تعريف تعبير لامدا وتكتشف الحرف المتغير. وهذا ما يسمى النطاق المعجمي. يعد النطاق المعجمي أمرًا مفيدًا جدًا، فهو يسمح لنا بتخزين متغير في نطاق واحد لاستخدامه لاحقًا في سياق آخر. نظرًا لأن تعبير لامدا يستخدم متغيرات في نطاقه، فإن هذا الموقف يسمى أيضًا بالإغلاق. فيما يتعلق بقيود الوصول إلى النطاق المعجمي، هل يمكنك قراءة القيود المفروضة على النطاق المعجمي في الصفحة 31؟
هل هناك أي قيود على النطاق المعجمي؟
في تعبير لامدا، يمكننا فقط الوصول إلى الأنواع النهائية في نطاقها أو المتغيرات المحلية الفعلية للنوع النهائي.
يمكن استدعاء تعبير لامدا على الفور، أو تأخيره، أو من سلسلة رسائل مختلفة. من أجل تجنب الصراعات العرقية، لا يُسمح بتعديل المتغيرات المحلية في المجال الذي نصل إليه بمجرد تهيئتها. أي عملية تعديل ستتسبب في استثناء الترجمة.
يؤدي وضع علامة نهائية إلى حل هذه المشكلة، لكن Java لا تجبرنا على وضع علامة عليها بهذه الطريقة. في الواقع، تنظر Java إلى شيئين. الأول هو أن المتغير الذي يتم الوصول إليه يجب أن تتم تهيئته بالطريقة التي تم تعريفه بها، وقبل تعريف تعبير لامدا. ثانيًا، لا يمكن تعديل قيم هذه المتغيرات - أي أنها في الواقع من النوع النهائي، على الرغم من عدم وضع علامة عليها على هذا النحو.
تعبيرات لامدا عديمة الحالة هي ثوابت وقت التشغيل، في حين أن تلك التي تستخدم المتغيرات المحلية لها حمل حسابي إضافي.
عند استدعاء طريقة التصفية، يمكننا استخدام تعبير لامدا الذي يتم إرجاعه بواسطة طريقة checkIfStartsWith، مثل هذا:
انسخ رمز الكود كما يلي:
العد الطويل النهائي FriendsStartN =
friends.stream() .filter(checkIfStartsWith("N")).count();
العد الطويل النهائيFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();
قبل استدعاء طريقة التصفية، قمنا أولاً باستدعاء طريقة checkIfStartsWith() وقمنا بتمرير الحروف المطلوبة. يُرجع هذا الاستدعاء سريعًا تعبير لامدا، والذي نمرره بعد ذلك إلى طريقة التصفية.
من خلال إنشاء وظيفة ذات ترتيب أعلى (checkIfStartsWith في هذه الحالة) واستخدام النطاق المعجمي، نجحنا في إزالة التكرار من التعليمات البرمجية. لم نعد بحاجة إلى تحديد مرارا وتكرارا ما إذا كان الاسم يبدأ بحرف معين.
إعادة البناء، تقليل النطاق
في المثال السابق، استخدمنا طريقة ثابتة، لكننا لا نريد استخدام الطريقة الثابتة لتخزين المتغيرات مؤقتًا، مما سيؤدي إلى إفساد التعليمات البرمجية الخاصة بنا. من الأفضل تضييق نطاق هذه الوظيفة إلى المكان الذي يتم استخدامها فيه. يمكننا استخدام واجهة الوظيفة لتحقيق ذلك.
انسخ رمز الكود كما يلي:
الوظيفة النهائية<String, Predicate<String>> beginWithLetter = (String letter) -> {
المسند <String> checkStarts = (اسم السلسلة) -> name.startsWith(letter);
إرجاع التحقق من البداية };
يحل تعبير لامدا هذا محل الطريقة الثابتة الأصلية، ويمكن وضعه في دالة وتحديده قبل الحاجة إليه. يشير المتغير startWithLetter إلى دالة تكون معلمتها المدخلة عبارة عن سلسلة ومعلمة الإخراج الخاصة بها هي المسند.
بالمقارنة مع الطريقة الثابتة، هذا الإصدار أبسط بكثير، ولكن يمكننا الاستمرار في إعادة هيكلته لجعله أكثر إيجازًا. من الناحية العملية، هذه الوظيفة هي نفس الطريقة الثابتة السابقة؛ كلاهما يتلقى سلسلة ويعيد المسند. بدلاً من الإعلان بشكل صريح عن المسند، نستبدله بالكامل بتعبير لامدا.
انسخ رمز الكود كما يلي:
الوظيفة النهائية<String, Predicate<String>> beginWithLetter = (String letter) -> (اسم السلسلة) -> name.startsWith(letter);
لقد تخلصنا من الفوضى، ولكن يمكننا أيضًا إزالة تعريف النوع لجعله أكثر إيجازًا، وسيقوم مترجم Java باستنتاج النوع بناءً على السياق. دعونا نلقي نظرة على النسخة المحسنة.
انسخ رمز الكود كما يلي:
الوظيفة النهائية<String, Predicate<String>> beginWithLetter =
حرف -> اسم -> name.startsWith(letter);
يستغرق التكيف مع بناء الجملة الموجز هذا بعض الجهد. إذا كان ذلك يعميك، فابحث في مكان آخر أولاً. لقد أكملنا إعادة بناء الكود ويمكننا الآن استخدامه لاستبدال طريقة checkIfStartsWith() الأصلية، كما يلي:
انسخ رمز الكود كما يلي:
العد الطويل النهائيFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
العد الطويل النهائيFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();
في هذا القسم نستخدم وظائف ذات ترتيب أعلى. لقد رأينا كيفية إنشاء دوال داخل الدوال إذا مررنا دالة إلى دالة أخرى، وكيفية إرجاع دالة من دالة. توضح جميع هذه الأمثلة البساطة وقابلية إعادة الاستخدام التي توفرها تعبيرات لامدا.
في هذا القسم، استخدمنا وظائف الوظيفة والمسند بشكل كامل، ولكن دعونا نلقي نظرة على الفرق بينهما. يقبل المسند معلمة من النوع T ويعيد قيمة منطقية لتمثيل صواب أو خطأ حالة الحكم المقابلة لها. عندما نحتاج إلى إصدار أحكام مشروطة، يمكننا استخدام Predicateg لإكمالها. طرق مثل التصفية التي تتلقى عناصر التصفية المسند كمعلمة. يمثل Funciton دالة تكون معلماتها المدخلة عبارة عن متغيرات من النوع T وتقوم بإرجاع نتيجة من النوع R. إنه أكثر عمومية من المسند الذي يمكنه إرجاع القيمة المنطقية فقط. طالما تم تحويل المدخلات إلى مخرجات، يمكننا استخدام الوظيفة، لذلك من المعقول أن تستخدم الخريطة الوظيفة كمعلمة.
كما ترون، اختيار العناصر من المجموعة أمر بسيط للغاية. سنقدم أدناه كيفية اختيار عنصر واحد فقط من المجموعة.