هل من المفيد استخدام "goto" بلغة تدعم الحلقات والوظائف؟إذا كان الأمر كذلك لماذا؟

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

سؤال

لقد كنت منذ فترة طويلة تحت الانطباع بأن goto لا ينبغي أبدا أن تستخدم إذا كان ذلك ممكنا.أثناء تصفحي لـ libavcodec (المكتوب بلغة C) في أحد الأيام، لاحظت استخدامات متعددة له.هل هو مفيد من أي وقت مضى للاستخدام goto بلغة تدعم الحلقات والوظائف؟إذا كان الأمر كذلك لماذا؟

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

المحلول

هناك عدة أسباب لاستخدام عبارة "goto" التي أعرفها (لقد تحدث البعض عن هذا بالفعل):

الخروج من الوظيفة بشكل نظيف

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

الخروج من الحلقات المتداخلة

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

تحسينات الأداء على مستوى منخفض

هذا صالح فقط في التعليمات البرمجية ذات الأداء الحرج، ولكن عبارات goto يتم تنفيذها بسرعة كبيرة ويمكن أن تمنحك دفعة عند التنقل عبر إحدى الوظائف.ومع ذلك، يعد هذا سيفًا ذو حدين، لأن المترجم عادةً لا يمكنه تحسين التعليمات البرمجية التي تحتوي على gotos.

لاحظ أنه في كل هذه الأمثلة، يقتصر gotos على نطاق وظيفة واحدة.

نصائح أخرى

كل من هو ضدgoto يستشهد، بشكل مباشر أو غير مباشر، بـ Edsger Dijkstra GoTo تعتبر ضارة مقال لإثبات موقفهم.مقالة Dijkstra سيئة للغاية تقريبًا لا شئ للقيام بالطريقة goto يتم استخدام العبارات هذه الأيام، وبالتالي فإن ما تقوله المقالة لا ينطبق إلا على مشهد البرمجة الحديث.ال goto- تقترب الميمات الأقل الآن من الدين، وصولاً إلى الكتب المقدسة التي تمليها من الأعلى، وكهنةها الكبار وتجنب (أو ما هو أسوأ) من الزنادقة المتصورين.

دعونا نضع ورقة ديكسترا في سياقها لإلقاء القليل من الضوء على الموضوع.

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

هذا هو "الاستخدام الجامح لبيان الانتقال" الذي كان ديكسترا يعترض عليه في ورقته البحثية عام 1968. هذا هي البيئة التي عاش فيها والتي دفعته إلى كتابة تلك الورقة.إن القدرة على القفز إلى أي مكان تريده في التعليمات البرمجية الخاصة بك وفي أي وقت تريده هو ما كان ينتقده ويطالب بإيقافه.مقارنة ذلك بالقوى الضعيفة لـ goto في لغة C أو غيرها من اللغات الحديثة أمر مثير للضحك.

أستطيع بالفعل سماع الهتافات المرتفعة للطائفيين وهم يواجهون الزنديق."لكن،" سوف يهتفون، "يمكنك أن تجعل قراءة التعليمات البرمجية أمرًا صعبًا للغاية goto في C." أوه نعم؟يمكنك جعل قراءة التعليمات البرمجية صعبة للغاية بدونها goto أيضًا.مثل هذه:

#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

ليس أ goto في الأفق، لذا يجب أن يكون من السهل قراءته، أليس كذلك؟أو ماذا عن هذا:

a[900];     b;c;d=1     ;e=1;f;     g;h;O;      main(k,
l)char*     *l;{g=      atoi(*      ++l);       for(k=
0;k*k<      g;b=k       ++>>1)      ;for(h=     0;h*h<=
g;++h);     --h;c=(     (h+=g>h     *(h+1))     -1)>>1;
while(d     <=g){       ++O;for     (f=0;f<     O&&d<=g
;++f)a[     b<<5|c]     =d++,b+=    e;for(      f=0;f<O
&&d<=g;     ++f)a[b     <<5|c]=     d++,c+=     e;e= -e
;}for(c     =0;c<h;     ++c){       for(b=0     ;b<k;++
b){if(b     <k/2)a[     b<<5|c]     ^=a[(k      -(b+1))
<<5|c]^=    a[b<<5      |c]^=a[     (k-(b+1     ))<<5|c]
;printf(    a[b<<5|c    ]?"%-4d"    :"    "     ,a[b<<5
|c]);}      putchar(    '\n');}}    /*Mike      Laman*/

