هل يجب أن تحاول...الالتقاط داخل الحلقة أو خارجها؟

StackOverflow https://stackoverflow.com/questions/141560

سؤال

لدي حلقة تبدو مثل هذا:

for (int i = 0; i < max; i++) {
    String myString = ...;
    float myNum = Float.parseFloat(myString);
    myFloats[i] = myNum;
}

هذا هو المحتوى الرئيسي لطريقة هدفها الوحيد هو إرجاع مصفوفة العوامات.أريد أن تعود هذه الطريقة null إذا كان هناك خطأ، لذلك أضع الحلقة داخل ملف try...catch كتلة، مثل هذا:

try {
    for (int i = 0; i < max; i++) {
        String myString = ...;
        float myNum = Float.parseFloat(myString);
        myFloats[i] = myNum;
    }
} catch (NumberFormatException ex) {
    return null;
}

ولكن بعد ذلك فكرت أيضًا في وضع try...catch كتلة داخل الحلقة، مثل هذا:

for (int i = 0; i < max; i++) {
    String myString = ...;
    try {
        float myNum = Float.parseFloat(myString);
    } catch (NumberFormatException ex) {
        return null;
    }
    myFloats[i] = myNum;
}

هل هناك أي سبب، أداء أو غير ذلك، لتفضيل أحدهما على الآخر؟


يحرر: يبدو أن هناك إجماعًا على أنه من الأفضل وضع الحلقة داخل المحاولة/الالتقاط، وربما داخل طريقتها الخاصة.ومع ذلك، لا يزال هناك جدل حول أيهما أسرع.هل يمكن لأي شخص أن يختبر هذا ويعود بإجابة موحدة؟

هل كانت مفيدة؟

المحلول 3

حسنًا، بعد قال جيفري إل ويتليدج أنه لم يكن هناك فرق في الأداء (اعتبارًا من عام 1997)، ذهبت واختبرته.قمت بتشغيل هذا المعيار الصغير:

public class Main {

    private static final int NUM_TESTS = 100;
    private static int ITERATIONS = 1000000;
    // time counters
    private static long inTime = 0L;
    private static long aroundTime = 0L;

    public static void main(String[] args) {
        for (int i = 0; i < NUM_TESTS; i++) {
            test();
            ITERATIONS += 1; // so the tests don't always return the same number
        }
        System.out.println("Inside loop: " + (inTime/1000000.0) + " ms.");
        System.out.println("Around loop: " + (aroundTime/1000000.0) + " ms.");
    }
    public static void test() {
        aroundTime += testAround();
        inTime += testIn();
    }
    public static long testIn() {
        long start = System.nanoTime();
        Integer i = tryInLoop();
        long ret = System.nanoTime() - start;
        System.out.println(i); // don't optimize it away
        return ret;
    }
    public static long testAround() {
        long start = System.nanoTime();
        Integer i = tryAroundLoop();
        long ret = System.nanoTime() - start;
        System.out.println(i); // don't optimize it away
        return ret;
    }
    public static Integer tryInLoop() {
        int count = 0;
        for (int i = 0; i < ITERATIONS; i++) {
            try {
                count = Integer.parseInt(Integer.toString(count)) + 1;
            } catch (NumberFormatException ex) {
                return null;
            }
        }
        return count;
    }
    public static Integer tryAroundLoop() {
        int count = 0;
        try {
            for (int i = 0; i < ITERATIONS; i++) {
                count = Integer.parseInt(Integer.toString(count)) + 1;
            }
            return count;
        } catch (NumberFormatException ex) {
            return null;
        }
    }
}

لقد قمت بفحص الكود الثانوي الناتج باستخدام javap للتأكد من عدم تضمين أي شيء.

وأظهرت النتائج أنه، على افتراض تحسينات JIT ضئيلة، جيفري على حق;هناك على الاطلاق لا يوجد فرق في الأداء على Java 6 وSun Client VM (لم أتمكن من الوصول إلى الإصدارات الأخرى).يكون الفارق الزمني الإجمالي في حدود بضعة أجزاء من الثانية خلال الاختبار بأكمله.

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

