Inne sposoby radzenia sobie z" inicjalizacją pętli " w C#

Na początek powiem, że Zgadzam się, że instrukcje goto są w dużej mierze nieistotne przez konstrukcje wyższego poziomu we współczesnych językach programowania i nie powinny być używane, gdy odpowiedni substytut jest dostępny.

[2]}czytałem ostatnio oryginalną wersję kodu Steve ' a McConnella i zapomniałem o jego sugestii dotyczącej wspólnego problemu z kodowaniem. Czytałem go lata temu, kiedy zaczynałem i nie sądzę, że zdałem sobie sprawę, jak przydatny przepis byłoby. Problem z kodowaniem jest następujący: podczas wykonywania pętli często trzeba wykonać część pętli, aby zainicjować stan, a następnie wykonać pętlę z inną logiką i zakończyć każdą pętlę z tą samą logiką inicjalizacji. Konkretnym przykładem jest implementacja String.Metoda Join (delimiter, array). Myślę, że pierwsze podejście każdego do problemu jest takie. Załóżmy, że metoda append jest zdefiniowana w celu dodania argumentu do zwracanej wartości.
bool isFirst = true;
foreach (var element in array)
{
  if (!isFirst)
  {
     append(delimiter);
  }
  else
  {
    isFirst = false;
  }

  append(element);
}

Uwaga: niewielki optymalizacja to usunięcie else i umieszczenie go na końcu pętli. Zadanie zwykle jest pojedynczą instrukcją i jest równoważne else i zmniejsza liczbę podstawowych bloków o 1 i zwiększa podstawowy rozmiar bloku głównej części. W rezultacie w każdej pętli wykonujemy warunek, aby określić, czy należy dodać ogranicznik, czy nie.

Widziałem i wykorzystałem inne podejścia do radzenia sobie z tym powszechnym problemem pętli. Możesz najpierw wykonać początkowy kod elementu poza pętlą, następnie wykonaj pętlę od drugiego elementu do końca. Możesz również zmienić logikę, aby zawsze dodawać element, a następnie ogranicznik, a po zakończeniu pętli możesz po prostu usunąć ostatni dodany ogranicznik.

To ostatnie rozwiązanie wydaje się być tym, które preferuję tylko dlatego, że nie powiela żadnego kodu. Jeśli logika sekwencji inicjalizacji kiedykolwiek się zmieni, nie musisz pamiętać, aby naprawić ją w dwóch miejscach. Wymaga jednak dodatkowej "pracy", aby zrób coś, a następnie cofnij to, powodując co najmniej dodatkowe cykle procesora i w wielu przypadkach, takich jak nasz ciąg znaków.Dołącz przykład wymaga dodatkowej pamięci.

Byłem wtedy podekscytowany, aby przeczytać ten konstrukt

var enumerator = array.GetEnumerator();
if (enumerator.MoveNext())
{
  goto start;
  do {
    append(delimiter);

  start:
    append(enumerator.Current);
  } while (enumerator.MoveNext());
}

Korzyść polega na tym, że nie dostajesz duplikowanego kodu i nie dostajesz dodatkowej pracy. Rozpoczynasz pętlę w połowie drogi do wykonania pierwszej pętli i to jest twoja inicjalizacja. Jesteś ograniczony do symulowania innych pętli za pomocą do while construct, ale tłumaczenie jest łatwe i czytanie nie jest trudne.

Więc teraz pytanie. Z radością poszedłem spróbować dodać to do jakiegoś kodu, nad którym pracowałem i okazało się, że nie działa. Działa świetnie w C, C++, Basic, ale okazuje się, że w C# nie można przeskoczyć do etykiety wewnątrz innego zakresu leksykalnego, który nie jest zakresem nadrzędnym. Byłem bardzo rozczarowany. Tak więc zastanawiałem się, jaki jest najlepszy sposób, aby poradzić sobie z tym bardzo częstym problemem kodowania (widzę go głównie w generowaniu ciągów) w C#?

Być może być bardziej specyficzne dla wymagań:

  • nie powielaj kodu
  • nie rób niepotrzebnej pracy
  • nie być więcej niż 2 lub 3 razy wolniejszym od innych kodów
  • być czytelne

Myślę, że czytelność jest jedyną rzeczą, która może prawdopodobnie cierpieć z przepisu, który podałem. Jednak to nie działa w C# więc co dalej?

* Edit * Zmieniłem kryteria wydajności ze względu na Część dyskusji. Osiągi na ogół nie są czynnik ograniczający tutaj, więc celem bardziej poprawnym powinno być, aby nie być nierozsądnym, nie być najszybszym w historii.

Powodem, dla którego nie lubię alternatywnych implementacji, które sugeruję, jest to, że albo powielają kod, który pozostawia miejsce na zmianę jednej części, a nie drugiej, albo dla tej, którą ogólnie wybieram, wymaga "cofnięcia" operacji, która wymaga dodatkowej myśli i czasu, aby cofnąć to, co właśnie zrobiłeś. W szczególności z manipulacją strunami zwykle pozostawia to otwarte dla wyłącza się przez jeden błąd lub nie uwzględnia pustej tablicy i próbuje cofnąć coś, co się nie stało.

Author: Mat, 2010-08-29

9 answers

Dla konkretnego przykładu istnieje standardowe rozwiązanie: string.Join. Obsługuje to poprawne dodawanie ogranicznika, dzięki czemu nie musisz samodzielnie pisać pętli.

Jeśli naprawdę chcesz to napisać samemu, możesz użyć następującego podejścia:

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

