Frage

Zunächst werde ich sagen, dass ich damit einverstanden, dass GOTO-Anweisungen werden durch höhere Ebene Konstrukte in modernen Programmiersprachen weitgehend irrelevant und soll nicht verwendet werden, wenn ein geeigneter Ersatz zur Verfügung steht.

Ich war Re-Lektüre vor kurzem eine Originalausgabe von Steve McConnell Code Complete und hatte über seinen Vorschlag für eine gemeinsame Codierung Problem vergessen. Ich hatte vor Jahren gelesen, als ich zum ersten Mal gestartet zu werden und nicht glaube, ich erkannt, wie nützlich das Rezept wäre. Die Codierung Problem ist folgende: wenn eine Schleife ausgeführt wird man oft Teil der Schleife ausführen muß Zustand initialisieren und dann die Schleife mit einer anderen Logik ausführen und jede Schleife mit der gleichen Initialisierung Logik endet. Ein konkretes Beispiel ist die Umsetzung String.Join (delimiter, array) -Methode.

Ich denke, jeder erste nehmen auf das Problem dieser ist. Es sei angenommen, das Anfügen Methode definiert das Argument zu Ihrem Rückgabewert hinzuzufügen.

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

  append(element);
}

Hinweis: Eine leichte Optimierung dazu ist das sonst zu entfernen und es am Ende der Schleife gesetzt. Eine Zuordnung ist in der Regel ein einzelner Befehl und äquivalent zu einem anderen und verringert die Anzahl der Basisblöcke um 1 und erhöht die grundlegenden Blockgrße des Hauptteils. Das Ergebnis ist, dass eine Bedingung in jeder Schleife ausführen, um festzustellen, ob Sie das Trennzeichen hinzufügen sollen oder nicht.

Ich habe auch gesehen und verwendet andere nimmt mit diesem gemeinsamen Schleife Problem handelt. Sie können den Anfangselementcode zunächst außerhalb der Schleife ausführen, dann Schleife von dem zweiten Element bis zum Ende durchzuführen. Sie können auch die Logik ändern, um immer das Element anhängen dann das Trennzeichen und sobald die Schleife Sie können die letzte Trennzeichen Sie hinzugefügt einfach abgeschlossen ist entfernen.

Die letztere Lösung neigt dazu, der zu sein, dass ich es vorziehen, nur weil es keinen Code kopieren. Wenn die Logik der Initialisierungssequenz jemals ändert, müssen Sie sich nicht erinnern, es an zwei Stellen zu beheben. Es erfordert jedoch zusätzliche „Arbeit“ etwas zu tun, und dann wieder rückgängig machen, so dass zumindest zusätzliche CPU-Zyklen und in vielen Fällen wie unser String.Join Beispiel auch zusätzliche Speicher benötigt.

Ich war aufgeregt dann dieses Konstrukt zu lesen

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

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

Der Vorteil hierbei ist, dass Sie keine duplizierten Code und Sie erhalten keine zusätzliche Arbeit. Sie starten Ihre Schleife auf halbem Weg in der Ausführung Ihrer ersten Schleife und das ist Ihre Initialisierung. Sie sind mit dem zu tun, während Konstrukt zu simulieren andere Schleifen beschränkt, sondern die Übersetzung ist einfach und es zu lesen ist nicht schwer.

So, jetzt die Frage. Ich ging glücklich zu versuchen, dies zu einem gewissen Code hinzufügen Ich arbeite an und fand es nicht funktioniert hat. Funktioniert hervorragend in C, C ++, einfach, aber es stellt sich in C # aus Sie in einem anderen lexikalischen Gültigkeitsbereich zu einem Label springen, die kein übergeordneter Bereich nicht ist. Ich war sehr enttäuscht. Also habe ich mich gefragt, links, was ist der beste Weg, um mit diesem sehr häufig Codierung Problem zu befassen (ich sehe es vor allem in String Generation) in C #?

Um vielleicht spezifischere Anforderungen sein:

  • Do not doppelte Code
  • Tun Sie das nicht unnötige Arbeit
  • Seien Sie nicht mehr als 2 oder 3-mal langsamer als andere Code
  • Seien Sie lesbar

Ich denke, die Lesbarkeit ist das einzige, was wohl mit dem Rezept leiden könnte ich angegeben. Doch es funktioniert nicht in C # so was ist die nächste beste Sache?

* Edit * Ich änderte meine Leistungskriterien wegen einiger der Diskussion. Die Leistung ist im Allgemeinen kein limitierender Faktor hier, so das Ziel, mehr korrekt sein sollte, nicht unvernünftig sein, nicht die schnellste jemals zu sein.

Der Grund, warum ich die alternativen Implementierungen nicht mag Ich schlage vor, weil sie entweder doppelten Code des Raum für einen Teil verändert und nicht die anderen oder für die man in der Regel wähle ich es erfordert „rückgängig gemacht“, um die Operation, die zu mehr Gedanken und Zeit erfordert rückgängig macht die Sache, die Sie gerade getan. Mit String-Manipulation insbesondere diese usually lässt Sie öffnen für durch einen Fehler oder Fehler zu Konto für ein leeres Array aus und versuchen, rückgängig zu machen, was nicht der Fall war.

War es hilfreich?

Lösung

Für Ihr spezifisches Beispiel gibt es eine Standardlösung: string.Join. Diese Griffe Hinzufügen der Begrenzer korrekt, so dass Sie nicht schreiben müssen, um die Schleife selbst.

Wenn Sie wirklich diese Texte selbst einen Ansatz zu schreiben, wollen Sie verwenden können, ist die folgende:

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

