Question

Pour commencer, je vais dire que je suis d'accord que goto déclarations sont en grande partie hors de propos par des constructions faites de plus haut niveau dans les langages de programmation modernes et ne devrait pas être utilisé lorsqu'un substitut approprié est disponible.

Je relisais une édition originale du Code de Steve McConnell complète récemment et avait oublié sa suggestion pour un problème de codage commun. Je l'avais lu il y a des années quand j'ai d'abord commencé et se ne pense pas que je réalisé à quel point la recette utile serait. Le problème de codage est le suivant: lors de l'exécution d'une boucle que vous avez souvent besoin d'exécuter une partie de la boucle pour initialiser l'état puis exécutez la boucle avec une autre logique et se terminant chaque boucle avec la même logique d'initialisation. Un exemple concret met en oeuvre la méthode String.Join (délimiteur, tableau).

Je pense d'abord prendre le problème de tout le monde est cela. On suppose la méthode append est définie pour ajouter l'argument à votre valeur de retour.

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

  append(element);
}

Note: Une légère optimisation pour cela est d'enlever le reste et le mettre à la fin de la boucle. Une affectation étant habituellement une seule instruction et équivalent à un autre et diminue le nombre de blocs de base de 1 et augmente la taille du bloc de base de la partie principale. Le résultat étant que exécuter une condition dans chaque boucle pour déterminer si vous devez ajouter le séparateur ou non.

J'ai aussi vu et utilisé d'autres prend face à ce problème de boucle commune. Vous pouvez exécuter le code d'élément initial d'abord en dehors de la boucle, puis effectuez votre boucle à partir du deuxième élément à la fin. Vous pouvez également modifier la logique de toujours ajouter l'élément le délimiteur et une fois que la boucle est terminée, vous pouvez simplement supprimer le dernier délimiteur que vous avez ajouté.

La dernière solution a tendance à être celui que je préfère seulement parce qu'elle ne correspond pas de code. Si la logique de la séquence d'initialisation change jamais, vous ne devez pas se rappeler de le réparer en deux endroits. Il nécessite cependant supplémentaire « travail » à faire quelque chose, puis le défaire, causant au moins des cycles cpu supplémentaires et dans de nombreux cas comme notre exemple string.join nécessite davantage de mémoire ainsi.

je suis excité alors de lire cette construction

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

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

L'avantage ici étant que vous obtenez pas de code dupliqués et vous obtenez pas de travail supplémentaire. Vous commencez votre chemin de demi-boucle dans l'exécution de votre première boucle et qui est votre initialisation. Vous êtes limité à simuler d'autres boucles avec le faire en construction mais la traduction est facile et la lecture il est pas difficile.

Alors, maintenant la question. Je suis allé heureusement pour essayer d'ajouter ceci à un code que je travaillais sur et trouvé cela ne fonctionnait pas. Fonctionne très bien en C, C ++, de base, mais il se trouve en C # vous ne pouvez pas passer à une étiquette à l'intérieur d'un champ lexical différent qui n'est pas un champ parent. J'ai été très déçu. Donc, je me suis demandé, quelle est la meilleure façon de traiter ce problème de codage très fréquent (je vois surtout dans la génération de chaîne) en C #?

Pour peut-être plus spécifique aux exigences:

  • Ne pas le code en double
  • Ne pas faire le travail inutile
  • Ne vous laissez pas plus de 2 ou 3 fois plus lent que tout autre code
  • être lisible

Je pense que la lisibilité est la seule chose qui pourrait sans doute souffrir avec la recette je l'ai dit. Toutefois, il ne fonctionne pas en C # Quelle est donc la meilleure chose?

* Edit * J'ai changé mes critères de performance en raison de certaines des discussions. La performance est généralement pas un facteur limitant ici, donc l'objectif plus correctement devrait être de ne pas être déraisonnable, de ne pas être le plus rapide jamais.

La raison pour laquelle je n'aime pas les implémentations alternatives que je propose est parce qu'ils le code soit double qui laisse place pour changer une partie et non l'autre ou pour celui que je choisis généralement, il exige « défaisant » l'opération qui exige une réflexion supplémentaire et le temps de annuler la chose que vous venez de faire. Avec la manipulation de chaînes en particulier ce usualaisse lly vous ouvrez pour hors d'une erreur ou défaut de tenir compte d'un tableau vide et d'essayer de défaire quelque chose qui n'a pas eu lieu.

Était-ce utile?

La solution

