Optional وجافا الحديثة

تعبيرات Switch

15 دقيقة الدرس 8 من 13

تعبيرات Switch

توجد عبارة switch في Java منذ الإصدار الأول. على مدى عقود أدّت وظيفتها، لكنّها كانت تحمل مخاطر خفية: نسيان break يتسبّب في تسلسل صامت للحالات (fall-through)، ولا يمكن استخدام switch حيث تُتوقَّع قيمة، وقواعد نطاق المتغيرات المحلية كانت مفاجئة. جعل Java 14 تعبيرات switch ميزةً دائمة، وتواصلت التحسينات حتى Java 17 و21. يتناول هذا الدرس الشكل الحديث بعمق.

مشكلة عبارة switch الكلاسيكية

تأمَّل هذه الدالة التي تصنّف أيام الأسبوع:

// الأسلوب القديم — عبارة تحكّم، خطر تسلسل الحالات String classify(DayOfWeek day) { String result; switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: result = "Weekday"; break; // <-- نسيانه يُعطي "Weekend" ليوم FRIDAY case SATURDAY: case SUNDAY: result = "Weekend"; break; default: throw new IllegalArgumentException("Unknown day: " + day); } return result; }

ثلاث مشكلات واضحة: (1) تكرار سطور case X:، (2) إلزامية break لمنع التسلسل، و(3) الحاجة إلى متغيّر result قابل للتغيير. تُعالج تعبيرات switch الثلاثة معًا.

صياغة الأسهم: switch كتعبير ذي قيمة

تستخدم صياغة الأسهم -> بدلًا من :. كل فرع قاعدة مستقلة — لا تسلسل، ولا حاجة لـ break.

// الأسلوب الحديث — تعبير بقيمة، بلا تسلسل String classify(DayOfWeek day) { return switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday"; case SATURDAY, SUNDAY -> "Weekend"; }; }

لاحظ عدّة أشياء دفعةً واحدة:

  • تُفصَل التسميات المتعددة في فرع واحد بفواصل — لا تكديس لسطور case X: المتطابقة.
  • ينتج عن switch بالكامل قيمة تُعاد مباشرةً بـ return.
  • لا حاجة لـ default لأن DayOfWeek enum وجميع قيمه السبع مغطّاة. يتحقّق المُصرِّف من ذلك وقت الترجمة — الشموليّة مضمونة.
الشموليّة ضمان وقت الترجمة. إن غطّيت enum كاملًا في تعبير switch ثم أضفت لاحقًا ثابتًا جديدًا إليها، لن يتمكّن تعبير switch من الترجمة، مما يُجبرك على معالجة الحالة الجديدة. هذا هو السلوك الصحيح للفشل — يكشف الثغرة وقت البناء لا كخطأ في الإنتاج الساعة الثانية صباحًا.

إرجاع قيم معقّدة: الكلمة المحجوزة yield

الجانب الأيمن من فرع السهم يمكن أن يكون تعبيرًا واحدًا، لكن أحيانًا تحتاج إلى منطق أكثر — متغيّر محلي أو if أو حلقة. في هذه الحالة تستخدم فرعًا كتليًا وتعبير yield لإنتاج قيمة switch:

