Другие способы борьбы с «инициализацией цикла» в C # - PullRequest
22 голосов
/ 29 августа 2010

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

Я был повторно- недавно прочитал оригинальную редакцию Code Complete Стива Макконнелла и забыл о своем предложении по поводу распространенной проблемы кодирования.Я читал его много лет назад, когда только начинал, и не думаю, что понял, насколько полезным будет этот рецепт.Проблема кодирования заключается в следующем: при выполнении цикла вам часто нужно выполнить часть цикла, чтобы инициализировать состояние, а затем выполнить цикл с некоторой другой логикой и завершить каждый цикл той же логикой инициализации.Конкретным примером является реализация метода String.Join (разделитель, массив).

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

bool isFirst = true;
foreach (var element in array)
{
  if (!isFirst)
  {
     append(delimiter);
  }
  else
  {
    isFirst = false;
  }

  append(element);
}

Примечание. Небольшая оптимизация заключается в том, чтобы удалить 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, C ++, Basic, но оказывается, что в C # нельзя перейти к метке внутри другой лексической области, которая не является родительской областью.Я был очень разочарован.Так что мне было интересно, как лучше всего справиться с этой очень распространенной проблемой кодирования (я вижу это в основном при генерации строк) в C #?

Чтобы быть более конкретным с требованиями:

  • Не дублировать код
  • Не выполнять ненужную работу
  • Не быть более чем в 2 или 3 раза медленнее, чем другой код
  • Быть читаемым

Я думаю, что читаемость - единственное, что может пострадать от рецепта, который я изложил.Однако это не работает в C #, так что же будет лучше?

* Редактировать * Я изменил свои критерии производительности из-за некоторых обсуждений.Производительность, как правило, здесь не является ограничивающим фактором, поэтому более правильной целью должно быть не быть необоснованным и не быть самым быстрым за всю историю.

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

Ответы [ 9 ]

18 голосов
/ 30 августа 2010

Лично мне нравится опция Марка Байера, но вы всегда можете написать свой собственный универсальный метод для этого:

public static void IterateWithSpecialFirst<T>(this IEnumerable<T> source,
    Action<T> firstAction,
    Action<T> subsequentActions)
{
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        if (iterator.MoveNext())
        {
            firstAction(iterator.Current);
        }
        while (iterator.MoveNext())
        {
            subsequentActions(iterator.Current);
        }
    }
}

Это относительно просто ... дать небольшое действие last немногосложнее:

public static void IterateWithSpecialLast<T>(this IEnumerable<T> source,
    Action<T> allButLastAction,
    Action<T> lastAction)
{
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        if (!iterator.MoveNext())
        {
            return;
        }            
        T previous = iterator.Current;
        while (iterator.MoveNext())
        {
            allButLastAction(previous);
            previous = iterator.Current;
        }
        lastAction(previous);
    }
}

РЕДАКТИРОВАТЬ: Поскольку ваш комментарий был связан с производительностью этого, я повторю свой комментарий в этом ответе: хотя эта общая проблема является достаточно распространенной, это не Для него характерно такое узкое место в производительности, что его стоит микро-оптимизировать.Действительно, я не могу вспомнить, чтобы когда-либо сталкивался с ситуацией, когда петлевое оборудование становилось узким местом.Я уверен, что это происходит, но , что не является "общим".Если я когда-нибудь столкнусь с ним, то я специально рассмотрю этот конкретный код, и наилучшее решение будет зависеть от точно того, что должен делать код.

В целом, однако, яЗначение читаемости и возможности повторного использования намного больше, чем микрооптимизация.

11 голосов
/ 30 августа 2010

Для вашего конкретного примера есть стандартное решение: string.Join. Это позволяет правильно добавлять разделитель, чтобы вам не пришлось писать цикл самостоятельно.

Если вы действительно хотите написать это самостоятельно, вы можете использовать следующий подход:

string delimiter = "";
foreach (var element in array)
{
    append(delimiter);
    append(element);
    delimiter = ",";
}

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

7 голосов
/ 30 августа 2010

Вы уже готовы отказаться от foreach. Так что это должно быть подходящим:

        using (var enumerator = array.GetEnumerator()) {
            if (enumerator.MoveNext()) {
                for (;;) {
                    append(enumerator.Current);
                    if (!enumerator.MoveNext()) break;
                    append(delimiter);
                }
            }
        }
6 голосов
/ 30 августа 2010