لا goto هناك سواء.ولذلك يجب أن تكون قابلة للقراءة.

ما هي وجهة نظري مع هذه الأمثلة؟ليست ميزات اللغة هي التي تجعل التعليمات البرمجية غير قابلة للقراءة ولا يمكن صيانتها.ليس بناء الجملة هو الذي يفعل ذلك.المبرمجون السيئون هم الذين يسببون هذا.ويمكن للمبرمجين السيئين، كما ترون في هذا البند أعلاه، أن يفعلوا ذلك أي ميزة اللغة غير قابلة للقراءة وغير قابلة للاستخدام.مثل ال for حلقات هناك.(يمكنك رؤيتهم، أليس كذلك؟)

ولكي نكون منصفين، فإن بعض بنيات اللغة أسهل في إساءة استخدامها من غيرها.إذا كنت مبرمجًا بلغة C، فأنا سأدقق أكثر في حوالي 50% من استخدامات لغة C. #define قبل وقت طويل من ذهابي في حملة صليبية ضد goto!

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

  1. ورقة ديكسترا على goto تمت كتابة البيانات لبيئة البرمجة حيث goto كان كثيرمن المحتمل أن تكون ضارة أكثر مما هي عليه في معظم اللغات الحديثة التي ليست مجمعة.
  2. التخلص تلقائيًا من جميع الاستخدامات goto وبسبب هذا الأمر يتعلق بالعقلاني مثل قوله "حاولت أن أستمتع مرة واحدة ولكن لم يعجبني الآن ، فأنا الآن ضدها".
  3. هناك استخدامات مشروعة للحديث (فقر الدم) goto البيانات في الكود التي لا يمكن استبدالها بشكل كاف بإنشاءات أخرى.
  4. هناك، بطبيعة الحال، استخدامات غير مشروعة لنفس العبارات.
  5. هناك أيضًا استخدامات غير مشروعة لبيانات التحكم الحديثة مثل "godo"رجس حيث دائما كاذبة do الحلقة مكسورة من الاستخدام break بدلاً من أ goto.هذه غالبا ما تكون أسوأ من الاستخدام الحكيم ل goto.

إن إطاعة أفضل الممارسات بشكل أعمى ليس من أفضل الممارسات.فكرة تجنب goto إن الشكل الأساسي للتحكم في التدفق هو تجنب إنتاج كود معكرونة غير قابل للقراءة.إذا تم استخدامها بشكل مقتصد في الأماكن الصحيحة، فقد تكون في بعض الأحيان أبسط وأوضح طريقة للتعبير عن الفكرة.يستخدمها والتر برايت، مبتكر مترجم Zortech C++ ولغة البرمجة D، بشكل متكرر، ولكن بحكمة.حتى مع goto البيانات، رمزه لا يزال قابلاً للقراءة تماما.

الحد الأدنى:تجنب goto من أجل تجنب goto لا معنى له.ما تريد تجنبه حقًا هو إنتاج تعليمات برمجية غير قابلة للقراءة.إذا كان لديك gotoالكود -laden قابل للقراءة، فلا حرج في ذلك.