Pour votre exemple particulier, il y a une solution standard: string.Join. Cette poignées en ajoutant le séparateur correctement de sorte que vous ne devez pas écrire vous-même boucle.

Si vous voulez vraiment écrire vous-même une approche que vous pouvez utiliser est la suivante:

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

Cela devrait être raisonnablement efficace et je pense qu'il est raisonnable de lire. La chaîne constante « » est interné si ce ne sera pas entraîner une nouvelle chaîne en cours de création à chaque itération. Bien sûr, si la performance est essentielle pour votre application, vous devriez référence plutôt que de deviner.

Autres conseils

Personnellement, je aime l'option de Mark Byer, mais vous pouvez toujours écrire votre propre méthode générique pour ceci:

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

C'est relativement simple ... donner un spécial dernier l'action est un peu plus 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: En tant que votre commentaire a été préoccupé par la performance de cela, je vais répéter mon commentaire dans cette réponse: alors que ce problème général est assez commun, il est pas commun pour qu'il soit un goulot d'étranglement qu'il vaut la peine de micro-optimisation autour. En effet, je ne me souviens pas venir jamais rencontré une situation où le mécanisme de bouclage est devenu un goulot d'étranglement. Je suis sûr que ça arrive, mais que est pas « commun ». Si jamais je tombe sur ce, je vais cas particulier ce code particulier, et la meilleure solution dépendra de exactement quels sont les besoins de code à faire.

En général, cependant, la lisibilité de la valeur I et réutilisabilité beaucoup plus de micro-optimisation.

Vous êtes déjà prêt à renoncer à foreach. Donc, cela devrait convenir:

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

Vous pouvez certainement créer une solution goto en C # (note: Je n'ai pas ajouté de chèques 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();
}

Pour spécifique par exemple, cela semble assez straighforward pour moi (et il est l'une des solutions que vous avez décrites):

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

Si vous voulez obtenir fonctionnel, vous pouvez essayer d'utiliser cette approche de pliage:

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

Bien qu'il lit vraiment agréable, il est de ne pas utiliser un StringBuilder, vous voudrez peut-être abuser Aggregate un peu à l'utiliser:

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

Ou vous pouvez l'utiliser (l'idée d'emprunter d'autres réponses ici):

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

Parfois, j'utilise LINQ .First() et .Skip(1) pour gérer cette ... Cela peut donner une solution relativement propre (et très lisible).

Utilisez-vous par exemple,

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

[Cela suppose qu'il est au moins un élément du tableau, un test facile à ajouter, si c'est à éviter.]

Utiliser F # serait une autre suggestion: -)

Il existe des moyens que vous « pouvez » contourner le code doublé, mais dans la plupart des cas, le code est dupliquée beaucoup moins laid / dangereux que les solutions possibles. La « goto » solution que vous citez ne semble pas comme une amélioration pour moi - je ne pense pas vraiment quoi que ce soit vraiment important gain (compacité, la lisibilité ou l'efficacité) en utilisant, alors que vous augmentez le risque d'un programmeur obtenir quelque chose de mal à un moment donné dans la vie du code.

En général, je tendance à aller pour l'approche:

  • Un cas spécial pour le premier (ou dernier) Action
  • boucle pour les autres actions.

Cela supprime les inefficacités introduites en vérifiant si la boucle est dans la première itération sur chaque fois, et il est vraiment facile à comprendre. Pour les cas non trivial, en utilisant une méthode de délégué ou aide pour appliquer l'action peut réduire la duplication de code.

Ou une autre approche que je l'utilise parfois où l'efficacité est pas important:

  • boucle, et le test si la chaîne est vide pour déterminer si un delimiter est nécessaire.

Il peut être écrit pour être plus compact et plus lisible que l'approche goto, et ne nécessite pas de variables supplémentaires / stockage / tests pour détecter le « cas particulier » iteraiton.

Mais je pense que l'approche de Mark Byers est une bonne solution pour votre propre exemple particulier.

Je préfère méthode variable first. Il est sans doute pas le plus propre mais plus moyen efficace. Sinon, vous pouvez utiliser Length de la chose que vous annexant et comparer à zéro. Fonctionne bien avec StringBuilder.

Pourquoi ne bouge pas traiter avec le premier élément en dehors d'une boucle?

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

Si vous voulez aller la route fonctionnelle, vous pouvez définir String.Join comme construction de LINQ qui est réutilisable pour tous les types.

Personnellement, j'aller presque toujours pour la clarté du code sur sauver quelques exécutions opcodes.

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

    }
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top