To powinno być w miarę skuteczne i myślę, że jest to rozsądne do czytania. Stały ciąg znaków "," jest internowany, więc nie spowoduje to utworzenia nowego ciągu przy każdej iteracji. Oczywiście, jeśli wydajność ma kluczowe znaczenie dla swoją aplikację powinieneś porównać, a nie zgadywać.

 12
Author: Mark Byers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-29 20:07:40

Osobiście podoba mi się opcja Marka Byera, ale zawsze możesz napisać własną metodę generyczną:

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);
        }
    }
}
To stosunkowo proste... podanie specjalnej ostatniej akcji jest nieco trudniejsze:
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);
    }
}

EDIT: ponieważ twój komentarz dotyczył wydajności tego, powtórzę mój komentarz w tej odpowiedzi: chociaż ten ogólny problem jest dość powszechny, to Nie jest powszechne, aby to było takie wąskie gardło wydajności, że warto mikro-optymalizacji w pobliżu. W rzeczy samej, nie pamiętam, żebym kiedykolwiek spotkał się z sytuacją, w której maszyny zapętlające stały się wąskim gardłem. Jestem pewien, że to się zdarza, ale to nie jest "powszechne". Jeśli kiedykolwiek natknę się na ten kod, to w specjalnym przypadku zastosuję ten konkretny kod, a najlepsze rozwiązanie będzie zależało od dokładnie tego, co kod musi zrobić.

Ogólnie jednak cenię czytelność i możliwość wielokrotnego użytku znacznie bardziej niż mikro-optymalizację.

 18
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-29 20:30:47

Jesteś już gotów zrezygnować z foreach. Więc to powinno być odpowiednie:

        using (var enumerator = array.GetEnumerator()) {
            if (enumerator.MoveNext()) {
                for (;;) {
                    append(enumerator.Current);
                    if (!enumerator.MoveNext()) break;
                    append(delimiter);
                }
            }
        }
 7
Author: Hans Passant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-30 10:13:10

Na pewno można stworzyć goto rozwiązanie w C# (uwaga: nie dodałem 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();
}

Dla Twojego konkretnego przykładu, wygląda to dla mnie dość prosto (i jest to jedno z rozwiązań, które opisałeś):

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();
}

Jeśli chcesz uzyskać funkcjonalność, możesz spróbować użyć takiego podejścia do składania:

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

Chociaż czyta się bardzo ładnie, nie używa StringBuilder, więc może warto trochę nadużywać Aggregate, aby go użyć:

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();
}

Lub możesz użyć tego (zapożyczenie pomysłu z innych odpowiedzi tutaj):

string Join(string[] array, string delimiter) {
  return array.
    Skip(1).
    Aggregate(new StringBuilder(array.FirstOrDefault()),
      (acc, s) => acc.Append(delimiter).Append(s)).
    ToString();
}
 6
Author: Jordão,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-30 19:36:39

Czasami używam LINQ .First() i .Skip(1) do tego... Może to dać stosunkowo czyste (i bardzo czytelne) rozwiązanie.

Używając Twojego przykładu,

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

[zakłada się, że w tablicy znajduje się co najmniej jeden element, co jest łatwym testem do dodania, jeśli trzeba tego uniknąć.]

Użycie F # byłoby kolejną propozycją : -)

 4
Author: Ian Mercer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-29 20:31:20

Istnieją sposoby na obejście podwojonego kodu, ale w większości przypadków zduplikowany kod jest znacznie mniej brzydki/niebezpieczny niż możliwe rozwiązania. Rozwiązanie" goto", które cytujesz, nie wydaje mi się ulepszeniem - nie wydaje mi się, że naprawdę zyskujesz coś znaczącego (zwartość, czytelność lub wydajność), używając go, podczas gdy zwiększasz ryzyko, że programista dostanie coś złego w pewnym momencie życia kodu.

Ogólnie mam tendencję do podejście:

  • specjalny przypadek pierwszego (lub ostatniego) działania
  • pętla dla pozostałych akcji.

Eliminuje to nieefektywność wprowadzoną przez sprawdzanie, czy pętla znajduje się w pierwszej iteracji za każdym razem i jest naprawdę łatwa do zrozumienia. W przypadku nietrywialnych przypadków użycie metody delegata lub helpera do zastosowania akcji może zminimalizować powielanie kodu.

Lub inne podejście, które używam czasami, gdy wydajność nie jest ważna:

  • pętla i test jeśli łańcuch znaków jest pusty, aby określić, czy wymagany jest ogranicznik.

Można to zapisać tak, aby było bardziej zwarte i czytelne niż podejście goto i nie wymaga żadnych dodatkowych zmiennych/pamięci / testów do wykrycia iteraitonu "specjalnego przypadku".

Ale myślę, że podejście Marka Byersa jest dobrym, czystym rozwiązaniem dla Twojego konkretnego przykładu.

 2
Author: Jason Williams,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-29 20:34:45

Preferuję first metodę zmienną. Prawdopodobnie nie jest to najczystszy, ale najbardziej skuteczny sposób. Alternatywnie możesz użyć Length rzeczy, do której dodajesz i porównać ją do zera. Działa dobrze z StringBuilder.

 0
Author: Ilia G,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-29 20:06:12

Dlaczego nie przenieść pierwszego elementu poza pętlę ?

StringBuilder sb = new StrindBuilder()
sb.append(array.first)
foreach (var elem in array.skip(1)) {
  sb.append(",")
  sb.append(elem)
}
 0
Author: Bart,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-29 20:13:23

Jeśli chcesz przejść trasę funkcjonalną, możesz zdefiniować String.Join like LINQ construct, który jest wielokrotnego użytku w różnych typach.

Osobiście prawie zawsze starałbym się o przejrzystość kodu przy zapisywaniu kilku egzekucji kodu opcode.

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
Author: Sam Saffron,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-30 02:01:57