منذ goto يجعل التفكير حول تدفق البرنامج صعبًا1 (الملقب ب.""رمز السباغيتي"))، goto يُستخدم بشكل عام فقط للتعويض عن الميزات المفقودة:استخدام goto قد يكون مقبولاً في الواقع، ولكن فقط إذا كانت اللغة لا تقدم صيغة أكثر تنظيماً للحصول على نفس الهدف.خذ مثال الشك:

القاعدة التي نستخدمها في goto هي أن goto لا بأس به للانتقال إلى نقطة تنظيف خروج واحدة في إحدى الوظائف.

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

في معظم اللغات الأخرى، الاستخدام الوحيد المقبول هو goto هو الخروج من الحلقات المتداخلة.وحتى هناك، من الأفضل دائمًا رفع الحلقة الخارجية بطريقة خاصة واستخدامها return بدلاً من.

بخلاف ذلك، goto هي علامة على أنه لم يتم التفكير بشكل كافٍ في جزء معين من التعليمات البرمجية.


1 اللغات الحديثة التي تدعم goto تنفيذ بعض القيود (على سبيل المثال. goto قد لا ينتقل إلى الوظائف أو خارجها) ولكن المشكلة تظل كما هي بشكل أساسي.

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

حسنًا، هناك شيء واحد يكون دائمًا أسوأ منه goto's;استخدام غريب لمشغلي تدفق البرامج الآخرين لتجنب الانتقال إلى:

أمثلة:

    // 1
    try{
      ...
      throw NoErrorException;
      ...
    } catch (const NoErrorException& noe){
      // This is the worst
    } 


    // 2
    do {
      ...break; 
      ...break;
    } while (false);


    // 3
    for(int i = 0;...) { 
      bool restartOuter = false;
      for (int j = 0;...) {
        if (...)
          restartOuter = true;
      if (restartOuter) {
        i = -1;
      }
    }

etc
etc

في ج# يُحوّل إفادة لا يسمح بالسقوط.لذا اذهب إلى يتم استخدامه لنقل التحكم إلى ملصق حالة التبديل المحدد أو تقصير ملصق.

على سبيل المثال:

switch(value)
{
  case 0:
    Console.Writeln("In case 0");
    goto case 1;
  case 1:
    Console.Writeln("In case 1");
    goto case 2;
  case 2:
    Console.Writeln("In case 2");
    goto default;
  default:
    Console.Writeln("In default");
    break;
}

يحرر:يوجد استثناء واحد لقاعدة "ممنوع السقوط".يُسمح بالخطأ إذا لم يكن بيان الحالة يحتوي على رمز.

#ifdef TONGUE_IN_CHEEK

بيرل لديه goto الذي يسمح لك بتنفيذ مكالمات ذيل الرجل الفقير.:-ص

sub factorial {
    my ($n, $acc) = (@_, 1);
    return $acc if $n < 1;
    @_ = ($n - 1, $acc * $n);
    goto &factorial;
}

#endif

حسنًا، هذا لا علاقة له بـ C goto.والأهم من ذلك، أنا أتفق مع التعليقات الأخرى حول الاستخدام goto للتنظيف أو للتنفيذ جهاز داف, ، أو ما شابه ذلك.الأمر كله يتعلق بالاستخدام وليس إساءة الاستخدام.

(نفس التعليق يمكن أن ينطبق على longjmp, الاستثناءات call/cc, وما شابه ذلك --- لها استخدامات مشروعة، ولكن يمكن إساءة استخدامها بسهولة.على سبيل المثال، طرح استثناء لمجرد الهروب من بنية التحكم المتداخلة للغاية، في ظل ظروف غير استثنائية تمامًا.)

لقد كتبت أكثر من بضعة أسطر من لغة التجميع على مر السنين.في النهاية، يتم تجميع كل لغة عالية المستوى وصولاً إلى gotos.حسنًا، أطلق عليها اسم "الفروع" أو "القفزات" أو أي شيء آخر، لكنها كلمات أساسية.هل يمكن لأي شخص أن يكتب المجمّع goto-less؟

الآن بالتأكيد، يمكنك أن تشير إلى مبرمج Fortran أو C أو BASIC أن تشغيل الشغب باستخدام gotos هو وصفة لاسباجيتي بولونيز.لكن الحل ليس تجنبها، بل استخدامها بعناية.

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

نلقي نظرة على متى تستخدم Goto عند البرمجة بلغة C:

على الرغم من أن استخدام goto غالبًا ما يكون ممارسة برمجة سيئة (بالتأكيد يمكنك العثور على طريقة أفضل للقيام بـ XYZ)، إلا أن هناك أوقاتًا لا يكون فيها هذا خيارًا سيئًا حقًا.قد يجادل البعض بأنه عندما يكون ذلك مفيدًا، فهو الخيار الأفضل.

معظم ما يجب أن أقوله عن goto ينطبق فقط على لغة C.إذا كنت تستخدم C++، فلا يوجد سبب وجيه لاستخدام goto بدلاً من الاستثناءات.ومع ذلك، في لغة C، ليس لديك قوة آلية معالجة الاستثناءات، لذلك إذا كنت تريد فصل معالجة الأخطاء عن بقية منطق البرنامج الخاص بك، وتريد تجنب إعادة كتابة كود التنظيف عدة مرات خلال التعليمات البرمجية الخاصة بك، ثم يمكن أن يكون goto خيارًا جيدًا.

ماذا أعني؟قد يكون لديك بعض التعليمات البرمجية التي تبدو مثل هذا:

int big_function()
{
    /* do some work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* clean up*/
    return [success];
}

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

باستخدام goto, ، سيكون ذلك

int big_function()
{
    int ret_val = [success];
    /* do some work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
end:
    /* clean up*/
    return ret_val;
}

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

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


في كلمة واحدة، goto يجب دائمًا استخدامه بشكل مقتصد، وكملاذ أخير - ولكن هناك وقت ومكان لذلك.يجب ألا يكون السؤال "هل يجب عليك استخدامه" ولكن "هل هذا هو الخيار الأفضل" لاستخدامه.

أحد أسباب سوء استخدام goto، إلى جانب أسلوب البرمجة، هو أنه يمكنك استخدامه للإنشاء تداخل, ، لكن غير متداخلة الحلقات:

loop1:
  a
loop2:
  b
  if(cond1) goto loop1
  c
  if(cond2) goto loop2

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

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

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

للتوضيح، سأعطيك مثالًا لم يعرضه أحد هنا بعد:

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

تحتوي كل مجموعة من جدول التجزئة على 4 فتحات، ولدي مجموعة من المعايير لتحديد العنصر الذي سيتم استبداله عندما تكون المجموعة ممتلئة.ويعني هذا الآن إجراء ما يصل إلى ثلاث تمريرات عبر الدلو، مثل هذا:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    goto add;

// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
    goto add;

// Additional passes go here...

add:
// element is written to the hash table here

الآن إذا لم أستخدم goto، كيف سيبدو هذا الرمز؟

شيء من هذا القبيل:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    break;

if (add_index >= ELEMENTS_PER_BUCKET) {
  // Otherwise, find first empty element
  for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
    if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
      break;
  if (add_index >= ELEMENTS_PER_BUCKET)
   // Additional passes go here (nested further)...
}

// element is written to the hash table here

سيبدو الأمر أسوأ فأسوأ إذا تمت إضافة المزيد من التمريرات، في حين أن الإصدار الذي يحتوي على goto يحافظ على نفس مستوى المسافة البادئة في جميع الأوقات ويتجنب استخدام عبارات if الزائفة التي يتم تضمين نتيجتها في تنفيذ الحلقة السابقة.

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

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

إن المناقشة الأكثر عمقًا وعمقًا لعبارات goto واستخداماتها المشروعة والبنيات البديلة التي يمكن استخدامها بدلاً من "عبارات goto الفاضلة" ولكن يمكن إساءة استخدامها بسهولة مثل عبارات goto، هي مقال دونالد كنوث "البرمجة المنظمة مع عبارات goto"، في المسوحات الحاسوبية الصادرة في ديسمبر 1974 (المجلد 6، رقم.4.ص.261 - 301).

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

في وحدة Perl، قد ترغب أحيانًا في إنشاء إجراءات فرعية أو عمليات إغلاق سريعة.الأمر هو أنه بمجرد إنشاء الروتين الفرعي، كيف يمكنك الوصول إليه.يمكنك فقط تسميتها، ولكن بعد ذلك إذا كان الروتين الفرعي يستخدم caller() لن يكون مفيدًا قدر الإمكان.هذا هو المكان الذي goto &subroutine يمكن أن يكون الاختلاف مفيدًا.

هنا هو مثال سريع:

sub AUTOLOAD{
  my($self) = @_;
  my $name = $AUTOLOAD;
  $name =~ s/.*:://;

  *{$name} = my($sub) = sub{
    # the body of the closure
  }

  goto $sub;

  # nothing after the goto will ever be executed.
}

يمكنك أيضًا استخدام هذا النموذج من goto لتوفير شكل بدائي لتحسين الاتصال الخلفي.

sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;

  $tally *= $n--;
  @_ = ($n,$tally);
  goto &factorial;
}

( في بيرل 5 الإصدار 16 من شأنه أن يكون أفضل مكتوبة كما goto __SUB__; )

هناك وحدة ستقوم باستيراد ملف tail المعدل والذي سيتم استيراده recur إذا كنت لا تحب استخدام هذا النموذج من goto.

use Sub::Call::Tail;
sub AUTOLOAD {
  ...
  tail &$sub( @_ );
}

use Sub::Call::Recur;
sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;
  recur( $n-1, $tally * $n );
}

