Начнем с того, что я соглашусь с тем, что утверждения goto в значительной степени не имеют отношения к конструкциям более высокого уровня в современных языках программирования и не должны использоваться, когда доступен подходящий заменитель.
Я недавно перечитал оригинальное издание Кодекса Стива Макконнелла и забыл о его предположении об общей проблеме кодирования. Я читал это много лет назад, когда я начинал сначала и не думал, что понял, насколько полезен рецепт. Проблема кодирования заключается в следующем: при выполнении цикла вам часто нужно выполнить часть цикла для инициализации состояния, а затем выполнить цикл с помощью некоторой другой логики и закончить каждый цикл с помощью той же логики инициализации. Конкретным примером является реализация метода String.Join(разделитель, массив).
Я думаю, что все сначала берут на себя эту проблему. Предположим, что метод append определен для добавления аргумента к возвращаемому значению.
bool isFirst = true;
foreach (var element in array)
{
if (!isFirst)
{
append(delimiter);
}
else
{
isFirst = false;
}
append(element);
}
Примечание. Небольшая оптимизация заключается в том, чтобы удалить else и поместить его в конец цикла. Назначение, как правило, является одной инструкцией и эквивалентно else и уменьшает количество базовых блоков на 1 и увеличивает основной размер блока основной части. Результатом является выполнение условия в каждом цикле, чтобы определить, следует ли добавлять разделитель или нет.
Я также видел и использовал другие способы решения этой проблемы с общим циклом. Вы можете выполнить начальный код элемента сначала за пределами цикла, а затем выполнить свой цикл со второго элемента до конца. Вы также можете изменить логику, чтобы всегда добавлять элемент, а затем разделитель, и как только цикл будет завершен, вы можете просто удалить добавленный последний разделитель.
Последнее решение стремится к тому, которое я предпочитаю только потому, что оно не дублирует код. Если логика последовательности инициализации когда-либо изменится, вам не нужно запоминать ее в двух местах. Тем не менее он требует дополнительной "работы", чтобы что-то сделать, а затем отменить, вызывая как минимум дополнительные циклы процессора, и во многих случаях, таких как наш пример String.Join, требует дополнительной памяти.
Тогда я был взволнован, чтобы прочитать эту конструкцию
var enumerator = array.GetEnumerator();
if (enumerator.MoveNext())
{
goto start;
do {
append(delimiter);
start:
append(enumerator.Current);
} while (enumerator.MoveNext());
}
Преимущество в том, что вы не получаете дублированного кода, и вы не получаете дополнительной работы. Вы начинаете свой цикл на полпути в выполнение вашего первого цикла, и это ваша инициализация. Вы ограничены имитацией других циклов с помощью do while, но перевод прост, и чтение это не сложно.
Итак, теперь вопрос. Я с удовольствием попытался добавить это к некоторому коду, над которым я работал, и нашел, что он не работает. Отлично работает на C, С++, Basic, но в С# вы не можете перейти на метку внутри другой лексической области, которая не является родительской областью. Я был очень разочарован. Поэтому мне стало интересно, как лучше всего справиться с этой очень распространенной проблемой кодирования (я вижу это в основном в генерации строк) на С#?
Чтобы быть более конкретным с требованиями:
- Не дублировать код
- Не делайте ненужной работы.
- Не более 2 или 3 раза медленнее, чем другой код.
- Быть читаемым
Я думаю, что удобочитаемость - единственное, что может быть связано с рецептом, который я изложил. Однако это не работает на С#, так что следующая лучшая вещь?
* Редактировать * Из-за некоторых обсуждений я изменил критерии эффективности. Производительность, как правило, не является ограничивающим фактором здесь, поэтому цель более правильно должна состоять в том, чтобы не быть необоснованным, а не быть самым быстрым из всех.
Причина, по которой мне не нравятся альтернативные реализации, которые я предлагаю, заключается в том, что они либо дублируют код, который оставляет место для изменения одной части, а не другой или для той, которую я обычно выбираю, это требует "отмены" операции, которая требует дополнительных мыслей и времени для отмените то, что вы только что сделали. В частности, при использовании строковых манипуляций это обычно оставляет вас открытыми для одной ошибки или не учитывает пустой массив и пытается отменить то, чего не произошло.