Есть ли причина использовать goto в современном.NET-коде?

Я только что нашел этот код в отражателе в базовых библиотеках.NET...

    if (this._PasswordStrengthRegularExpression != null)
    {
        this._PasswordStrengthRegularExpression = this._PasswordStrengthRegularExpression.Trim();
        if (this._PasswordStrengthRegularExpression.Length == 0)
        {
            goto Label_016C;
        }
        try
        {
            new Regex(this._PasswordStrengthRegularExpression);
            goto Label_016C;
        }
        catch (ArgumentException exception)
        {
            throw new ProviderException(exception.Message, exception);
        }
    }
    this._PasswordStrengthRegularExpression = string.Empty;
Label_016C:
    ... //Other stuff

Я слышал все о том, что "ты не должен использовать goto из-за страха изгнания в ад на вечность". Я всегда относился к кодировщикам MS довольно высоко, и, хотя я, возможно, не соглашался со всеми их решениями, я всегда уважал их рассуждения.

Итак, есть ли веская причина для такого кода, который я пропускаю? Был ли этот фрагмент кода собран неумелым разработчиком? или.NET рефлектор возвращает неточный код?

Я надеюсь, что есть веская причина, и я просто слепо скучаю по ней.

Спасибо за вклад каждого

20 ответов

Решение

Отражатель не идеален. Фактический код этого метода доступен из справочного источника. Он находится в ndp\fx\src\xsp\system\web\security\admembershipprovider.cs:

        if( passwordStrengthRegularExpression != null )
        { 
            passwordStrengthRegularExpression = passwordStrengthRegularExpression.Trim();
            if( passwordStrengthRegularExpression.Length != 0 ) 
            { 
                try
                { 
                    Regex regex = new Regex( passwordStrengthRegularExpression );
                }
                catch( ArgumentException e )
                { 
                    throw new ProviderException( e.Message, e );
                } 
            } 
        }
        else 
        {
            passwordStrengthRegularExpression = string.Empty;
        }

Обратите внимание, что он не смог обнаружить последнее предложение else и компенсировал это с помощью goto. Он почти наверняка отключен блоками try/catch внутри операторов if().

Очевидно, вы захотите отдать предпочтение фактическому исходному коду вместо декомпилированной версии. Сами по себе комментарии весьма полезны, и вы можете рассчитывать на точность источника. Ну, по большей части, есть небольшое повреждение от ошибочного инструмента постобработки, который убрал имена программистов Microsoft. Идентификаторы иногда заменяются тире, а код повторяется дважды. Вы можете скачать исходный код здесь.

Я видел, как goto использовался, чтобы вырваться из вложенных циклов:

Как вырваться из двух вложенных циклов for в Objective-C?

Я не вижу в этом ничего плохого.

Его, вероятно, нет в исходном коде, просто выглядит дизассемблированный код.