معظم الأسباب الأخرى للاستخدام goto من الأفضل القيام بذلك باستخدام كلمات رئيسية أخرى.

يحب redoالقليل من الكود:

LABEL: ;
...
goto LABEL if $x;
{
  ...
  redo if $x;
}

أو الذهاب إلى last من القليل من التعليمات البرمجية من أماكن متعددة:

goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
  last if $x;
  ...
  last if $y
  ...
}

إذا كان الأمر كذلك لماذا؟

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

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

libavcodec عبارة عن جزء من التعليمات البرمجية حساس للأداء.من المحتمل أن يكون التعبير المباشر عن تدفق التحكم أولوية، لأنه يميل إلى العمل بشكل أفضل.

وكذلك لم ينفذ أحد عبارة "COME FROM" على الإطلاق....

أجد أن استخدام do{} while(false) مقزز تمامًا.من الممكن أن يقنعني أنه ضروري في بعض الحالات الغريبة، ولكن ليس أبدًا أنه رمز نظيف ومعقول.

إذا كان عليك القيام ببعض هذه الحلقات، فلماذا لا تجعل الاعتماد على متغير العلم واضحًا؟

for (stepfailed=0 ; ! stepfailed ; /*empty*/)

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

على سبيل المثال، انظر إلى مقتطفي الكود التاليين:

If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)

رمز مكافئ مع GOTO

If A == 0 Then GOTO FINAL EndIf
   A = 0
FINAL:
Write("Value of A:" + A)

أول شيء نفكر فيه هو أن نتيجة كلتا الجزأين من التعليمات البرمجية ستكون "قيمة A:0" (نفترض تنفيذًا بدون توازي بالطبع)

هذا غير صحيح:في العينة الأولى، ستكون A دائمًا 0، ولكن في العينة الثانية (مع عبارة GOTO) قد لا تكون A 0.لماذا؟

والسبب هو أنه من نقطة أخرى في البرنامج يمكنني إدراج ملف GOTO FINAL دون التحكم في قيمة A.

هذا المثال واضح جدًا، ولكن مع ازدياد تعقيد البرامج، تزداد صعوبة رؤية هذا النوع من الأشياء.

يمكن العثور على المواد ذات الصلة في المقال الشهير للسيد.ديكسترا "قضية ضد بيان GO TO"

1) الاستخدام الأكثر شيوعًا لـ goto الذي أعرفه هو محاكاة معالجة الاستثناءات في اللغات التي لا توفرها، وبالتحديد في لغة C.(الكود الذي قدمه برنامج Nuclear أعلاه هو مجرد ذلك.) انظر إلى كود مصدر Linux وسترى عددًا كبيرًا من gotos مستخدمًا بهذه الطريقة؛كان هناك حوالي 100000 gotos في كود Linux وفقًا لمسح سريع تم إجراؤه في عام 2013: http://blog.regehr.org/archives/894.تم ذكر استخدام Goto في دليل أسلوب ترميز Linux: https://www.kernel.org/doc/Documentation/CodingStyle.تمامًا كما تتم محاكاة البرمجة الموجهة للكائنات باستخدام بنيات مملوءة بمؤشرات وظيفية، فإن goto لها مكانها في برمجة C.إذن من هو على حق:Dijkstra أو Linus (وجميع برامج ترميز Linux kernel)؟إنها النظرية مقابل.ممارسة في الأساس.

