Pregunta

Para empezar voy a decir que estoy de acuerdo que Goto declaraciones hacen en gran parte irrelevante por construcciones de alto nivel en lenguajes de programación modernos y no debe ser usado cuando un sustituto adecuado disponible.

Me relectura una edición original del Código de Steve McConnell completo recientemente y había olvidado de su sugerencia para un problema común de codificación. Había leído hace años cuando por primera vez estaba empezando y no creo que me di cuenta de lo útil que sería la receta. El problema de codificación es la siguiente: cuando se ejecuta un bucle a menudo es necesario para ejecutar parte del bucle para inicializar el estado y luego ejecutar el bucle con alguna otra lógica y terminando cada bucle con la misma lógica de inicialización. Un ejemplo concreto está implementando método string.join (delimitador, array).

creo primera toma de todo el mundo sobre el problema es el siguiente. Supongamos que el método de agregación se define para añadir el argumento a su valor de retorno.

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

  append(element);
}

Nota: Poca optimización para esto es para quitar el otro y ponerlo al final del bucle. Una asignación de ser por lo general una sola instrucción y equivalente a una persona y disminuye el número de bloques básicos por 1 y aumenta el tamaño del bloque de base de la parte principal. El ser resultado de que ejecute una condición en cada bucle para determinar si se debe añadir el delimitador o no.

También he visto y utilizado otros tarda en hacer frente a este problema común de lazos. Puede ejecutar el código de elementos inicial por primera vez fuera del bucle, a continuación, realizar el bucle desde el segundo elemento al final. También puede cambiar la lógica para siempre añadir el elemento a continuación, el delimitador y una vez que se ha completado el bucle se puede simplemente eliminar el último delimitador ha agregado.

La última solución tiende a ser el que prefiero sólo porque no duplica ningún código. Si la lógica de la secuencia de inicialización cambia nunca, usted no tiene que recordar para fijarlo en dos lugares. Sin embargo requiere extra "trabajo" para hacer algo y luego deshacerlo, causando al menos ciclos de CPU adicionales y en muchos casos como nuestro ejemplo string.join necesita más memoria también.

I se excitó luego de leer esta construcción

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

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

La ventaja aquí es que no se obtiene ningún código duplicado y se obtiene ningún trabajo adicional. Se inicia el bucle de medio camino en la ejecución de su primer bucle y que es su inicialización. Usted está limitado a la simulación de otros lazos con el hacer mientras constructo pero la traducción es fácil y la lectura no es difícil.

Por lo tanto, ahora la pregunta. Felizmente fui a probar a añadir esto a un cierto código que estaba trabajando y se encontró que no funcionaba. Funciona muy bien en C, C ++, básico pero resulta que en C # no se puede saltar a una etiqueta dentro de un ámbito léxico diferente que no es un ámbito primario. Yo estaba muy decepcionado. Así que me quedé pensando, ¿cuál es la mejor manera de hacer frente a este problema de codificación muy común (lo veo sobre todo en la generación de cuerdas) en C #?

Para quizá sea más específico con los requisitos de:

  • Do código no duplicado
  • No hacer trabajo innecesario
  • No tenga más de 2 o 3 veces más lento que otro código
  • ser legible

Creo que la lectura es la única cosa que podría posiblemente sufrir con la receta he dicho. Sin embargo, no funciona en C # ¿Cuál es la mejor alternativa?

Editar * * He cambiado mis criterios de rendimiento debido a algunas de la discusión. El rendimiento general no es un factor limitante aquí, así que la meta más correctamente debe estar a no ser razonable, por no ser el más rápido cada vez.

La razón me gusta las implementaciones alternativas que sugiero es debido a que el código sea duplicado, que deja espacio para el cambio de una parte y no la otra, o para el que eligen generalmente se requiere "deshacer" la operación que requiere una reflexión adicional y tiempo para deshacer lo que acabas de hacer. Con la manipulación de cadenas, en particular, este usuaLLY deja abrir por fuera por uno los errores o en su defecto para dar cuenta de una matriz vacía y tratando de deshacer algo que no sucedió.

¿Fue útil?

Solución