Конечно, вы можете создать goto решение в C # (примечание: я не добавил null проверок):

string Join(string[] array, string delimiter) {
  var sb = new StringBuilder();
  var enumerator = array.GetEnumerator();
  if (enumerator.MoveNext()) {
    goto start;
    loop:
      sb.Append(delimiter);
      start: sb.Append(enumerator.Current);
      if (enumerator.MoveNext()) goto loop;
  }
  return sb.ToString();
}

Для вашего конкретного примера это выглядит довольно простым для меня (и это одно из описанных вами решений):

string Join(string[] array, string delimiter) {
  var sb = new StringBuilder();
  foreach (string element in array) {
    sb.Append(element);
    sb.Append(delimiter);
  }
  if (sb.Length >= delimiter.Length) sb.Length -= delimiter.Length;
  return sb.ToString();
}

Если вы хотите получить функциональность, вы можете попробовать использовать этот подход сворачивания:

string Join(string[] array, string delimiter) {
  return array.Aggregate((left, right) => left + delimiter + right);
}

Несмотря на то, что он выглядит очень хорошо, он не использует StringBuilder, поэтому вы можете немного злоупотребить Aggregate, чтобы использовать его:

string Join(string[] array, string delimiter) {
  var sb = new StringBuilder();
  array.Aggregate((left, right) => {
    sb.Append(left).Append(delimiter).Append(right);
    return "";
  });
  return sb.ToString();
}

Или вы можете использовать это (заимствуя идею из других ответов здесь):

string Join(string[] array, string delimiter) {
  return array.
    Skip(1).
    Aggregate(new StringBuilder(array.FirstOrDefault()),
      (acc, s) => acc.Append(delimiter).Append(s)).
    ToString();
}
4 голосов
/ 30 августа 2010

Иногда я использую LINQ .First() и .Skip(1), чтобы справиться с этим ... Это может дать относительно чистое (и очень удобочитаемое) решение.

Используя ваш пример,

append(array.First());
foreach(var x in array.Skip(1))
{
  append(delimiter);
  append (x);
}

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

Использование F # было бы другим предложением: -)

2 голосов
/ 30 августа 2010

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

В общем, я склоняюсь к подходу:

  • Особый случай для первого (или последнего) действия
  • цикл для других действий.

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

Или другой подход, который я иногда использую, когда эффективность не важна:

  • цикли проверьте, является ли строка пустой, чтобы определить, требуется ли разделитель.

Это можно записать как более компактный и читаемый, чем подход goto, и не требует каких-либо дополнительных переменных / хранилища./ tests для обнаружения "особого случая" iteraiton.

Но я думаю, что подход Марка Байерса является хорошим чистым решением для вашего конкретного примера.

0 голосов
/ 30 августа 2010

Если вы хотите пойти по функциональному маршруту, вы можете определить String.Join как конструкцию LINQ, которую можно многократно использовать в разных типах.

Лично я почти всегда хотел бы прояснить код, сохранив несколько исполнений кода операции.

EG:

namespace Play
{
    public static class LinqExtensions {
        public static U JoinElements<T, U>(this IEnumerable<T> list, Func<T, U> initializer, Func<U, T, U> joiner)
        {
            U joined = default(U);
            bool first = true;
            foreach (var item in list)
            {
                if (first)
                {
                    joined = initializer(item);
                    first = false;
                }
                else
                {
                    joined = joiner(joined, item);
                }
            }
            return joined;
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            List<int> nums = new List<int>() { 1, 2, 3 };
            var sum = nums.JoinElements(a => a, (a, b) => a + b);
            Console.WriteLine(sum); // outputs 6

            List<string> words = new List<string>() { "a", "b", "c" };
            var buffer = words.JoinElements(
                a => new StringBuilder(a), 
                (a, b) => a.Append(",").Append(b)
                );

            Console.WriteLine(buffer); // outputs "a,b,c"

            Console.ReadKey();
        }

    }
}
0 голосов
/ 30 августа 2010

Почему бы не переместить работу с первым элементом вне цикла?

StringBuilder sb = new StrindBuilder()
sb.append(array.first)
foreach (var elem in array.skip(1)) {
  sb.append(",")
  sb.append(elem)
}
0 голосов
/ 30 августа 2010

Я предпочитаю first метод переменных. Это, вероятно, не самый чистый, но самый эффективный способ. В качестве альтернативы вы можете использовать Length того, что вы добавляете, и сравнить его с нулем Хорошо работает с StringBuilder.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...