ومع ذلك، هناك المشكلة المعتادة لعدم وجود دعم على مستوى المترجم والتحقق من البنيات/الأنماط الشائعة:فمن الأسهل استخدامها بشكل خاطئ وإدخال الأخطاء دون إجراء فحوصات وقت الترجمة.يوفر Windows وVisual C++ ولكن في الوضع C معالجة الاستثناءات عبر SEH/VEH لهذا السبب بالذات:الاستثناءات مفيدة حتى خارج لغات OOP، على سبيل المثال.باللغة الإجرائية.لكن المترجم لا يمكنه دائمًا حفظ لحم الخنزير المقدد الخاص بك، حتى لو كان يقدم دعمًا نحويًا للاستثناءات في اللغة.لننظر كمثال على الحالة الأخيرة إلى الخطأ الشهير "goto Fail" الخاص بـ Apple SSL، والذي أدى إلى تكرار عملية goto واحدة مما أدى إلى عواقب وخيمة (https://www.Imperialviolet.org/2014/02/22/applebug.html):

if (something())
  goto fail;
  goto fail; // copypasta bug
printf("Never reached\n");
fail:
  // control jumps here

يمكن أن يكون لديك نفس الخطأ تمامًا باستخدام الاستثناءات المدعومة من المترجم، على سبيل المثال.في لغة C++:

struct Fail {};

try {
  if (something())
    throw Fail();
    throw Fail(); // copypasta bug
  printf("Never reached\n");
}
catch (Fail&) {
  // control jumps here
}

ولكن يمكن تجنب كلا النوعين من الخطأ إذا قام المترجم بتحليلك وتحذيرك بشأن التعليمات البرمجية التي لا يمكن الوصول إليها.على سبيل المثال، يؤدي التحويل البرمجي باستخدام Visual C++ عند مستوى التحذير /W4 إلى العثور على الخطأ في كلتا الحالتين.تمنع Java على سبيل المثال التعليمات البرمجية التي لا يمكن الوصول إليها (حيث يمكن العثور عليها!) لسبب وجيه جدًا:من المحتمل أن يكون هناك خطأ في كود جو العادي.طالما أن بنية goto لا تسمح بالأهداف التي لا يستطيع المترجم اكتشافها بسهولة، مثل gotos إلى العناوين المحسوبة (**)، فليس من الصعب على المترجم العثور على تعليمات برمجية لا يمكن الوصول إليها داخل دالة مع gotos بدلاً من استخدام Dijkstra - الكود المعتمد

(**) هامش:يمكن الانتقال إلى أرقام الأسطر المحسوبة في بعض إصدارات Basic، على سبيل المثال.انتقل إلى 10*x حيث x متغير.ومن المربك إلى حد ما، في فورتران، يشير مصطلح "goto المحسوب" إلى بناء يعادل بيان التبديل في لغة C.لا يسمح المعيار C بنقاط gotos المحسوبة في اللغة، ولكنه يسمح فقط بgotos للتسميات المعلنة بشكل ثابت/تركيبي.ومع ذلك، فإن GNU C لديه امتداد للحصول على عنوان الملصق (المشغل الأحادي، البادئة &&) ويسمح أيضًا بالانتقال إلى متغير من النوع void*.يرى https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html للمزيد حول هذا الموضوع الفرعي الغامض.لا يتعلق باقي هذا المنشور بميزة GNU C الغامضة.

المعيار ج (أي.غير محسوبة) لا تكون gotos عادةً هي السبب وراء عدم إمكانية العثور على التعليمات البرمجية التي لا يمكن الوصول إليها في وقت الترجمة.السبب المعتاد هو رمز المنطق مثل ما يلي.منح

int computation1() {
  return 1;
}

int computation2() {
  return computation1();
}

من الصعب أيضًا على المترجم العثور على تعليمات برمجية لا يمكن الوصول إليها في أي من التركيبات الثلاثة التالية:

void tough1() {
  if (computation1() != computation2())
    printf("Unreachable\n");
}

void tough2() {
  if (computation1() == computation2())
    goto out;
  printf("Unreachable\n");
out:;
}

struct Out{};

void tough3() {
  try {
    if (computation1() == computation2())
      throw Out();
    printf("Unreachable\n");
  }
  catch (Out&) {
  }
}

(أعذروني على أسلوب الترميز المتعلق بالأقواس، لكنني حاولت إبقاء الأمثلة مضغوطة قدر الإمكان.)

يفشل Visual C++ /W4 (حتى مع /Ox) في العثور على تعليمات برمجية لا يمكن الوصول إليها في أي منها، وكما تعلم على الأرجح، فإن مشكلة العثور على تعليمات برمجية لا يمكن الوصول إليها غير قابلة للحسم بشكل عام.(إذا كنت لا تصدقني في ذلك: https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf)

كمسألة ذات صلة، يمكن استخدام C goto لمحاكاة الاستثناءات فقط داخل نص الوظيفة.توفر مكتبة C القياسية زوجًا من الوظائف setjmp() وlongjmp() لمحاكاة المخارج/الاستثناءات غير المحلية، لكن تلك لها بعض العيوب الخطيرة مقارنة بما تقدمه اللغات الأخرى.مقالة ويكيبيديا http://en.wikipedia.org/wiki/Setjmp.h يشرح بشكل جيد هذه المسألة الأخيرة.يعمل زوج الوظائف هذا أيضًا على نظام التشغيل Windows (http://msdn.microsoft.com/en-us/library/yz2ez4as.aspx)، ولكن نادرًا ما يستخدمها أي شخص هناك لأن SEH/VEH متفوق.حتى في نظام Unix، أعتقد أن setjmp وlongjmp نادرًا ما يتم استخدامهما.

2) أعتقد أن الاستخدام الثاني الأكثر شيوعًا لـ goto في لغة C هو تنفيذ فاصل متعدد المستويات أو متابعة متعددة المستويات، وهي أيضًا حالة استخدام غير مثيرة للجدل إلى حد ما.تذكر أن Java لا تسمح بتسمية goto، ولكنها تسمح بكسر التسمية أو الاستمرار في التسمية.وفق http://www.Oracle.com/technetwork/Java/simple-142616.html, ، هذه هي في الواقع حالة الاستخدام الأكثر شيوعًا لـ gotos في لغة C (يقول 90٪)، ولكن في تجربتي الشخصية، يميل رمز النظام إلى استخدام gotos لمعالجة الأخطاء في كثير من الأحيان.ربما في التعليمات البرمجية العلمية أو حيث يوفر نظام التشغيل معالجة الاستثناءات (Windows)، فإن المخارج متعددة المستويات هي حالة الاستخدام السائدة.إنهم لا يقدمون حقًا أي تفاصيل فيما يتعلق بسياق المسح الخاص بهم.