Para su ejemplo específico hay una solución estándar: string.Join. Este asas añadiendo el delimitador correctamente, de modo que usted no tiene que escribir el bucle de ti mismo.

Si realmente desea escribir esto por sí mismo un enfoque que puede utilizar es la siguiente:

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

Esto debe ser razonablemente eficiente y creo que es razonable interpretar. La constante de cadena "" es internado por lo que esto no dará lugar a una nueva cadena que se crea en cada iteración. Por supuesto, si el rendimiento es fundamental para su aplicación se debería comparar en lugar de adivinar.

Otros consejos

En lo personal me gusta la opción de Mark Byers, pero siempre se puede escribir su propio método genérico para esto:

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

Esto es relativamente sencillo ... dando una especial última acción es un poco más difícil:

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: A medida que su comentario se refiere a la realización de esto, voy a reiterar mi comentario en esta respuesta: mientras que este problema general es bastante común, que es no común para que sea como una cuello de botella que vale la pena micro-optimización de alrededor. De hecho, no puedo recordar haber viniendo a través de una situación en la que la maquinaria de bucle se convirtió en un cuello de botella. Estoy seguro de que suceda, pero que no es "común". Si alguna vez me encuentro con él, voy especial caso de que el código en particular, y la mejor solución dependerá de exactamente lo que necesita el código para hacerlo.

En general, sin embargo, yo valor de legibilidad y reutilización más más de micro-optimización.

Ya están dispuestos a renunciar a foreach. Por lo que este debe ser adecuado:

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

Por supuesto que puede crear una solución goto en C # (nota: No añadí cheques 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();
}

Para su específica ejemplo, esto parece bastante directo conducir a mí (y es una de las soluciones que ha descrito):

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 desea obtener funcional, puede intentar utilizar este método de plegado:

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

A pesar de que se lee muy bien, no es el uso de un StringBuilder, así que sería bueno para abusar Aggregate un poco para utilizarlo:

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

O usted puede utilizar este (préstamo de la idea de otras respuestas aquí):

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

A veces uso .First() LINQ y .Skip(1) de manejar esto ... Esto puede dar una solución relativamente limpia (y muy legible).

El uso que usted ejemplo,

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

[Este asume que hay al menos un elemento de la matriz, una prueba sencilla para añadir si eso es que deben evitarse.]

Usar F # sería otra sugerencia: -)

Hay maneras que usted "puede" moverse por el código duplicado, pero en la mayoría de los casos, el código duplicado es mucho menos feo / peligroso que las posibles soluciones. El "Goto" solución que usted cita no parece ser una mejora para mí - yo realmente no creo que realmente se gana nada significativo (compacidad, la legibilidad o la eficiencia) mediante el uso de ella, mientras que aumenta el riesgo de un programador conseguir algo mal en algún momento de la vida del código.

En general, tiendo a ir para el enfoque:

  • Un caso especial de la primera (o última) la acción
  • bucle para las otras acciones.

Esto elimina las ineficiencias introducidas mediante la comprobación de si el bucle está en la primera iteración en cada ocasión, y es muy fácil de entender. Para los casos no triviales, utilizando un método de delegado o ayudante para aplicar la acción puede minimizar la duplicación de código.

O otro enfoque que utilizo en ocasiones donde la eficiencia no es importante:

  • lazo, y prueba si la cadena está vacía para determinar si se requiere un delimitador.

Esto puede ser escrito para ser más compacto y fácil de leer que el enfoque Goto, y no requiere ningún variables / almacenamiento / pruebas adicionales para detectar el "caso especial" iteraiton.

Sin embargo, creo que el enfoque Mark Byers' es una buena solución limpia para su caso particular.

Yo prefiero método de la variable first. Probablemente no es manera más limpia, pero más eficiente. Otra posibilidad es utilizar Length de la cosa que anexar a y compararlo con cero. Funciona bien con StringBuilder.

¿Por qué no se mueven tratar con primer elemento fuera de un bucle?

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

Si quieres ir a la ruta funcional, se podría definir como la construcción string.join LINQ que se pueden reutilizar en tipos.

En lo personal, yo casi siempre van de la claridad del código sobre el ahorro de unas pocas ejecuciones de código de operación.

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

    }
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top