Rangos

Si el compilador de D encuentra un bucle foreach

foreach (element; range) {
    // Cuerpo del bucle
}

este es reescrito internamente de forma similar a lo siguiente:

for (auto __rangeCopy = range;
     !__rangeCopy.empty;
     __rangeCopy.popFront())
 {
    auto element = __rangeCopy.front;
    // Cuerpo del bucle
}

Se llama rango (range en inglés) a cualquier objeto que cumpla con la siguiente interfaz:

    interface InputRange(E) {
        bool empty();
        E front();
        void popFront();
    }

Más concretamente esta interfaz es un InputRange y es gracias a ella por la que los elementos de los objetos de este tipo pueden ser iterados.

En el ejemplo de la derecha se puede ver la implementación y el uso de un rango de este tipo (input range).

Pereza (laziness)

Los rangos son perezosos (lazy en inglés), lo que significa que estos no son evaluados hasta que no es requerido. Es por esto por lo que se puede coger un número finito de elementos de un rango infinito:

42.repeat.take(3).writeln; // [42, 42, 42]

Tipos de datos por valor y por referencia

Si el objeto de tipo rango es un valor, el rango se copia y sólo esta copia será consumida en la iteración:

auto r = 5.iota;
r.drop(5).writeln; // []
r.writeln; // [0, 1, 2, 3, 4]

Si el objeto de tipo rango es una referencia (por ejemplo una clase declarada mediante class o un std.range.refRange), los valores del rango serán consumidos en la iteración y no podrán ser restablecidos.

auto r = 5.iota;
auto r2 = refRange(&r);
r2.drop(5).writeln; // []
r2.writeln; // []

Los InputRanges que se pueden copiar son ForwardRanges

Muchos de los rangos usados en la librería estándar son estructuras (struct), por lo que los bucles foreach son generalmente no destructivos, aunque esto no se garantiza. Si esta garantía es importante, se puede usar una especialización de los InputRange: rangos con el método .save, también conocidos como ForwardRange:

interface ForwardRange(E) : InputRange!E {
    typeof(this) save();
}
// por valor (estructuras)
auto r = 5.iota;
auto r2 = refRange(&r);
r2.save.drop(5).writeln; // []
r2.writeln; // [0, 1, 2, 3, 4]

Los ForwardRanges se pueden extender para ser bidireccionales y con acceso aleatorio a elementos

Hay dos extensiones de los ForwardRange: (1) un rango bidireccional y (2) un rango con acceso aleatorio a sus elementos.

Un rango bidireccional permite la iteración desde el final:

interface BidirectionalRange(E) : ForwardRange!E {
     E back();
     void popBack();
}
5.iota.retro.writeln; // [4, 3, 2, 1, 0]

Un rango con acceso aleatorio a elementos tiene una longitud conocida, length, y se puede acceder a cada elemento de forma directa:

interface RandomAccessRange(E) : ForwardRange!E {
     E opIndex(size_t i);
     size_t length();
}

Los rangos con acceso aleatorio a elementos más famosos son los arrays de D.

auto r = [4, 5, 6];
r[1].writeln; // 5

Algoritmos con acceso perezoso (lazy) a rangos

Las funciones en std.range y std.algorithm proporcionan bloques de construcción que hacen uso de esta interfaz de rangos. Los rangos permiten componer complejos algoritmos detrás de objetos sobre los que se itera con facilidad. Además, los rangos permiten crear objetos perezosos (lazy en inglés) que sólo realizan cálculos cuando realmente se necesitan en la iteración, es decir, cuando se accede al siguiente elemento del rango. Algoritmos especiales sobre rangos se verán en la sección perlas de D.

En profundidad

rdmd playground.d