Domanda

Per iniziare con devo dire che sono d'accordo che goto dichiarazioni sono in gran parte realizzati irrilevante da costrutti di livello superiore nei moderni linguaggi di programmazione e non dovrebbe essere usato quando un sostituto adatto è disponibile.

Stavo rileggendo un'edizione originale del Codice di Steve McConnell completa di recente e aveva dimenticato il suo suggerimento per un problema di codifica comune. Avevo letto anni fa, quando ero prima guida introduttiva e non credo che mi sono reso conto quanto sia utile la ricetta sarebbe. Il problema di codifica è il seguente: durante l'esecuzione di un ciclo spesso è necessario eseguire una parte del ciclo per inizializzare stato e poi eseguire il ciclo con qualche altra logica e termina ogni ciclo con la stessa logica di inizializzazione. Un esempio concreto sta attuando il metodo String.Join (delimitatore, array).

penso primo ciak di tutti sul problema è questo. Assumere il metodo append è definito in modo da aggiungere l'argomento al valore di ritorno.

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

  append(element);
}

Nota: Una leggera ottimizzazione per questo è quello di rimuovere il resto e metterlo alla fine del ciclo. Un'assegnazione di solito essere una singola istruzione ed equivalente a un altro e diminuisce il numero di blocchi di base da 1 e aumenta la dimensione del blocco di base della parte principale. L'essere risultato che esegue una condizione in ogni ciclo per determinare se è necessario aggiungere il delimitatore oppure no.

Ho anche visto e usato altri assume affrontare questo problema ciclo comune. È possibile eseguire il codice elemento iniziale primo al di fuori del ciclo, quindi eseguire il ciclo dal secondo elemento alla fine. È inoltre possibile modificare la logica di aggiungere sempre l'elemento poi il delimitatore e una volta che il ciclo è completato si può semplicemente rimuovere l'ultimo delimitatore è stato aggiunto.

La seconda soluzione tende ad essere quella che preferisco solo perché non duplica alcun codice. Se la logica della sequenza di inizializzazione cambia mai, non c'è bisogno di ricordare per risolvere il problema in due posti. Ha, tuttavia richiede più "lavoro" per fare qualcosa e poi annullarla, provocando almeno cicli di CPU in più e in molti casi come il nostro esempio String.Join richiede memoria extra pure.

Ero eccitato poi a leggere questo costrutto

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

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

Il vantaggio è che si ottiene nessun codice duplicato e si ottiene alcun lavoro supplementare. Si avvia il ciclo a metà strada verso l'esecuzione del primo ciclo e che è la vostra inizializzazione. Si sono limitati a simulare altri loop con il fare, mentre costrutto ma la traduzione è facile e la lettura non è difficile.

Quindi, ora la domanda. Sono felicemente andato a provare ad aggiungere questo per un certo codice stavo lavorando su e l'ho trovato non ha funzionato. Funziona alla grande in C, C ++, di base ma si scopre che in C # non è possibile passare a un'etichetta all'interno di un diverso ambito lessicale che non è un ambito genitore. Sono rimasto molto deluso. Così mi è stato lasciato chiedendo, qual è il modo migliore per affrontare questo problema di codifica molto comune (lo vedo in gran parte nella generazione string) in C #?

Per forse essere più precisi con i requisiti:

  • Do codice non duplicato
  • Non fare lavoro inutile
  • Non essere più di 2 o 3 volte più lento di altro codice
  • essere leggibile

Credo che la leggibilità è l'unica cosa che potrebbe forse soffrire con la ricetta ho detto. Tuttavia non funziona in C # quindi qual è la cosa migliore?

* Edit * Ho cambiato i miei criteri di prestazione a causa di alcune delle discussioni. Le prestazioni non è generalmente un fattore limitante qui, quindi l'obiettivo più correttamente dovrebbe essere quello di non essere irragionevole, di non essere il più veloce mai.

La ragione per cui non mi piace le implementazioni alternative che suggerisco è perché il codice sia duplicato, che lascia spazio per cambiare una parte e non l'altro o per quello che in genere scelgo Richiede "annullare" l'operazione che richiede pensiero in più e il tempo per annullare la cosa che hai appena fatto. Con la manipolazione stringa in particolare questo usualascia lly si apre per off da uno errori o, in mancanza di considerazione per un array vuoto e cercando di annullamento qualcosa che non è accaduto.

È stato utile?

Soluzione