نصائح أخرى

أداء:

لا يوجد أي اختلاف في الأداء على الإطلاق في مكان وضع بنيات المحاولة/الالتقاط.داخليًا، يتم تنفيذها كجدول نطاق تعليمات برمجية في بنية يتم إنشاؤها عند استدعاء الطريقة.أثناء تنفيذ الطريقة، تكون هياكل المحاولة/الالتقاط خارج الصورة تمامًا ما لم يحدث رمي، ثم تتم مقارنة موقع الخطأ بالجدول.

وهنا مرجع: http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

تم وصف الجدول في منتصف الطريق تقريبًا.

أداء:مثل جيفري وقال في رده، في جافا لا يحدث فرقا كبيرا.

عمومًا, ، لسهولة قراءة التعليمات البرمجية، يعتمد اختيارك لمكان التقاط الاستثناء على ما إذا كنت تريد أن تستمر الحلقة في المعالجة أم لا.

في المثال الخاص بك عدت عند التقاط استثناء.في هذه الحالة، سأضع المحاولة/الالتقاط حول الحلقة.إذا كنت تريد ببساطة الحصول على قيمة سيئة مع الاستمرار في المعالجة، ضعها في الداخل.

الطريقة الثالثة:يمكنك دائمًا كتابة طريقة ParseFloat الثابتة الخاصة بك والتعامل مع معالجة الاستثناءات بهذه الطريقة بدلاً من الحلقة الخاصة بك.جعل معالجة الاستثناء معزولة عن الحلقة نفسها!

class Parsing
{
    public static Float MyParseFloat(string inputValue)
    {
        try
        {
            return Float.parseFloat(inputValue);
        }
        catch ( NumberFormatException e )
        {
            return null;
        }
    }

    // ....  your code
    for(int i = 0; i < max; i++) 
    {
        String myString = ...;
        Float myNum = Parsing.MyParseFloat(myString);
        if ( myNum == null ) return;
        myFloats[i] = (float) myNum;
    }
}

في حين أن الأداء قد يكون هو نفسه وما "يبدو" أفضل هو أمر شخصي للغاية، إلا أنه لا يزال هناك اختلاف كبير جدًا في الوظيفة.خذ المثال التالي:

Integer j = 0;
    try {
        while (true) {
            ++j;

            if (j == 20) { throw new Exception(); }
            if (j%4 == 0) { System.out.println(j); }
            if (j == 40) { break; }
        }
    } catch (Exception e) {
        System.out.println("in catch block");
    }

حلقة while موجودة داخل كتلة محاولة الالتقاط، ويتم زيادة المتغير "j" حتى يصل إلى 40، ويتم طباعته عندما تكون قيمة j mod 4 تساوي صفرًا ويتم طرح استثناء عندما يصل j إلى 20.

وقبل أي تفاصيل إليكم المثال الآخر:

Integer i = 0;
    while (true) {
        try {
            ++i;

            if (i == 20) { throw new Exception(); }
            if (i%4 == 0) { System.out.println(i); }
            if (i == 40) { break; }

        } catch (Exception e) { System.out.println("in catch block"); }
    }

نفس المنطق المذكور أعلاه، والفرق الوحيد هو أن كتلة المحاولة/الالتقاط موجودة الآن داخل حلقة while.

هنا يأتي الإخراج (أثناء المحاولة/الالتقاط):

4
8
12 
16
in catch block

والمخرج الآخر (جرب/التقط بينما):

4
8
12
16
in catch block
24
28
32
36
40

هناك لديك فرق كبير جدا:

أثناء المحاولة/الالتقاط يخرج من الحلقة

حاول/الحق مع إبقاء الحلقة نشطة

