¿Qué forma de terminar el ciclo de lectura es el enfoque preferido?

13

Cuando tiene que iterar un lector donde se desconoce el número de elementos para leer, y la única forma de hacerlo es seguir leyendo hasta llegar al final.

Este es a menudo el lugar donde necesita un bucle sin fin.

  1. Siempre existe el trueque indica que debe haber una declaración breako en algún lugar dentro del bloque.return

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
    
  2. Existe el método de doble lectura para bucle.

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
    
  3. Hay una sola lectura mientras que el bucle. No todos los idiomas admiten este método .

    int offset = 0;
    Record r = null;
    while((r = Read(++offset)) != null)
    {
        // do work
    }
    

Me pregunto qué enfoque es el menos probable para introducir un error, el más legible y el más utilizado.

Cada vez que tengo que escribir uno de estos pienso "tiene que haber una mejor manera" .

Reactgular
fuente
2
¿Por qué estás manteniendo un desplazamiento? ¿La mayoría de los lectores de Stream no te permiten simplemente "leer a continuación"?
Robert Harvey
@RobertHarvey en mi necesidad actual, el lector tiene una consulta SQL subyacente que usa el desplazamiento para paginar resultados. No sé cuánto duran los resultados de la consulta hasta que devuelve un resultado vacío. Pero, la pregunta no es realmente un requisito.
Reactgular
3
Parece confundido: el título de la pregunta trata sobre bucles sin fin, pero el texto de la pregunta trata sobre la terminación de bucles. La solución clásica (desde los días de la programación estructurada) es hacer un ciclo de lectura previa, mientras tiene datos, y leer nuevamente como la última acción en el ciclo. Es simple (cumple con el requisito de error), el más común (ya que ha sido escrito durante 50 años). Lo más legible es una cuestión de opinión.
andy256
@ Andy256 confundido es una condición previa al café para mí.
Reactgular
1
Ah, entonces el procedimiento correcto es 1) Beber café evitando el teclado, 2) Comenzar el ciclo de codificación.
andy256

Respuestas:

49

Daría un paso atrás aquí. Te estás concentrando en los detalles exigentes del código pero te falta la imagen más grande. Echemos un vistazo a uno de sus bucles de ejemplo:

int offset = 0;
while(true)
{
    Record r = Read(offset);
    if(r == null)
    {
        break;
    }
    // do work
    offset++;
}

¿Cuál es el significado de este código? El significado es "hacer un trabajo para cada registro en un archivo". Pero ese no es el aspecto del código . El código se ve como "mantener un desplazamiento. Abra un archivo. Ingrese un bucle sin condición final. Lea un registro. Pruebe la nulidad". ¡Todo eso antes de llegar al trabajo! La pregunta que debe hacerse es " ¿cómo puedo hacer que la apariencia de este código coincida con su semántica? " Este código debería ser:

foreach(Record record in RecordsFromFile())
    DoWork(record);

Ahora el código se lee como su intención. Separe sus mecanismos de su semántica . En su código original, mezcla el mecanismo, los detalles del bucle, con la semántica, el trabajo realizado para cada registro.

Ahora tenemos que implementar RecordsFromFile(). ¿Cuál es la mejor manera de implementar eso? ¿A quien le importa? Ese no es el código que cualquiera va a mirar. Es un código de mecanismo básico y sus diez líneas de largo. Escríbela como quieras. ¿Qué tal esto?

public IEnumerable<Record> RecordsFromFile()
{
    int offset = 0;
    while(true)
    {
        Record record = Read(offset);
        if (record == null) yield break;
        yield return record;
        offset += 1;
    }
}

Ahora que estamos manipulando una secuencia de registros calculada de manera perezosa, es posible todo tipo de escenarios:

foreach(Record record in RecordsFromFile().Take(10))
    DoWork(record);

foreach(Record record in RecordsFromFile().OrderBy(r=>r.LastName))
    DoWork(record);