Dies sollte einigermaßen effizient sein, und ich denke, dass es vernünftig ist zu lesen. Die konstante String „“ interniert ist so diese in eine neue Zeichenfolge nicht bei jeder Iteration erstellt führen wird. Natürlich ist, wenn die Leistung entscheidend für Ihre Anwendung sollten Sie Benchmark eher als Vermutung.

Andere Tipps

Ich persönlich mag Mark Byers Option, aber man konnte immer Ihre eigene generische Methode für diese schreiben:

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

Das ist relativ einfach ... ein besonderes geben letzte Aktion ist etwas härter:

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: Als Ihr Kommentar mit der Leistung dieser anging, ich werde meinen Kommentar in dieser Antwort wiederholen: während das allgemeine Problem einigermaßen verbreitet ist, ist es nicht gemeinsam für sie eine solche sein Performance-Engpass, dass es wert ist Mikro-Optimierung um. In der Tat, ich kann mich nicht erinnern, jemals in eine Situation kommen, wo der Looping-Maschinen zu einem Engpass wurde. Ich bin sicher, dass es passiert, aber , die ist nicht „gemeinsam“. Wenn ich jemals in sie laufen, werde ich einen speziellen Fall, dass bestimmte Code, und die beste Lösung hängt von genau , was die Code Bedürfnisse zu tun.

Im Allgemeinen jedoch lege ich Wert auf Lesbarkeit und Wiederverwertbarkeit viel mehr als Mikro-Optimierung.

Sie sind schon bereit, auf foreach aufgeben. Also das sollte geeignet sein:

        using (var enumerator = array.GetEnumerator()) {
            if (enumerator.MoveNext()) {
                for (;;) {
                    append(enumerator.Current);
                    if (!enumerator.MoveNext()) break;
                    append(delimiter);
                }
            }
        }

Sie können sicherlich eine goto Lösung in C # (Anmerkung: Ich habe nicht hinzufügen null Kontrollen): create

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

Für Ihre spezifische Beispiel, das sieht ziemlich ich straighforward (und es ist eine der Lösungen, die Sie beschrieben):

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

Wenn Sie funktionelle erhalten möchten, können Sie diese Faltung Ansatz versuchen Sie es mit:

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

Obwohl es wirklich schön liest, es ist kein StringBuilder verwenden, so dass Sie vielleicht wollen Aggregate ein wenig missbrauchen, es zu benutzen:

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

Oder Sie können diese nutzen (leihen die Idee von anderen Antworten hier):

string Join(string[] array, string delimiter) {
  return array.
    Skip(1).
    Aggregate(new StringBuilder(array.FirstOrDefault()),
      (acc, s) => acc.Append(delimiter).Append(s)).
    ToString();
}

Manchmal verwende ich LINQ .First() und .Skip(1) zu handhaben ... Dies kann eine relativ saubere geben (und sehr gut lesbar) Lösung.

Verwenden Sie beispielsweise

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

[Dies nimmt es zumindest ein Element in dem Array, ein einfacher Test hinzuzufügen, wenn das, vermieden werden.]

Mit F # wäre ein weiterer Vorschlag: -)

Es gibt Möglichkeiten, wie Sie den doppelten Code umgehen „können“, aber in den meisten Fällen der duplizierten Code ist viel weniger hässlich / gefährlich als die möglichen Lösungen. Die „goto“ Lösung, die Sie zitieren scheint nicht, wie eine Verbesserung zu mir - ich wirklich Sie wirklich nicht denken Gewinn etwas signifikant (Kompaktheit, Lesbarkeit oder Effizienz), indem Sie es, während Sie das Risiko eines Programmierers erhöhen etwas falsch bekommen zu einem bestimmten Zeitpunkt in der Lebensdauer des Code.

Generell neige ich dazu, für die Annäherung gehen:

  • Ein Sonderfall für die erste (oder letzte) action
  • Schleife für die anderen Aktionen.

Dies entfernt die eingeführten Ineffizienzen durch Prüfen, ob die Schleife auf jede Zeit in der ersten Iteration ist um, und ist wirklich einfach zu verstehen. Für nicht-triviale Fälle eines Delegaten oder Hilfsmethode mit der Aktion anwenden kann Code-Duplizierung minimieren.

oder ein anderer Ansatz, den ich manchmal verwenden, in denen Effizienz nicht wichtig ist:

  • Schleife, und testen, ob der String leer ist, um zu bestimmen, ob ein Trennzeichen erforderlich ist.

Dies kann geschrieben werden kompakten und lesbar als der goto Ansatz zu sein, und erfordert keine zusätzliche Variablen / storage / Tests, um den „Sonderfall“ iteraiton zu erkennen.

Aber ich denke, Mark Byers' Ansatz eine gute, saubere Lösung für Ihr spezielles Beispiel ist.

Ich ziehe first variable Methode. Es ist wahrscheinlich nicht sauberste, aber die effizienteste Art und Weise. Alternativ können Sie Length der Sache verwenden Sie das Anhängen und vergleichen Sie sie auf Null. Funktioniert gut mit StringBuilder.

Warum bewegen sich nicht mit dem ersten Element außerhalb einer Schleife zu tun?

StringBuilder sb = new StrindBuilder()
sb.append(array.first)
foreach (var elem in array.skip(1)) {
  sb.append(",")
  sb.append(elem)
}

Wenn Sie die Funktions Weg gehen möchten, können Sie String.Join wie LINQ Konstrukt definieren, die über Typen wiederverwendbar ist.

Persönlich würde ich gehe fast immer für Code Klarheit über einige Opcode Hinrichtung zu speichern.

Beispiel:

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

    }
}
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top