وأنا أتفق مع جميع المشاركات الأداء وسهولة القراءة.ومع ذلك، هناك حالات يكون فيها الأمر مهمًا حقًا.ذكر شخصان آخران هذا، ولكن قد يكون من الأسهل رؤيته مع الأمثلة.

خذ بعين الاعتبار هذا المثال المعدل قليلاً:

public static void main(String[] args) {
    String[] myNumberStrings = new String[] {"1.2345", "asdf", "2.3456"};
    ArrayList asNumbers = parseAll(myNumberStrings);
}

public static ArrayList parseAll(String[] numberStrings){
    ArrayList myFloats = new ArrayList();

    for(int i = 0; i < numberStrings.length; i++){
        myFloats.add(new Float(numberStrings[i]));
    }
    return myFloats;
}

إذا كنت تريد أن تقوم طريقة parseAll() بإرجاع قيمة فارغة في حالة وجود أي أخطاء (مثل المثال الأصلي)، فستضع محاولة/التقاط في الخارج مثل هذا:

public static ArrayList parseAll1(String[] numberStrings){
    ArrayList myFloats = new ArrayList();
    try{
        for(int i = 0; i < numberStrings.length; i++){
            myFloats.add(new Float(numberStrings[i]));
        }
    } catch (NumberFormatException nfe){
        //fail on any error
        return null;
    }
    return myFloats;
}

في الواقع، ربما ينبغي عليك إرجاع خطأ هنا بدلاً من null، وبشكل عام لا أحب الحصول على إرجاعات متعددة، لكنك حصلت على الفكرة.

من ناحية أخرى، إذا كنت تريد أن تتجاهل المشاكل فقط، وأن تقوم بتحليل ما تستطيع من سلاسل، فيمكنك وضع المحاولة/الالتقاط في داخل الحلقة مثل هذا:

public static ArrayList parseAll2(String[] numberStrings){
    ArrayList myFloats = new ArrayList();

    for(int i = 0; i < numberStrings.length; i++){
        try{
            myFloats.add(new Float(numberStrings[i]));
        } catch (NumberFormatException nfe){
            //don't add just this one
        }
    }

    return myFloats;
}

كما ذكرنا سابقًا، الأداء هو نفسه.ومع ذلك، تجربة المستخدم ليست متطابقة بالضرورة.في الحالة الأولى، سوف تفشل بسرعة (أي.بعد الخطأ الأول)، ولكن إذا وضعت كتلة المحاولة/الالتقاط داخل الحلقة، فيمكنك التقاط جميع الأخطاء التي سيتم إنشاؤها لاستدعاء معين للأسلوب.عند تحليل مجموعة من القيم من سلاسل حيث تتوقع بعض أخطاء التنسيق، هناك بالتأكيد حالات تريد أن تكون قادرًا على تقديم جميع الأخطاء فيها إلى المستخدم حتى لا يحتاج إلى محاولة إصلاحها واحدًا تلو الآخر .

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

طالما أنك على دراية بما تحتاج إلى تحقيقه في الحلقة، يمكنك وضع المحاولة خارج الحلقة.لكن من المهم أن تفهم أن الحلقة ستنتهي بمجرد حدوث الاستثناء وقد لا يكون هذا هو ما تريده دائمًا.هذا في الواقع خطأ شائع جدًا في البرامج المستندة إلى Java.يحتاج الأشخاص إلى معالجة عدد من العناصر، مثل إفراغ قائمة الانتظار، والاعتماد بشكل خاطئ على بيان محاولة/التقاط خارجي يتعامل مع جميع الاستثناءات المحتملة.من الممكن أيضًا أن يتعاملوا مع استثناء محدد فقط داخل الحلقة ولا يتوقعون حدوث أي استثناء آخر.ثم إذا حدث استثناء لم تتم معالجته داخل الحلقة، فسيتم "استباق" الحلقة، ومن المحتمل أن تنتهي قبل الأوان وتتعامل عبارة الالتقاط الخارجية مع الاستثناء.

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