Существует несколько допустимых вариантов использования goto в.NET (особенно на C#):

Симуляция провала симуляции оператора переключения.

Те, которые происходят из C++ фона, используются для написания операторов switch, которые автоматически переходят от случая к случаю, если явно не завершаются с помощью break. Для C# только тривиальные (пустые) падежи.

Например, в C++

int i = 1;
switch (i)
{
case 1:
  printf ("Case 1\r\n");
case 2:
  printf ("Case 2\r\n");
default:
  printf ("Default Case\r\n");
  break;
}

В этом коде C++ вывод:

Случай 1
Дело 2
Случай по умолчанию

Вот аналогичный код C#:

int i = 1;
switch (i)
{
case 1:
  Console.Writeline ("Case 1");
case 2:
  Console.Writeline ("Case 2");
default:
  Console.Writeline ("Default Case");
  break;
}

Как написано, это не скомпилируется. Есть несколько ошибок компиляции, которые выглядят так:

Элемент управления не может перейти от одной метки кейса ('case 1:') к другой

Добавление операторов goto заставляет его работать:

int i = 1;
switch (i)
{
case 1:
    Console.WriteLine ("Case 1");
    goto case 2;
case 2:
    Console.WriteLine("Case 2");
    goto default;
default:
    Console.WriteLine("Default Case");
    break;
}

... другое полезное использование Goto в C#...

Бесконечные циклы и развернутая рекурсия

Я не буду вдаваться в детали, так как это менее полезно, но иногда мы пишем бесконечные циклы, используя while(true) конструкции, которые явно заканчиваются break или перезапущен с continue заявление. Это может произойти, когда мы пытаемся симулировать вызовы рекурсивных методов, но не имеем никакого контроля над потенциальной областью действия рекурсии.

Вы можете, очевидно, рефакторинг, что в while(true) зацикливание или рефакторинг его в отдельный метод, но также использование метки и оператора goto работает.

Такое использование goto является более спорным, но все же стоит помнить о нем как об одном из вариантов в очень редких случаях.

Я не в восторге от gotos, но говорить, что они никогда не действительны, глупо.

Я использовал один раз, чтобы исправить дефект в особенно грязном куске кода. Реорганизовать код и протестировать его было бы непрактично, учитывая временные ограничения.

Кроме того, разве мы не видели условные конструкции, которые были настолько плохо закодированы, что они заставляют gotos казаться доброкачественными?

Вы можете использовать GOTO для выполнения рекурсии с лучшей производительностью. Поддерживать его намного сложнее, но если вам нужны эти дополнительные циклы, вы, возможно, захотите оплатить расходы по обслуживанию.

Вот простой пример с результатами:

class Program
{
    // Calculate (20!) 1 million times using both methods.
    static void Main(string[] args)
    {
        Stopwatch sw = Stopwatch.StartNew();
        Int64 result = 0;
        for (int i = 0; i < 1000000; i++)
            result += FactR(20);
        Console.WriteLine("Recursive Time: " + sw.ElapsedMilliseconds);

        sw = Stopwatch.StartNew();
        result = 0;
        for (int i = 0; i < 1000000; i++)
            result += FactG(20);
        Console.WriteLine("Goto Time: " + sw.ElapsedMilliseconds);
        Console.ReadLine();
    }

    // Recursive Factorial
    static Int64 FactR(Int64 i)
    {
        if (i <= 1)
            return 1;
        return i * FactR(i - 1);
    }

    // Recursive Factorial (using GOTO)
    static Int64 FactG(Int64 i)
    {
        Int64 result = 1;

    Loop:
        if (i <= 1)
            return result;

        result *= i;
        i--;
        goto Loop;
    }

Вот результаты, которые я получаю на своей машине:

 Recursive Time: 820
 Goto Time: 259

Ты не должен смотреть на код рефлектора.

Хотя, если вы когда-нибудь посмотрите на разобранный IL, вы увидите повсюду gotos. По сути, все циклы и другие используемые нами управляющие конструкции в любом случае преобразуются в gotos, просто превращая их в конструкции в нашем коде, они становятся более читабельными и их легче поддерживать.

Между прочим, я не думаю, что размещенный вами код будет хорошим местом для использования goto, и я изо всех сил пытаюсь придумать его.

Я не видел подходящего случая для Goto во многих, многих строках кода.NET, написанных и рассмотренных.

В языках, которые не поддерживают обработку структурированных исключений с помощью блока finally (PASCAL - дедушка языков структурированного программирования, а также классического C), тактическое использование GOTO может значительно облегчить понимание кода при использовании для выполнения. очистка, когда выполнение прекращается во вложенных циклах (в отличие от правильной установки условий завершения нескольких циклов). Даже в те дни я не использовал goto лично по этой причине (вероятно, из-за страха "вечного изгнания в ад").

Нет, нет веских причин для использования goto, Я в последний раз кодировал goto заявление в 1981 году, и с тех пор я не пропустил эту конкретную конструкцию.

Goto часто полезны при написании парсеров и лексеров.

Посмотрите на диаграмму состояний. Если вы считаете, что лучше всего использовать структуру кода, которая наиболее точно и четко выражает ваше намерение, то каждый из этих переходов состояний должен быть закодирован как переход.

Это имеет тенденцию ломаться в реальном мире, все же. Первая проблема заключается в том, что нам часто приходится останавливать машину, выходить из другого кода и позже возобновлять работу машины, что означает, что каждый из этих переходов, как правило, является изменением переменной состояния, используемой для определения правильного состояния в коммутаторе. / заявление о ситуации. Это действительно просто способ скрыть и задержать переход - запись в переменную состояния на самом деле мало чем отличается от записи в регистр счетчика программ. Это просто способ реализовать "Перейти туда - но не сейчас, а позже".

Однако есть случаи, когда goto хорошо работает, чтобы выразить то, что происходит в какой-то модели состояния, - я предполагаю, что примером будет одна из тех диагностических блок-схем, которые иногда используют врачи. Если вы реализуете одну из них как программу без использования gotos для переходов, то на самом деле вы просто усложняете себе жизнь, шифруя намерения вашего кода.

Просто наиболее распространенные случаи вряд ли будут написаны от руки. Я написал генераторы кода, которые генерируют операторы goto для переходов в различных типах модели состояния (обработка решений, регулярный анализ грамматики и т. Д.), Но я не помню, когда в последний раз я использовал goto в рукописном коде.

Что касается этого пункта:

Итак, есть ли веская причина для такого кода, который я пропускаю? Был ли этот фрагмент кода собран дерьмовым разработчиком? или.NET рефлектор возвращает неточный код?

Я не согласен с предпосылкой, что это единственные три возможности.

Может быть, это правда, как и многие другие предположили, что это просто не точное отражение реального исходного кода в библиотеке. Несмотря на это, мы все были виновны (ну, в любом случае, я) в написании кода "грязным путем" с целью:

  1. Быстрое внедрение функции
  2. Исправление ошибки быстро
  3. Выжимая небольшое увеличение производительности (иногда с оправданием, иногда не так сильно)
  4. Другая причина, которая имела смысл в то время

Это не делает кого-то "дерьмовым разработчиком". Большинство руководств, таких как "ты не будешь использовать goto", в основном созданы для защиты разработчиков от самих себя; их не следует рассматривать как ключ к различению хороших и плохих разработчиков.

В качестве аналогии рассмотрим простое правило, которое многие из нас преподают на уроках английского языка: никогда не заканчивайте предложение предлогом. Это не настоящее правило; Это руководство, которое не дает людям говорить что-то вроде: "Где машина?" Важно понять этот факт; как только вы начнете относиться к нему как к действительному правилу, а не как к руководству, вы обнаружите, что "исправляете" людей к совершенно хорошим предложениям типа "Чего вы боитесь?".

Имея это в виду, я бы с осторожностью относился к любому разработчику, который называл другого разработчика "дерьмовым", потому что он использовал goto,

Я конечно не пытаюсь защищать goto По сути - просто утверждая, что его использование никоим образом не указывает на некомпетентность.

В дополнение ко всем этим хорошим действительным вещам, когда вы смотрите на дизассемблированный код, имейте в виду, что разработчики МОГУТ использовать обфускатор на этих сборках. Один из методов запутывания - добавление случайных переходов к IL

Как показали другие, код, который вы видите в отражателе, обязательно является кодом, написанным в Framework. Компилятор и оптимизаторы могут изменять код так, чтобы он функционировал аналогичным образом, если только он не изменяет фактическую работу, выполняемую кодом. Следует также отметить, что компилятор реализует все ветви и циклы как goto (ветви в IL или переходы в сборке.) Когда запускается режим выпуска и компилятор пытается оптимизировать код до простейшей формы, которая функционально совпадает с вашей источник.

У меня есть пример по различным методам зацикливания, которые все компилируются на 100% один и тот же IL при компиляции для релиза. Смотрите другой ответ

(Я не могу найти его прямо сейчас, но Эрик Липперт опубликовал заметку о том, как компилятор C# обрабатывает код. Один из замечаний, которые он сделал, это то, как все циклы заменяются на goto.)

При этом у меня нет проблем с goto. Если есть лучшая петлевая структура, используйте ее. Но иногда вам нужно что-то немного больше, чем то, из чего вы можете выжать, foreach, while, do/while, но вы не хотели, чтобы дополнительный беспорядок и боль возникали от вызовов методов (зачем тратить 5 с лишним строк, чтобы преобразовать вложенный элемент в рекурсивные методы.)

Существует один допустимый случай - когда вы пытаетесь смоделировать рекурсивный вызов процедуры и выполнить возврат в нерекурсивном коде или делаете что-то подобное (такое требование также встречается в интерпретаторе Prolog). Но в целом, если вы не занимаетесь чем-то, требующим микрооптимизации, например, шахматной программой или переводчиком языка, гораздо лучше просто использовать обычный стек процедур и использовать вызовы функций / процедур.

Это может быть не лучшим примером, но это показывает случай, когда goto может быть очень удобным.

private IDynamic ToExponential(Engine engine, Args args)
{
    var x = engine.Context.ThisBinding.ToNumberPrimitive().Value;

    if (double.IsNaN(x))
    {
        return new StringPrimitive("NaN");
    }

    var s = "";

    if (x < 0)
    {
        s = "-";
        x = -x;
    }

    if (double.IsPositiveInfinity(x))
    {
        return new StringPrimitive(s + "Infinity");
    }

    var f = args[0].ToNumberPrimitive().Value;
    if (f < 0D || f > 20D)
    {
        throw new Exception("RangeError");
    }

    var m = "";
    var c = "";
    var d = "";
    var e = 0D;
    var n = 0D;

    if (x == 0D)
    {
        f = 0D;
        m = m.PadLeft((int)(f + 1D), '0');
        e = 0;
    }
    else
    {
        if (!args[0].IsUndefined) // fractionDigits is supplied
        {
            var lower = (int)Math.Pow(10, f);
            var upper = (int)Math.Pow(10, f + 1D);
            var min = 0 - 0.0001;
            var max = 0 + 0.0001; 

            for (int i = lower; i < upper; i++)
            {
                for (int j = (int)f;; --j)
                {
                    var result = i * Math.Pow(10, j - f) - x;
                    if (result > min && result < max)
                    {
                        n = i;
                        e = j;
                        goto Complete;
                    }
                    if (result <= 0)
                    {
                        break;
                    }
                }

                for (int j = (int)f + 1; ; j++)
                {
                    var result = i * Math.Pow(10, j - f) - x;
                    if (result > min && result < max)
                    {
                        n = i;
                        e = j;
                        goto Complete;
                    }
                    if (result >= 0)
                    {
                        break;
                    }
                }
            }
        }
        else
        {
            var min = x - 0.0001;
            var max = x + 0.0001; 

            // Scan for f where f >= 0
            for (int i = 0;; i++)
            {
                // 10 ^ f <= n < 10 ^ (f + 1)
                var lower = (int)Math.Pow(10, i);
                var upper = (int)Math.Pow(10, i + 1D);
                for (int j = lower; j < upper; j++)
                {
                    // n is not divisible by 10
                    if (j % 10 == 0)
                    {
                        continue;
                    }

                    // n must have f + 1 digits
                    var digits = 0;
                    var state = j;
                    while (state > 0)
                    {
                        state /= 10;
                        digits++;
                    }
                    if (digits != i + 1)
                    {
                        continue;
                    }

                    // Scan for e in both directions
                    for (int k = (int)i; ; --k)
                    {
                        var result = j * Math.Pow(10, k - i);
                        if (result > min && result < max)
                        {
                            f = i;
                            n = j;
                            e = k;
                            goto Complete;
                        }
                        if (result <= i)
                        {
                            break;
                        }
                    }
                    for (int k = (int)i + 1; ; k++)
                    {
                        var result = i * Math.Pow(10, k - i);
                        if (result > min && result < max)
                        {
                            f = i;
                            n = j;
                            e = k;
                            goto Complete;
                        }
                        if (result >= i)
                        {
                            break;
                        }
                    }
                }
            }
        }

    Complete:

        m = n.ToString("G");
    }

    if (f != 0D)
    {
        m = m[0] + "." + m.Substring(1);
    }

    if (e == 0D)
    {
        c = "+";
        d = "0";
    }
    else
    {
        if (e > 0D)
        {
            c = "+";
        }
        else
        {
            c = "-";
            e = -e;
        }
        d = e.ToString("G");
    }

    m = m + "e" + c + d;
    return new StringPrimitive(s + m);
}

goto отлично подходит для очистки, по крайней мере, в таких языках, как C, где оно несколько имитирует понятие исключений. Я уверен, что в.NET есть лучшие способы обработки подобных вещей, поэтому goto просто устарел и подвержен ошибкам.

Мне не нравится этот код.

Я бы предпочел хранить Regex в элементе и проверять его при настройке, избегая необходимости логики при его чтении.

Нет. (Было отмечено, что в данном случае это артефакт декомпилятора, заметьте.)

Единственным исключением является CASE GOTO, который позволяет вам совместно использовать код между случаями переключения. Однако на самом деле это не GOTO, просто у него одно и то же имя.

Проблема с оператором GOTO не идеологическая, как многие предполагают, а в том, что в современных языках высокого уровня всегда есть лучшие, более четкие, более удобные в сопровождении альтернативы, а использование GOTO всегда добавляет сложности, что не оправдывается экономией минуты или около того. правильно его закодировать. Вместо этого стоит потратить время на использование одного из них, даже если это означает, что вы должны соответствовать критериям для выхода из ряда вложенных циклов или условий.

Блочные конструкции, такие как WHILE, DO-UNTIL и FOR, намного лучше выполняют циклы и имеют стандартную форму, которая по своей сути очевидна для тех, кто поддерживает код, и вы не можете выпасть из блока или забыть завершить его в некоторых путях кода. По этой причине они по своей сути являются лучшим кодом. Именно поэтому они были изобретены и получили широкое распространение. Вы можете, конечно, добиться того же цикла с помощью GOTO, но вы также можете сделать ошибку где-то внутри него (особенно если вы GOTO вне цикла). Эти специальные операторы также оптимизированы для этой цели, поэтому они, вероятно, сохраняют обратный адрес более эффективным способом, чем GOTO находит свою цель.

Подобные условные блоки кода устраняют необходимость перехода через несколько строк, которые не применяются в данных обстоятельствах (да, в ранних языках условие могло быть применено только к одной строке кода), а условный блок проще. видеть как атомарную часть кода, а условие достаточно четко указано вверху блока. Если вы используете GOTO, вы можете перепрыгнуть через несколько мест, и поток кода должен быть тщательно и безошибочно проработан каждый раз, когда вы посещаете код, что занимает очень много времени и чревато ошибками и непониманием. Раньше я распечатывал программы и использовал маркер для разметки потока. Вам не нужно с современными конструкциями.

Еще лучше, современная передовая практика заключалась бы в том, чтобы инкапсулировать эту условную функциональность в функцию с понятным именем, поэтому вместо того, чтобы читать ее, чтобы увидеть, что она делает, или пропустить ее, если это не имеет значения, вы просто видите приятное маленькое имя функции, которое говорит вам, что она делает (которую, конечно, вы можете рассматривать отдельно, если вам нужно, и которая может использовать локальные переменные, если ей нужны рабочие переменные, которые определенно не могут мешать другому коду в другом месте).

Другая проблема — состояние стека. Я не знаю, что .net не проверяет, выходите ли вы из функции, цикла или другого блока, но традиционно GOTO не выполняет таких проверок, что означает, что любая информация о состоянии или локальная информация, такая как локальные переменные , или адреса возврата, хранящиеся в стеке, остаются в стеке, и когда вы снова вводите эту конструкцию, добавляются новые копии, пока, в конце концов, стек не переполнится. Это самая старая ошибка в книге; это даже вдохновило на создание известного технического сайта вопросов и ответов.

Я использовал GOTO широко в языках без какой-либо альтернативы 35 лет назад, и даже на ретро-компьютерах в последние годы (потому что у них до сих пор нет альтернативы), но в современных языках мне не приходилось этого делать примерно с 1990 года. Я даже не знал, что он существует в C#, пока месяц назад не погуглил его после аналогичного обсуждения в другом месте.

Вы также можете увидеть, как люди говорят, что это эквивалентно скачку машинного кода. Это совсем не так. Это очень сильно зависит от реализации языка, но он должен найти точный адрес памяти, к которому нужно перейти, и это может включать в себя поиск методом грубой силы в исходном коде (обычный в интерпретаторах, где вы, скорее всего, будете использовать GOTO), поиск в векторной таблице (скорее всего, с C# или скомпилированными языками) или что-то в этом роде. На самом деле это не имеет значения, но вряд ли это будет эквивалентно простой загрузке жестко закодированного адреса в счетчик программ ЦП, и даже если это произойдет, когда вы его запишете, это может быть не так, когда он будет работать в эмулируемой реализации в будущем. Конструкции выделенных циклов, вероятно, более оптимизированы и, следовательно, быстрее.

Я никогда не кодировал GO TO назад, когда писал Фортран.

Мне никогда не приходилось этим пользоваться. Я не могу понять, почему какой-либо современный язык потребовал бы такого от пользователя. Я бы сказал однозначно "нет".

Другие вопросы по тегам