كان switch
القديم الجيد في Java منذ اليوم الأول. نستخدمها جميعًا ونستخدمها - خاصةً المراوغات. (هل يتضايق أي شخص آخر عند break
؟) ولكن الآن بدأ كل شيء يتغير: في Java 12 ، أصبح المفتاح بدلاً من المشغل تعبيرًا:
boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException());
لدى Switch الآن القدرة على إرجاع نتيجة عملها ، والتي يمكن تعيينها لمتغير ؛ يمكنك أيضًا استخدام بناء جملة lambda style ، والذي يسمح لك بالتخلص من التمرير لجميع case
التي لا يوجد فيها بيان break
.
في هذا الدليل ، سوف أخبرك بكل ما تحتاج إلى معرفته حول تعبيرات التبديل في Java 12.
معاينة
وفقًا للمواصفات الأولية للغة ، فإن تعبيرات التبديل بدأت للتو في تطبيق Java 12.
هذا يعني أنه قد يتم تغيير إنشاء عنصر التحكم هذا في الإصدارات المستقبلية لمواصفات اللغة.
لبدء استخدام الإصدار الجديد من switch
تحتاج إلى استخدام خيار سطر الأوامر --enable-preview
أثناء التجميع وأثناء بدء تشغيل البرنامج (يجب أيضًا استخدام - --release 12
عند الترجمة - ملاحظة من قبل المترجم).
لذلك ضع في اعتبارك أن التبديل ، كتعبير ، لا يحتوي حاليًا على الصيغة النهائية في Java 12.
إذا كنت ترغب في اللعب بكل هذا بنفسك ، فيمكنك زيارة مشروع Java X التجريبي الخاص بك على github .
مشكلة في البيانات في التبديل
قبل أن ننتقل إلى نظرة عامة على الابتكارات في مجال التبديل ، فلنقم بتقييم موقف واحد بسرعة. لنفترض أننا نواجه بوليان ثلاثي "رهيب" ونريد تحويله إلى بوليان عادي. فيما يلي طريقة واحدة للقيام بذلك:
boolean result; switch(ternaryBool) { case TRUE: result = true;
توافق على أن هذا غير مريح للغاية. مثل العديد من خيارات التبديل الأخرى الموجودة في "الطبيعة" ، يحسب المثال أعلاه ببساطة قيمة المتغير ويعينه ، ولكن تم تجاوز التطبيق (أعلن result
المعرف result
لاحقًا) ، وكرر ( break
ودائماً ما يكون نتيجة نسخ المعكرونة) خطأ عرضة (نسيت فرع آخر؟ أوه!). من الواضح أن هناك شيء يجب تحسينه.
دعونا نحاول حل هذه المشكلات عن طريق وضع رمز التبديل في طريقة منفصلة:
private static boolean toBoolean(Bool ternaryBool) { switch(ternaryBool) { case TRUE: return true; case FALSE: return false; case FILE_NOT_FOUND: throw new UncheckedIOException("This is ridiculous!", new FileNotFoundException());
هذا أفضل بكثير: لا يوجد متغير وهمية ، لا توجد break
تشوش الكود ورسائل المترجم عن غياب default
(حتى لو لم يكن ذلك ضروريًا ، كما في هذه الحالة).
ولكن ، إذا فكرت في الأمر ، فلسنا ملزمين بإنشاء طرق للتحايل على ميزة اللغة المحرجة. وهذا حتى بدون اعتبار أن إعادة البناء هذه غير ممكنة دائمًا. لا ، نحن بحاجة إلى حل أفضل!
إدخال تعبيرات التبديل!
كما أوضحت في بداية المقالة ، بدءًا من Java 12 وما فوق ، يمكنك حل المشكلة أعلاه على النحو التالي:
boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException());
أعتقد أن هذا واضح جدًا: إذا كان ternartBool
، فسيتم تعيين result
" true
(بمعنى آخر ، تتحول TRUE
إلى true
). FALSE
يصبح false
.
فكرتان تنشأ على الفور:
switch
قد يكون نتيجة.- ما هو مع السهام؟
قبل الخوض في تفاصيل ميزات المحول الجديد ، في البداية سأتحدث عن هذين الجانبين الرئيسيين.
التعبير أو البيان
قد تفاجأ أن التبديل هو الآن تعبير. ولكن ماذا كان من قبل؟
قبل تشغيل Java 12 ، كان المفتاح هو المشغل - وهو بناء ضروري ينظم تدفق التحكم.
فكر في الاختلافات بين الإصدارات القديمة والجديدة من التبديل كالفرق بين if
والمشغل الثلاثي. كلاهما التحقق من الحالة المنطقية وأداء المتفرعة اعتمادا على نتيجتها.
الفرق هو أنه في if
تنفيذ الكتلة المقابلة فقط ، في حين أن المشغل الثلاثي يعرض بعض النتائج:
if(condition) { result = doThis(); } else { result = doThat(); } result = condition ? doThis() : doThat();
الأمر نفسه بالنسبة للتبديل : قبل Java 12 ، إذا كنت تريد حساب القيمة وحفظ النتيجة ، فأنت إما تقوم بتعيينها لمتغير (ثم break
) ، أو ترجعها من طريقة تم إنشاؤها خصيصًا switch
.
الآن ، يتم تقييم التعبير الكامل لبيان التبديل (يتم تحديد الفرع المقابل للتنفيذ) ، ويمكن تعيين نتيجة الحسابات لمتغير.
هناك اختلاف آخر بين التعبير والعبارة هو أن عبارة التبديل ، لأنها جزء من العبارة ، يجب أن تنتهي بفاصلة منقوطة ، على عكس بيان التبديل الكلاسيكي.
السهم أو القولون
استخدم المثال التمهيدي بناء جملة lambda style الجديد مع سهم بين الملصق وجزء التشغيل. من المهم أن نفهم أنه لهذا ليس من الضروري استخدام switch
كتعبير. في الواقع ، المثال التالي مكافئ للرمز الوارد في بداية المقال:
boolean result = switch(ternaryBool) { case TRUE: break true; case FALSE: break false; case FILE_NOT_FOUND: throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); default: throw new IllegalArgumentException("Seriously?!!?"); };
لاحظ أنه الآن يمكنك استخدام break
بقيمة! هذا يناسب تماما مع switch
النمط القديم التي تستخدم break
دون أي معنى. في هذه الحالة ، هل يعني السهم تعبيرًا بدلاً من عامل ، لماذا هو هنا؟ مجرد بناء الجملة محب؟
تاريخيا ، علامات القولون ببساطة علامة نقطة الدخول إلى كتلة البيان. من هذه النقطة ، يبدأ تنفيذ جميع التعليمات البرمجية أدناه ، حتى عند مواجهة تسمية أخرى. في switch
نحن نعرف هذا على أنه يمر إلى case
التالية (سقوط): تحدد تسمية case
المكان الذي ينتقل إليه تدفق التحكم. لإكماله ، تحتاج إلى break
أو return
.
بدوره ، يعني استخدام السهم أنه سيتم تنفيذ الكتلة الموجودة على يمينها فقط. ولا "تفشل".
المزيد عن تطور التبديل
علامات متعددة في القضية
حتى الآن ، كل case
تسمية واحدة فقط. ولكن الآن تغير كل شيء - يمكن أن تتوافق case
واحدة مع العديد من التصنيفات:
String result = switch(ternaryBool) { case TRUE, FALSE -> "sane";
يجب أن يكون السلوك واضحًا: ينتج عن TRUE
و FALSE
نفس النتيجة - يتم تقييم التعبير "عاقل".
هذا ابتكار لطيف إلى حد ما يحل محل الاستخدام المتعدد case
عندما كان مطلوبًا لتنفيذ الانتقال التمريري إلى case
التالية.
أنواع خارج التعداد
تستخدم جميع أمثلة switch
في هذه المقالة enum
. ماذا عن الأنواع الأخرى؟ يمكن أن تعمل التعبيرات وعبارات switch
أيضًا مع String
و int
(التحقق من الوثائق ) short
، byte
، char
ومغلفاتها. لم يتغير أي شيء هنا حتى الآن ، على الرغم من أن فكرة استخدام أنواع البيانات مثل float
لا تزال صالحة (من الثانية إلى الفقرة الأخيرة).
أكثر على السهم
لنلقِ نظرة على خاصيتين خاصتين بنموذج سهم سجل فاصل:
- عدم الانتقال من النهاية إلى النهاية إلى
case
التالية ؛ - كتل من المشغلين.
لا المار إلى الحالة التالية
إليكم ما يقوله JEP 325 عن هذا:
يرتبط التصميم الحالي switch
في Java ارتباطًا وثيقًا بلغات مثل C و C ++ ويدعم دلالات نهاية إلى نهاية افتراضيًا. على الرغم من أن هذه الطريقة التقليدية للتحكم تكون مفيدة في كثير من الأحيان لكتابة الكود المنخفض المستوى (مثل المحلل اللغوي للترميز الثنائي) ، نظرًا لأن switch
يستخدم في الكود العالي المستوى ، فإن أخطاء هذا النهج تفوق مرونته.
أوافق تمامًا وأرحب بفرصة استخدام التبديل دون سلوك افتراضي:
switch(ternaryBool) { case TRUE, FALSE -> System.out.println("Bool was sane");
من المهم معرفة أن هذا لا علاقة له بما إذا كنت تستخدم رمز التبديل كتعبير أو عبارة. العامل الحاسم هنا هو السهم ضد القولون.
كتل المشغل
كما هو الحال في lambdas ، يمكن أن يشير السهم إلى إما عامل واحد (على النحو الوارد أعلاه) أو كتلة مظللة بأقواس مجعدة:
boolean result = switch(Bool.random()) { case TRUE -> { System.out.println("Bool true");
تتمتع الكتل التي يجب إنشاؤها لمشغلي الخطوط المتعددة بميزة إضافية (وهو أمر غير مطلوب عند استخدام نقطتين) ، مما يعني أنه لاستخدام أسماء المتغيرات نفسها في فروع مختلفة ، لا يتطلب switch
معالجة خاصة.
إذا كان يبدو من غير المعتاد بالنسبة لك الخروج من الكتل باستخدام break
بدلاً من return
، فلا تقلق - هذا الأمر حيرني أيضًا وبدا غريبًا. لكنني فكرت في الأمر وتوصلت إلى استنتاج مفاده أنه من المنطقي ، لأنه يحافظ على النمط القديم لبناء switch
، والذي يستخدم break
بدون قيم.
تعرف على المزيد حول عبارات التبديل
وأخيراً وليس آخراً ، فإن تفاصيل استخدام switch
كتعبير:
- تعبيرات متعددة
- العودة المبكرة (
return
المبكرة) ؛ - تغطية جميع القيم.
يرجى ملاحظة أنه لا يهم الشكل الذي يتم استخدامه!
تعبيرات متعددة
تعبيرات التبديل هي تعبيرات متعددة. هذا يعني أنه ليس لديهم نوع خاص بهم ، ولكن يمكن أن يكون أحد الأنواع المتعددة. في أغلب الأحيان ، يتم استخدام تعبيرات lambda كتعبير: s -> s + " "
، قد تكون Function<String, String>
، ولكن قد تكون أيضًا Function<Serializable, Object>
أو UnaryOperator<String>
.
باستخدام تعبيرات المحول ، يتم تحديد نوع ما بواسطة التفاعل بين مكان استخدام المحول وأنواع فروعه. إذا تم تعيين تعبير التبديل إلى متغير مكتوب ، أو تم تمريره كوسيطة ، أو تم استخدامه بطريقة أخرى في سياق يعرف فيه النوع الدقيق (يطلق عليه النوع الهدف) ، فيجب أن تتطابق جميع فروعه مع هذا النوع. هنا ما قمنا به حتى الآن:
String result = switch (ternaryBool) { case TRUE, FALSE -> "sane"; default -> "insane"; };
نتيجة لذلك ، switch
تعيين switch
إلى متغير result
من النوع String
. لذلك ، String
هي النوع المستهدف ، ويجب أن ترجع جميع الفروع نتيجة نوع String
.
نفس الشيء يحدث هنا:
Serializable serializableMessage = switch (bool) { case TRUE, FALSE -> "sane";
ماذا سيحدث الان؟
(لاستخدام نوع var ، اقرأ في آخر 26 مقالة توصيات لاستخدام نوع var في Java - ملاحظة من المترجم)
إذا كان النوع المستهدف غير معروف ، نظرًا لحقيقة أننا نستخدم var ، فسيتم حساب النوع من خلال البحث عن النوع الأكثر تحديدًا للأنواع التي أنشأتها الفروع.
العودة المبكرة
نتيجة الاختلاف بين التعبير switch
هي أنه يمكنك استخدام return
لإنهاء switch
:
public String sanity(Bool ternaryBool) { switch (ternaryBool) {
... لا يمكنك استخدام return
داخل تعبير ...
public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) {
هذا أمر منطقي سواء كنت تستخدم سهمًا أو نقطتين.
تغطي جميع الخيارات
إذا استخدمت switch
كمشغل ، فلا يهم إذا كانت جميع الخيارات مغطاة أم لا. بالطبع ، يمكنك تخطي case
عن طريق الخطأ ، ولن تعمل الشفرة بشكل صحيح ، ولكن المترجم لا يهتم - أنت ، IDE الخاص بك وأدوات تحليل الشفرة الخاصة بك ستترك مع هذا وحده.
تبديل التعبيرات تفاقم هذه المشكلة. أين يجب أن تذهب التبديل إذا كانت التسمية المطلوبة مفقودة؟ الإجابة الوحيدة التي يمكن لـ Java تقديمها هي إرجاع قيمة null
لأنواع المراجع والقيمة الافتراضية للأولويات. هذا من شأنه أن يسبب الكثير من الأخطاء في الكود الرئيسي.
لمنع مثل هذه النتيجة ، يمكن للمترجم مساعدتك. بالنسبة لبيانات التبديل ، سيصر المترجم على تغطية جميع الخيارات الممكنة. دعونا نلقي نظرة على مثال قد يؤدي إلى حدوث خطأ في الترجمة:
الحل التالي مثير للاهتمام: إضافة الفرع default
ستعمل بالتأكيد على إصلاح الخطأ ، لكن هذا ليس هو الحل الوحيد - لا يزال بإمكانك إضافة case
لـ FALSE
.
نعم ، سيتمكن المحول البرمجي أخيرًا من تحديد ما إذا كانت جميع قيم التعداد قد تمت تغطيتها (ما إذا كانت جميع الخيارات مستنفدة) ، ولا يتم تعيين قيم افتراضية غير مجدية! دعنا نجلس لحظة في امتنان صامت.
ومع ذلك ، هذا لا يزال يثير سؤال واحد. ماذا لو أخذ شخص ما وتحول Bool مجنون إلى رباعي quoolion بإضافة قيمة رابعة؟ إذا قمت بإعادة ترجمة تعبير التبديل الخاص بـ Bool الممتد ، فستظهر لك رسالة خطأ (لم يعد التعبير شاملاً). بدون إعادة التحويل ، سيتحول هذا إلى مشكلة وقت التشغيل. للوقوف على هذه المشكلة ، ينتقل المحول البرمجي إلى الفرع default
، الذي يتصرف بنفس الطريقة التي استخدمناها حتى الآن ، مع استثناء.
في Java 12 ، يعمل توسيع جميع القيم بدون الفرع default
على enum
فقط ، ولكن عندما يصبح switch
أكثر قوة في الإصدارات المستقبلية من Java ، يمكن أن يعمل أيضًا مع أنواع عشوائية. إذا لم تتمكن تسميات case
التحقق من المساواة فقط ، ولكن أيضًا إجراء مقارنات (على سبيل المثال ، _ <5 -> ...) - سيغطي ذلك جميع الخيارات لأنواع رقمية.
التفكير
لقد تعلمنا من المقالة أن Java 12 يحول switch
إلى تعبير ، مما يمنحه ميزات جديدة:
- الآن
case
واحدة يمكن أن تتوافق مع العديد من التسميات. case … -> …
نموذج السهم الجديد case … -> …
تتبع صيغة تعبيرات lambda:
- يسمح المشغلين أو كتل خط واحد ؛
- منع المرور إلى
case
التالية ؛
- الآن يتم تقييم التعبير بالكامل كقيمة ، والتي يمكن بعد ذلك تعيينها إلى متغير أو تمريرها كجزء من عبارة أكبر ؛
- تعبير متعدد: إذا كان النوع المستهدف معروفًا ، فيجب أن تتوافق جميع الفروع معه. خلاف ذلك ، يتم تحديد نوع معين يطابق جميع الفروع ؛
break
يمكن إرجاع قيمة من كتلة.- للحصول على تعبير
switch
باستخدام enum
، يتحقق المحول البرمجي من نطاق كل قيمه. إذا كان default
غائبا ، تتم إضافة فرع يلقي استثناء.
أين سيقودنا؟ أولاً ، نظرًا لأن هذا ليس هو الإصدار النهائي switch
، فلا يزال لديك وقت لترك الملاحظات على قائمة بريد Amber إذا كنت لا توافق على شيء ما.
بعد ذلك ، بافتراض استمرار التبديل كما هو في الوقت الحالي ، أعتقد أن شكل السهم سيصبح الخيار الافتراضي الجديد. وبدون المرور إلى case
التالية وبتعبيرات موجزة من lambda (من الطبيعي جدًا أن يكون هناك حالة وعبارة واحدة في سطر واحد) ، يبدو switch
أكثر إحكاما ولا يؤثر على قابلية قراءة الكود. أنا متأكد من أنني لن أستخدم القولون إلا إذا كنت بحاجة إلى المرور عبر الممر.
ما رايك راضية عن كيف تحولت الأمور؟