في الأمثلة الخاصة بك لا يوجد فرق وظيفي.أجد المثال الأول الخاص بك أكثر قابلية للقراءة.

يجب أن تفضل الإصدار الخارجي على الإصدار الداخلي.هذه مجرد نسخة محددة من القاعدة، قم بتحريك أي شيء خارج الحلقة التي يمكنك نقلها خارج الحلقة.اعتمادًا على مترجم IL ومترجم JIT، قد ينتهي أو لا ينتهي الأمر بإصدارين مختلفين بخصائص أداء مختلفة.

وفي ملاحظة أخرى، ربما ينبغي عليك إلقاء نظرة على float.TryParse أو Convert.ToFloat.

إذا قمت بوضع المحاولة/الالتقاط داخل الحلقة، فسوف تستمر في التكرار بعد حدوث استثناء.إذا وضعته خارج الحلقة فسوف تتوقف بمجرد طرح الاستثناء.

وجهة نظري هي أن كتل المحاولة/الالتقاط ضرورية لضمان معالجة الاستثناءات بشكل صحيح، ولكن إنشاء مثل هذه الكتل له آثار على الأداء.نظرًا لأن الحلقات تحتوي على حسابات متكررة مكثفة، فمن غير المستحسن وضع كتل المحاولة/الالتقاط داخل الحلقات.بالإضافة إلى ذلك، يبدو أنه عند حدوث هذه الحالة، غالبًا ما يتم اكتشاف "الاستثناء" أو "RuntimeException".يجب تجنب اكتشاف RuntimeException في التعليمات البرمجية.مرة أخرى، إذا كنت تعمل في شركة كبيرة، فمن الضروري تسجيل هذا الاستثناء بشكل صحيح، أو إيقاف حدوث الاستثناء في وقت التشغيل.بيت القصيد من هذا الوصف هو PLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS

يؤدي إعداد إطار مكدس خاص للمحاولة/الالتقاط إلى إضافة حمل إضافي، ولكن قد يتمكن JVM من اكتشاف حقيقة عودتك وتحسين ذلك.

اعتمادًا على عدد التكرارات، من المحتمل أن يكون فرق الأداء ضئيلًا.

ومع ذلك فأنا أتفق مع الآخرين على أن وجودها خارج الحلقة يجعل جسم الحلقة يبدو أكثر نظافة.

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

إذا كان بالداخل، فسوف تحصل على الحمل الزائد لبنية المحاولة/الالتقاط N مرات، بدلاً من مرة واحدة فقط بالخارج.


في كل مرة يتم فيها استدعاء بنية Try/Catch، فإنها تزيد من الحمل على تنفيذ الطريقة.فقط القليل من علامات الذاكرة والمعالج اللازمة للتعامل مع البنية.إذا كنت تقوم بتشغيل حلقة 100 مرة، ومن أجل الافتراض، لنفترض أن التكلفة هي علامة واحدة لكل مكالمة محاولة/التقاط، فإن إجراء المحاولة/الالتقاط داخل الحلقة يكلفك 100 علامة، مقابل علامة واحدة فقط إذا كانت خارج الحلقة.

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

ضعه في الداخل.يمكنك الاستمرار في المعالجة (إذا أردت) أو يمكنك طرح استثناء مفيد يخبر العميل بقيمة myString وفهرس المصفوفة التي تحتوي على القيمة السيئة.أعتقد أن NumberFormatException سيخبرك بالفعل بالقيمة السيئة ولكن المبدأ هو وضع جميع البيانات المفيدة في الاستثناءات التي تطرحها.فكر فيما قد يثير اهتمامك في مصحح الأخطاء في هذه المرحلة من البرنامج.

يعتبر:

try {
   // parse
} catch (NumberFormatException nfe){
   throw new RuntimeException("Could not parse as a Float: [" + myString + 
                              "] found at index: " + i, nfe);
} 

في وقت الحاجة، ستقدر حقًا استثناءً مثل هذا مع أكبر قدر ممكن من المعلومات فيه.