foreach(Record record in RecordsFromFile().Where(r=>r.City == "London")
    DoWork(record);

Y así.

Cada vez que escriba un bucle, pregúntese "¿este bucle se lee como un mecanismo o como el significado del código?" Si la respuesta es "como un mecanismo", intente mover ese mecanismo a su propio método y escriba el código para que el significado sea más visible.

Eric Lippert
fuente
3
+1 finalmente una respuesta sensata. Esto es exactamente lo que haré. Gracias.
Reactgular
1
"intenta mover ese mecanismo a su propio método" , eso suena muy parecido a la refactorización del método de extracción, ¿no? "Convierta el fragmento en un método cuyo nombre explique el propósito del método".
mosquito
2
@gnat: Lo que sugiero es un poco más complicado que el "método de extracción", que considero como simplemente mover un trozo de código a otro lugar. La extracción de métodos es definitivamente un buen paso para que el código se lea más como su semántica. Estoy sugiriendo que la extracción del método se haga cuidadosamente, con miras a mantener las políticas y los mecanismos separados.
Eric Lippert
1
@gnat: ¡Exactamente! En este caso, el detalle que quiero extraer es el mecanismo por el cual todos los registros se leen del archivo, manteniendo la política. La política es "tenemos que trabajar un poco en cada registro".
Eric Lippert
1
Veo. De esa manera, es más fácil de leer y mantener. Al estudiar este código, puedo centrarme por separado en la política y el mecanismo, no me obliga a dividir la atención
mosto
19

No necesitas un bucle sin fin. Nunca debería necesitar uno en escenarios de lectura de C #. Este es mi enfoque preferido, suponiendo que realmente necesite mantener un desplazamiento:

Record r = Read(0);
offset=1;
while(r != null)
{
    // Do work
    r = Read(offset);
    offset++
}

Este enfoque reconoce el hecho de que hay un paso de configuración para el lector, por lo que hay dos llamadas al método de lectura. La whilecondición está en la parte superior del bucle, en caso de que no haya ningún dato en el lector.

Robert Harvey
fuente
6

Bueno, depende de tu situación. Pero una de las soluciones más "C # -ish" que se me ocurre es usar la interfaz IEnumerable incorporada y un bucle foreach. La interfaz para IEnumerator solo llama a MoveNext con verdadero o falso, por lo que el tamaño puede ser desconocido. Luego, su lógica de terminación se escribe una vez, en el enumerador, y no tiene que repetir en más de un lugar.

MSDN proporciona un ejemplo de IEnumerator <T> . También deberá crear un IEnumerable <T> para devolver el IEnumerator <T>.

J Trana
fuente
¿Puedes dar un código de ejemplo?
Reactgular
gracias, esto es lo que he decidido hacer. Si bien no creo que crear nuevas clases cada vez que tenga un bucle sin fin resuelva la pregunta.
Reactgular
Sí, sé a qué te refieres: muchos gastos generales. El verdadero genio / problema de los iteradores es que definitivamente están configurados para poder tener dos cosas iterar sobre una colección al mismo tiempo. Muchas veces no necesita esa funcionalidad, por lo que puede hacer que el contenedor alrededor de los objetos implemente IEnumerable e IEnumerator. Otra forma de ver es que cualquier colección subyacente de cosas en las que estás iterando no fue diseñada teniendo en cuenta el paradigma aceptado de C #. Y eso está bien. Sin embargo, una ventaja adicional de los iteradores es que puedes obtener todas las cosas paralelas de LINQ de forma gratuita.
J Trana
2

Cuando tengo operaciones de inicialización, condición e incremento, me gusta usar los bucles for de lenguajes como C, C ++ y C #. Me gusta esto:

for (int offset = 0, Record r = Read(offset); r != null; r = Read(++offset)){
    // loop here!
}

O esto si crees que esto es más legible. Yo personalmente prefiero el primero.

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
Trylks
fuente