تم التعديل للإضافة:اتضح أن نمطي الاستخدام هذين موجودان في كتاب C لكيرنيغان وريتشي، حوالي الصفحة 60 (حسب الطبعة).شيء آخر جدير بالملاحظة هو أن كلتا حالتي الاستخدام تتضمنان فقط gotos للأمام.واتضح أن إصدار MISRA C 2012 (على عكس إصدار 2004) يسمح الآن بـ gotos، طالما أنها فقط للأمام.

في لغة Perl، استخدم تسمية "goto" من حلقة - باستخدام عبارة "last"، والتي تشبه العبارة Break.

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

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

مشكلة "goto" والحجة الأكثر أهمية لحركة "goto-less programming" هي أنه إذا كنت تستخدمها بشكل متكرر، فإن التعليمات البرمجية الخاصة بك، على الرغم من أنها قد تتصرف بشكل صحيح، تصبح غير قابلة للقراءة، وغير قابلة للصيانة، وغير قابلة للمراجعة وما إلى ذلك.في 99.99% من الحالات، تؤدي كلمة "goto" إلى رمز السباغيتي.أنا شخصياً لا أستطيع التفكير في أي سبب وجيه وراء استخدامي لكلمة "goto".

Edsger Dijkstra، عالم الكمبيوتر الذي كان له مساهمات كبيرة في هذا المجال، كان مشهورًا أيضًا بانتقاده لاستخدام GoTo.هناك مقال قصير عن حجته ويكيبيديا.

أستخدم goto في الحالة التالية:عند الحاجة للعودة من الوظائف في أماكن مختلفة، وقبل العودة يجب إجراء بعض عمليات عدم التهيئة:

نسخة غير الذهاب:

int doSomething (struct my_complicated_stuff *ctx)    
{
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
        db_disconnect(conn);
        return -1;      
        }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        free(temp_data);    
        db_disconnect(conn);    
        return -2;
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        pthread_mutex_unlock(ctx->mutex);
        free(temp_data);
        db_disconnect(conn);        
        return -3;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -4;  
    }

    if (ctx->something_else->additional_check) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -5;  
    }


    pthread_mutex_unlock(ctx->mutex);
    free(temp_data);    
    db_disconnect(conn);    
    return 0;     
}

اذهب إلى الإصدار:

int doSomething_goto (struct my_complicated_stuff *ctx)
{
    int ret=0;
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
            ret=-1;
           goto exit_db;   
          }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        ret=-2;
        goto exit_freetmp;      
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        ret=-3;
        goto exit;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
        ret=-4;
        goto exit_freekey; 
    }

    if (ctx->something_else->additional_check) {
        ret=-5;
        goto exit_freekey;  
    }

exit_freekey:
    rsa_free(key);
exit:    
    pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
    free(temp_data);        
exit_db:
    db_disconnect(conn);    
    return ret;     
}

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

يقول البعض أنه لا يوجد سبب للانتقال إلى لغة C++.يقول البعض أنه في 99% من الحالات توجد بدائل أفضل. وهذا ليس تفكيرًا، بل مجرد انطباعات غير عقلانية. فيما يلي مثال قوي حيث يؤدي goto إلى رمز جميل، مثل حلقة do-while المحسنة:

int i;

PROMPT_INSERT_NUMBER:
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    goto PROMPT_INSERT_NUMBER;          
  }

std::cout << "your number is " << i;

قارنه برمز goto-free:

int i;

bool loop;
do {
  loop = false;
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    loop = true;          
  }
} while(loop);

std::cout << "your number is " << i;

أرى هذه الاختلافات:

  • متداخلة {} هناك حاجة إلى كتلة (على الرغم من do {...} while تبدو مألوفة أكثر)
  • إضافي loop المتغير مطلوب ويستخدم في أربعة مواضع
  • يستغرق الأمر وقتًا أطول لقراءة العمل وفهمه باستخدام ملف loop
  • ال loop لا يحتفظ بأي بيانات، بل يتحكم فقط في تدفق التنفيذ، وهو أمر أقل قابلية للفهم من التسمية البسيطة

هناك مثال آخر

void sort(int* array, int length) {
SORT:
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    goto SORT; // it is very easy to understand this code, right?
  }
}

الآن دعونا نتخلص من غوتو "الشر":

void sort(int* array, int length) {
  bool seemslegit;
  do {
    seemslegit = true;
    for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
      swap(data[i], data[i+1]);
      seemslegit = false;
    }
  } while(!seemslegit);
}

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

void sort(int* array, int length) {
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    i = -1; // it works, but WTF on the first glance
  }
}

النقطة المهمة هي أنه يمكن إساءة استخدام goto بسهولة، ولكن لا يقع اللوم على goto نفسه.لاحظ أن التسمية لها نطاق وظيفي في C++، لذلك لا تلوث النطاق العالمي كما هو الحال في التجميع النقي، حيث حلقات متداخلة لها مكانها وهي شائعة جدًا - كما هو الحال في الكود التالي لـ 8051، حيث يتم توصيل شاشة 7segment بـ P1.يقوم البرنامج بتكرار مقطع البرق حول:

; P1 states loops
; 11111110 <-
; 11111101  |
; 11111011  |
; 11110111  |
; 11101111  |
; 11011111  |
; |_________|

init_roll_state:
    MOV P1,#11111110b
    ACALL delay
next_roll_state:
    MOV A,P1
    RL A
    MOV P1,A
    ACALL delay
    JNB P1.5, init_roll_state
    SJMP next_roll_state

هناك ميزة أخرى:يمكن أن يكون goto بمثابة حلقات وشروط وتدفقات أخرى مسماة:

if(valid) {
  do { // while(loop)

// more than one page of code here
// so it is better to comment the meaning
// of the corresponding curly bracket

  } while(loop);
} // if(valid)

أو يمكنك استخدام goto المكافئ مع المسافة البادئة، لذلك لا تحتاج إلى تعليق إذا اخترت اسم التصنيف بحكمة:

if(!valid) goto NOTVALID;
  LOOPBACK:

// more than one page of code here

  if(loop) goto LOOPBACK;
NOTVALID:;
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top