الدليل الكامل لتبديل التعبيرات في Java 12


كان 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()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

لدى 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; // don't forget to `break` or you're screwed! break; case FALSE: result = false; break; case FILE_NOT_FOUND: // intermediate variable for demo purposes; // wait for it... var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; default: // ... here we go: // can't declare another variable with the same name var ex2 = new IllegalArgumentException("Seriously?!"); throw ex2; } 

توافق على أن هذا غير مريح للغاية. مثل العديد من خيارات التبديل الأخرى الموجودة في "الطبيعة" ، يحسب المثال أعلاه ببساطة قيمة المتغير ويعينه ، ولكن تم تجاوز التطبيق (أعلن 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()); // without default branch, the method wouldn't compile default: throw new IllegalArgumentException("Seriously?!"); } } 

هذا أفضل بكثير: لا يوجد متغير وهمية ، لا توجد 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()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

أعتقد أن هذا واضح جدًا: إذا كان 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"; // `default, case FILE_NOT_FOUND -> ...` does not work // (neither does other way around), but that makes // sense because using only `default` suffices default -> "insane"; }; 

يجب أن يكون السلوك واضحًا: ينتج عن 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"); // in colon-form, if `ternaryBool` is `TRUE` or `FALSE`, // we would see both messages; in arrow-form, only one // branch is executed default -> System.out.println("Bool was insane"); } 

من المهم معرفة أن هذا لا علاقة له بما إذا كنت تستخدم رمز التبديل كتعبير أو عبارة. العامل الحاسم هنا هو السهم ضد القولون.


كتل المشغل


كما هو الحال في lambdas ، يمكن أن يشير السهم إلى إما عامل واحد (على النحو الوارد أعلاه) أو كتلة مظللة بأقواس مجعدة:


 boolean result = switch(Bool.random()) { case TRUE -> { System.out.println("Bool true"); // return with `break`, not `return` break true; } case FALSE -> { System.out.println("Bool false"); break false; } case FILE_NOT_FOUND -> { var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; } default -> { var ex = new IllegalArgumentException("Seriously?!"); throw ex; } }; 

تتمتع الكتل التي يجب إنشاؤها لمشغلي الخطوط المتعددة بميزة إضافية (وهو أمر غير مطلوب عند استخدام نقطتين) ، مما يعني أنه لاستخدام أسماء المتغيرات نفسها في فروع مختلفة ، لا يتطلب 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"; // note that we don't throw the exception! // but it's `Serializable`, so it matches the target type default -> new IllegalArgumentException("insane"); }; 

ماذا سيحدث الان؟


 // compiler infers super type of `String` and // `IllegalArgumentException` ~> `Serializable` var serializableMessage = switch (bool) { case TRUE, FALSE -> "sane"; // note that we don't throw the exception! default -> new IllegalArgumentException("insane"); }; 

(لاستخدام نوع var ، اقرأ في آخر 26 مقالة توصيات لاستخدام نوع var في Java - ملاحظة من المترجم)


إذا كان النوع المستهدف غير معروف ، نظرًا لحقيقة أننا نستخدم var ، فسيتم حساب النوع من خلال البحث عن النوع الأكثر تحديدًا للأنواع التي أنشأتها الفروع.


العودة المبكرة


نتيجة الاختلاف بين التعبير switch هي أنه يمكنك استخدام return لإنهاء switch :


 public String sanity(Bool ternaryBool) { switch (ternaryBool) { // `return` is only possible from block case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

... لا يمكنك استخدام return داخل تعبير ...


 public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) { // this does not compile - error: // "return outside of enclosing switch expression" case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

هذا أمر منطقي سواء كنت تستخدم سهمًا أو نقطتين.


تغطي جميع الخيارات


إذا استخدمت switch كمشغل ، فلا يهم إذا كانت جميع الخيارات مغطاة أم لا. بالطبع ، يمكنك تخطي case عن طريق الخطأ ، ولن تعمل الشفرة بشكل صحيح ، ولكن المترجم لا يهتم - أنت ، IDE الخاص بك وأدوات تحليل الشفرة الخاصة بك ستترك مع هذا وحده.


تبديل التعبيرات تفاقم هذه المشكلة. أين يجب أن تذهب التبديل إذا كانت التسمية المطلوبة مفقودة؟ الإجابة الوحيدة التي يمكن لـ Java تقديمها هي إرجاع قيمة null لأنواع المراجع والقيمة الافتراضية للأولويات. هذا من شأنه أن يسبب الكثير من الأخطاء في الكود الرئيسي.


لمنع مثل هذه النتيجة ، يمكن للمترجم مساعدتك. بالنسبة لبيانات التبديل ، سيصر المترجم على تغطية جميع الخيارات الممكنة. دعونا نلقي نظرة على مثال قد يؤدي إلى حدوث خطأ في الترجمة:


 // compile error: // "the switch expression does not cover all possible input values" boolean result = switch (ternaryBool) { case TRUE -> true; // no case for `FALSE` case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

الحل التالي مثير للاهتمام: إضافة الفرع default ستعمل بالتأكيد على إصلاح الخطأ ، لكن هذا ليس هو الحل الوحيد - لا يزال بإمكانك إضافة case لـ FALSE .


 // compiles without `default` branch because // all cases for `ternaryBool` are covered boolean result = switch (ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

نعم ، سيتمكن المحول البرمجي أخيرًا من تحديد ما إذا كانت جميع قيم التعداد قد تمت تغطيتها (ما إذا كانت جميع الخيارات مستنفدة) ، ولا يتم تعيين قيم افتراضية غير مجدية! دعنا نجلس لحظة في امتنان صامت.


ومع ذلك ، هذا لا يزال يثير سؤال واحد. ماذا لو أخذ شخص ما وتحول 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 أكثر إحكاما ولا يؤثر على قابلية قراءة الكود. أنا متأكد من أنني لن أستخدم القولون إلا إذا كنت بحاجة إلى المرور عبر الممر.


ما رايك راضية عن كيف تحولت الأمور؟

Source: https://habr.com/ru/post/ar443464/


All Articles