Per il vostro esempio specifico esiste una soluzione standard: string.Join. Questo gestisce aggiungendo il delimitatore correttamente in modo che non si deve scrivere il loop da soli.

Se si vuole veramente di scrivere da soli un approccio è possibile utilizzare è il seguente:

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

Questo dovrebbe essere ragionevolmente efficiente e penso che sia ragionevole da leggere. La stringa costante "" viene internato quindi non provocherà una nuova stringa viene creato ad ogni iterazione. Naturalmente, se le prestazioni sono critiche per la vostra applicazione si dovrebbe punto di riferimento, piuttosto che indovinare.

Altri suggerimenti

Personalmente opzione di Mark Byer come, ma si può sempre scrivere il proprio metodo generico per questo:

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

Questo è relativamente semplice ... dando uno speciale ultima azione è leggermente più difficile:

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: Come il tuo commento riguardava le prestazioni di questo, io ribadisco il mio commento in questa risposta: mentre questo problema generale è ragionevolmente comune, è non comune per essere tale collo di bottiglia che vale la pena micro-ottimizzazione intorno. Anzi, non mi ricordo mai imbattersi in una situazione in cui le macchine loop è diventato un collo di bottiglia. Sono sicuro che accade, ma che non è "comune". Se ho mai incontrato, te lo special-case quel particolare codice, e la soluzione migliore dipenderà esattamente quello che il codice deve fare.

In generale, comunque, ho valore leggibilità e riutilizzabilità molto più di micro-ottimizzazione.

Si sono già disposti a rinunciare a foreach. Quindi questo dovrebbe essere adatto:

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

Si può certamente creare una soluzione goto in C # (nota: non ho aggiunto assegni 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();
}

Per la vostra specifica esempio, questo sembra piuttosto straighforward a me (ed è una delle soluzioni che hai descritto):

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

Se si desidera ottenere funzionale, si può provare a utilizzare questo approccio pieghevole:

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

Anche se si legge veramente bello, non è con un StringBuilder, così si potrebbe desiderare di abusare Aggregate un po 'di usarlo:

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

In alternativa, è possibile utilizzare questo (prendendo in prestito l'idea da altre risposte qui):

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

A volte uso LINQ .First() e .Skip(1) per gestire questa situazione ... Questo può dare una soluzione relativamente pulita (e molto leggibile).

Utilizzando ti esempio,

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

[Ciò presuppone v'è almeno un elemento della matrice, un test facile aggiungere se questo è da evitare.]

Usa F # sarebbe un altro suggerimento: -)

Ci sono modi per "possibile" aggirare il codice raddoppiata, ma nella maggior parte dei casi il codice duplicato è molto meno brutto / pericoloso di quanto le possibili soluzioni. La soluzione "goto" si citazione non sembra come un miglioramento per me - non mi pensi veramente guadagno nulla di significativo (compattezza, leggibilità o efficienza) usandolo, mentre si aumenta il rischio di un programmatore ottenere qualcosa di sbagliato ad un certo punto nel corso della vita del codice.

In generale, tendo ad andare per l'approccio:

  • Un caso particolare per il primo (o l'ultimo) l'azione
  • ciclo per le altre azioni.

Questo elimina le inefficienze introdotte verificando se l'anello è nella prima iterazione su ogni volta, ed è veramente facile da capire. Per i casi non banali, utilizzando un metodo delegato o di supporto per applicare l'azione può ridurre al minimo la duplicazione del codice.

O un altro approccio che uso a volte in cui l'efficienza non è importante:

  • ciclo, e verificare se la stringa è vuota per determinare se è richiesto un delimitatore.

Questo può essere scritto per essere più compatto e leggibile rispetto all'approccio goto, e non richiede ulteriori variabili / stoccaggio / test per rilevare il "caso speciale" iteraiton.

Ma penso che l'approccio Mark Byers' è una buona soluzione pulita per il vostro esempio particolare.

Io preferisco il metodo variabile first. Probabilmente non è modo più pulito ma più efficiente. In alternativa è possibile utilizzare Length della cosa che aggiungendo per e confrontarlo con zero. Funziona bene con StringBuilder.

Perché non si muovono trattare con primo elemento al di fuori di un ciclo?

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

Se si vuole andare via funzionale, è possibile definire String.Join come LINQ costrutto che è riutilizzabile tra i tipi.

Personalmente, vorrei quasi sempre andare per il codice chiarezza sulle risparmiare qualche esecuzioni di codice operativo.

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

    }
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top