أود أن أضيف بلدي 0.02c حول اعتبارين متنافسين عند النظر إلى المشكلة العامة المتعلقة بمكان وضع معالجة الاستثناءات:

  1. "الأوسع" مسؤولية try-catch كتلة (أي.خارج الحلقة في حالتك) يعني أنه عند تغيير الكود في مرحلة لاحقة، قد تقوم عن طريق الخطأ بإضافة سطر يتم التعامل معه بواسطة الكود الموجود لديك catch حاجز؛ربما عن غير قصد.في حالتك، يكون هذا أقل احتمالًا لأنك تلتقط ملفًا بشكل صريح NumberFormatException

  2. "أضيق" مسؤولية try-catch الكتلة، تصبح إعادة الهيكلة أكثر صعوبة.خاصة عندما تقوم (كما في حالتك) بتنفيذ تعليمات "غير محلية" من داخل ملف catch كتلة ( return null إفادة).

ذلك يعتمد على التعامل مع الفشل.إذا كنت تريد فقط تخطي عناصر الخطأ، فجرب ما يلي:

for(int i = 0; i < max; i++) {
    String myString = ...;
    try {
        float myNum = Float.parseFloat(myString);
        myFloats[i] = myNum;
    } catch (NumberFormatException ex) {
        --i;
    }
}

في أي حالة أخرى أفضل المحاولة في الخارج.الكود أكثر قابلية للقراءة وأكثر نظافة.ربما يكون من الأفضل طرح IllegalArgumentException في حالة الخطأ بدلاً من ذلك في حالة إرجاع قيمة فارغة.

سأضع مبلغ 0.02 دولار الخاص بي.في بعض الأحيان ينتهي بك الأمر إلى الحاجة إلى إضافة "أخيرًا" لاحقًا في التعليمات البرمجية الخاصة بك (لأنه من يكتب التعليمات البرمجية الخاصة به بشكل مثالي في المرة الأولى؟).في تلك الحالات، فجأة يصبح من المنطقي إجراء المحاولة/الالتقاط خارج الحلقة.على سبيل المثال:

try {
    for(int i = 0; i < max; i++) {
        String myString = ...;
        float myNum = Float.parseFloat(myString);
        dbConnection.update("MY_FLOATS","INDEX",i,"VALUE",myNum);
    }
} catch (NumberFormatException ex) {
    return null;
} finally {
    dbConnection.release();  // Always release DB connection, even if transaction fails.
}

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

جانب آخر لم يتم ذكره في ما سبق هو حقيقة أن كل محاولة للصيد لها بعض التأثير على المكدس، والذي يمكن أن يكون له آثار على الأساليب العودية.

إذا كان الأسلوب "outer()" يستدعي الأسلوب "inner()" (والذي قد يطلق على نفسه اسمًا متكررًا)، فحاول تحديد موقع محاولة الالتقاط في الأسلوب "outer()" إن أمكن.مثال بسيط على "تعطل المكدس" الذي نستخدمه في فئة الأداء يفشل عند حوالي 6400 إطار عندما تكون محاولة الالتقاط في الطريقة الداخلية، وعند حوالي 11600 عندما تكون في الطريقة الخارجية.

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

إذا كنت تريد التقاط الاستثناء لكل تكرار، أو التحقق من التكرار الذي تم طرح الاستثناء فيه والتقاط كل الاستثناءات في التكرار، ضع محاولة...التقاط داخل الحلقة.لن يؤدي هذا إلى كسر الحلقة في حالة حدوث استثناء ويمكنك اكتشاف كل استثناء في كل تكرار خلال الحلقة.

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

كل هذا يتوقف على حاجتك.أفضّل استخدام المحاولة...التقاط داخل الحلقة أثناء النشر، لأنه في حالة حدوث استثناء، فإن النتائج ليست غامضة ولن تنقطع الحلقة ويتم تنفيذها بالكامل.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top