int score(String grade) { return switch (grade) { case "A" -> 100; case "B" -> 85; case "C" -> 70; case "D" -> 55; default -> { System.err.println("Unrecognised grade: " + grade); yield 0; // yield يمدّ القيمة من الفرع الكتلي } }; }

yield ليس مثل return. return يخرج من الدالة المحيطة؛ أما yield فيخرج من كتلة switch فحسب ويُسلِّم قيمتها للتعبير. لا يمكنك استخدام return داخل كتلة switch لتوفير قيمة التعبير.

أبقِ الفروع الكتليّة نادرة. إن كان كل فرع يحتاج إلى كتلة، فكّر في نقل منطق كل فرع إلى دالة مساعدة واستدعِها من فرع سهم مختصر. الفروع الكتليّة تزيد الضوضاء؛ احتفظ بها للحالات التي تحتاج فعلًا إلى حالة محلية.

دمج الفروع بـ default

لمحدِّدات غير قابلة للتحقق من شموليتها — String، int، أو كائنات اعتباطية في switch Java 21 بالأنماط — لا يمكن دائمًا التحقق من الشموليّة وقت الترجمة، لذا يلزم وجود فرع default:

String httpMessage(int status) { return switch (status) { case 200 -> "OK"; case 301, 302 -> "Redirect"; case 400 -> "Bad Request"; case 401 -> "Unauthorised"; case 403 -> "Forbidden"; case 404 -> "Not Found"; case 500 -> "Internal Server Error"; default -> "Unknown (" + status + ")"; }; }
لا تخلط فروع الأسهم مع فروع النقطتين في نفس switch. يمكن لـ switch استخدام شكل الأسهم أو شكل النقطتين في جميع الفروع، لكن لا كليهما معًا. الخلط خطأ ترجمي. شكل النقطتين لا يزال يدعم التسلسل ويتطلّب yield (لا break) لإنتاج قيمة تعبير — سبب إضافي لتفضيل فروع الأسهم حصرًا في الكود الجديد.

تعيين النتيجة لمتغيّر

تعبير switch مجرّد تعبير. أينما صلحت قيمة فتعبير switch يصلح — تعيين، معامل دالة، جانب تعبير ثلاثي، أو return:

// تعيين boolean isWeekend = switch (today) { case SATURDAY, SUNDAY -> true; default -> false; }; // مضمَّن في استدعاء دالة System.out.println(switch (today) { case SATURDAY, SUNDAY -> "Rest day"; default -> "Work day"; });

مطابقة الأنماط في switch (Java 21)

وسّع Java 21 تعبيرات switch بـأنماط الأنواع (معاينة في 17، نهائية في 21). يمكنك الآن المطابقة على النوع الحقيقي للكائن وقت التشغيل:

String describe(Object obj) { return switch (obj) { case Integer i -> "Integer: " + i; case Double d -> "Double: " + d; case String s -> "String of length " + s.length(); case null -> "null"; default -> "Other: " + obj.getClass().getSimpleName(); }; }

متغيّر النمط (i، d، s) محدود النطاق بذلك الفرع فقط وهو بالفعل مُحوَّل نوعه — لا تحويل صريح مطلوب، ولا خطر ClassCastException. يمكن الآن معالجة حالة null داخل switch بدلًا من فحص مسبق، وما زالت الشموليّة مفروضة عند المطابقة على التدرجات المغلقة (sealed).

المقارنات ومتى تستخدم كل شكل

  • استخدم تعبيرات switch بالأسهم كلما احتجت إلى تعيين قيمة ما لقيمة أخرى. هذه معظم حالات switch.
  • استخدم yield فقط حين يحتاج فرع فعلًا إلى منطق متعدد السطور لا يمكن استخراجه لدالة مساعدة.
  • تجنّب شكل النقطتين في الكود الجديد. السبب الوحيد للإبقاء عليه هو التسلسل المتعمَّد — وهو نادر.
  • فضّل تعبيرات switch على سلاسل if-else طويلة على متغيّر واحد. المُصرِّف يتحقق من الشموليّة؛ سلسلة if-else لا تستطيع.

الخلاصة

تحوّل تعبيرات switch الأداة من عبارة تحكّم إلى بنية تُنتج قيمة. تُلغي صياغة الأسهم التسلسل وتُزيل الحاجة لـ break. تُعوِّض التسميات المتعددة على فرع واحد تكديسَ سطور case. يسمح yield للفروع الكتليّة بإنتاج قيمة دون الخروج من الدالة. تُفرض الشموليّة وقت الترجمة للـ enums والأنواع المغلقة. معًا تجعل هذه الميزات switch أكثر أمانًا وأكثر تعبيريّة — أداة من الدرجة الأولى للتعيين والتوزيع، لا آليّة تحكّم موروثة فحسب.