He leído ultimamente:
En este artículo se hará una introducción al lenguaje de programación D para lo cual se explicarán las principales diferencias con respecto a C y C++; por ello sería aconsejable que el lector de este pequeño artículo conozca las características dichos lenguajes (u otros similares, como Java o C#) y algunos conceptos como la programación orientada a objetos o el control de errores mediante excepciones aunque no es necesario que sea un experto en dichos temas.
Este artículo no es un manual ni un tutorial y por lo tanto no es la mejor referencia para aprender el lenguaje, aunque alguien que conozca C++ o de Java podrá tener una idea muy completa de D tras leerlo.
En mi opinión, D es un lenguaje con un potencial increible, capaz de proporcionar al desarrollador la productividad y la elegancia de Python o Ruby pero con la eficiencia de C.
Este documento no está terminado y se irá ampliando con el tiempo; puede verse una lista de las cuestiones pendientes al final del mismo. La versión actual es la: 0.3 actualizada el 25 de abril de 2007.
La clasificación del lenguaje de programación D sería: lenguaje compilado a código nativo (con compilación opcional a .NET como C#), orientado a objetos pero permitiendo programar con funciones libres y clases ligeras (structs), con plantillas y mixins (que permiten la programación genérica) y con posibilidad de acceso a bajo nivel.
Esta clasificación parece exactamente la misma que la de C++ y no es casual, pues C++ es el lenguaje al que D aspira a sustituir. D es un lenguaje evolucionario, no revolucionario y en un principio puede parecer que no aporta demasiado sobre C++. Pero no son las características generales sino los detalles de las mismas los que marcan la diferencia. En primer lugar D conserva todas las características de expresividad de C++ (cosa que ni C# ni mucho menos Java consiguen en su afán por hacerse lenguajes más accesibles), pero con una sintaxis y unas construcciones mucho más sencillas y lógicas. Además, otro de los puntos fuertes de C++, su rendimiento, también se ve reflejado en D (en algunas ocasiones incluso superado.)
Por otro lado D cuenta con muchas otra características de las que C++ no dispone, de las cuales vamos a hacer un pequeño repaso a continuación. Quisiera recordar al lector que, al contrario de lo que pasa con Java o C#, estas características no suponen una perdida apreciable de rendimiento para D en comparación con C++:
En C++ cuando creamos objetos que queremos que transciendan el ámbito de la función, método o bloque de código que los ha creado los creamos con utilizando el operador "new". Este operador se encargará de asignar automáticamente la memoria que sea necesaria para el objeto y llamará a su constructor, devolviéndonos una referencia al objeto ya inicializado que podremos almacenar en una variable del tipo del objeto.
Cuando ya no vamos a necesitar el objeto procedemos a borrarlo con el operador "delete" lo que liberará la memoria asignada por el objeto. ¿Parece sencillo verdad? Bien, debería serlo, pero en la práctica es raro el programa en C++ que no tiene o ha tenido una fuga de memoria porque el flujo de ejecución de una parte del mismo termina antes de llegar al delete (y por lo tanto la memoria queda asignada para siempre sin posibilidad de recuperarla) o que ha fallado porque erróneamente se ha hecho un delete del mismo objeto dos veces o se ha intentado acceder (también erróneamente) a un miembro de un objeto previamente borrado. Obviamente estos errores son evitables pero errar es humano y cuantas más tareas delegue en el programador el lenguaje de programación más posibilidad hay de que haya errores (si no fuera así, todos seguiríamos haciendo ensamblador.)
La gestión automática de memoria quiere decir que el programador seguirá creando los nuevos objetos con "new" pero ya no tendrá que preocuparse de borrarlos con "delete" porque existirá un "recolector de basura" que se encargará de eliminar automáticamente los objetos para los que ya no exista ninguna referencia. Es decir, si tenemos la siguiente función:
void pierdeaceite() {
Obj *o = new Obj();
}
En C++ esto produciría una fuga de memoria cada vez que la llamáramos porque al terminar la función ya no habría ninguna referencia a "o" (y por lo tanto no podríamos usarlo) y sin embargo al no haber hecho delete seguiría existiendo en la memoria. El código equivalente en D:
void nopierdeaceite() {
Obj o = new Obj();
}
No produciría ninguna perdida de memoria porque el recolector de basura detectaría que como "o" fue declarado dentro de "nopierdeaceite" y su alcance termina al terminar la función, la memoria puede ser liberada.
Esta característica (con la que cuentan también C# y Java) puede desactivarse si por cuestiones de rendimiento o de cualquier otro tipo no queremos que se ejecute el recolector de basura llamando a gc.disable() y luego reactivarse llamando a gc.enable(). Lo más espectacular es que mientras el recolector de basura sigue desactivado las referencias a los objetos que hagamos con "new" se seguirán contabilizando por lo que una vez que re-activemos la recolección de basura esos objetos serán igualmente gestionados (no necesitaremos llamar a delete sobre los mismos.) Esto no es así en C# ni en Java,por ejemplo.
Finalmente también podemos borrar, como en C++, nosotros mismos los objetos llamando a "delete" sin que ello cause ningún tipo de conflicto con el recolector de basura.
C++ permite gestionar los errores mediante el mecanismo de manejo de excepciones, aunque por herencia de C en demasiadas ocasiones se sigue utilizando el sistema de "valores mágicos devueltos por la función".
En D el sistema de manejo de excepciones es superior al de C++ al incorporar algunas características de lenguajes más recientes. Cuando una excepción producida en código D no se captura, se muestra un mensaje de error con información de la excepción. La sintáxis es parecida a la de otros lenguajes con manejo de excepciones:
try {
throw new Exception("mensaje de error");
}
catch(Exception e) {
printf("capturada la excepción, mensaje: %.*s\n", e.msg);
} finally {
printf("este mensaje se mostrará siempre");
}
El código que se ejecuta dentro del Try en caso de lanzar una excepción se ejecutará el código que esté en el bloque del "catch" si la excepción especificada entre paréntesis coincide con la lanzada o es padre de la misma en la jerarquía de la excepción (las excepciones son clases con su propia jerarquía.) El padre de la jerarquía es la clase "Exception" así que en ejemplo el catch capturaría cualquier excepción. En el caso del constructor de la excepción en la instrucción que lo lanza (el "throw") hemos especificado una cadena como parámetro pues en la clase Exception el argumento del constructor es un mensaje que queda almacenado en el miembro "msg", que generalmente se va a usar para amplíar la información del error, y ese miembro es el que imprimimos precisamente en el printf del catch.
El bloque con la instrucción "finally" se ejecutará siempre, se produzca una excepción o no y en caso de que se produzca incluso aunque se capture en un catch. Por lo tanto la salida de este programa sería:
capturada la excepción, mensaje: mensaje de error este mensaje se mostrará siempre
En C/C++ la estructuración del código se hace creando "ficheros de cabecera" con extensión .h o .hpp que contienen las declaraciones de los símbolos que cada subparte explorta, y luego esos ficheros de cabecera se importan usando la directiva del preprocesador #include. Esta importación lo único que hace es incluir el código del .h importado allí donde se encuentra la directiva import. Esto lleva a problemas cuando varios ficheros de cabecera definen un mismo símbolo, con soluciones poco elegantes.
C++ añade además el concepto de namespaces que permite agrupar los símbolos de distintos ficheros en un único espacio de nombres lógico.
En D, como en casi todos los lenguajes modernos, la estructuración del código y las bibliotecas se hace usando módulos y paquetes. Un módulo no es más que un fichero fuente de código D (generalmente con extensión .d).
Para que un módulo pueda acceder a los símbolos de otro módulo se usa la sentencia import:
---- inicio modulo1.d ---- module modulo1; int simbolomodulo1 = 10; ---- fin modulo1.d ---- ---- inicio modulo2.d ---- module modulo2; import modulo1; writefln( simbolomodulo1 ); // correcto ---- fin modulo2.d ----
Como puede verse, al contrario de lo que sucede en otros lenguajes, cuando importamos los símbolos de un módulo no hace falta anteponer el nombre del módulo con un punto antes de llamar a un símbolo; en modulo2 podemos ver que hemos usado simbolomodulo1 en lugar de modulo1.simbolomodulo1 como se haría en otros lenguajes. A pesar de ello la notación de modulo.simbolo se permite también en D, y es necesaria útil cuando se importan dos módulos que definen símbolos homónimos para indicar el módulo del cual queremos usar el símbolo; no hacerlo así en ese caso es un error de compilación.
Además, también se puede ver que al principio de cada modulo se usa la palabra clave module seguida del nombre del módulo. Esta sentencia es opcional y para módulos no pertenecientes a paquetes no suele ser necesario pues por defecto se toma como nombre del módulo el nombre del fichero sin la extensión. Si existe, debe ser la primera sentencia (comentarios aparte) del fichero fuente.
Si quieremos que los símbolos importados sean accedidos siempre mediante la notación modulo.símbolo usaremos un static import:
---- inicio modulo2.d --- static import modulo1; // writefln( simbolomodulo1 ); // error writefln( modulo1.simbolomodulo1 ); // correcto
Las palabras clave public y private antepuestas a un import significan:
Ejemplo:
---- inicio modulo2.d ----
import modulo1; // private import por defecto
int simbolomodulo2 = 20;
writefln( simbolomodulo1 );
---- fin modulo2.d ----
---- inicio modulo3 ----
import modulo2;
writefln( simbolomodulo2 ); // correcto, simbolomodulo2 esta definido en modulo 2
writefln( simbolomodulo1 ); // error, simbolomodulo1 está definido en módulo1,
// y como modulo3 no importa a modulo1 no puede
//acceder al módulo1 aunque modulo2 lo importe
//y modulo3 importe modulo2
Para que no diera error:
---- inicio modulo2.d ----
public import modulo1; // ahora exportamos todos los símbolos de modulo1
// como si fueran de modulo2
int simbolomodulo2 = 20;
---- fin modulo2.d ----
---- inicio modulo3 ----
import modulo2;
// correcto, simbolomodulo2 esta definido en modulo 2:
writefln( simbolomodulo2 );
// correcto porque modulo1 esta importado públicamente en modulo2:
writefln( simbolomodulo1 );
Los módulos pueden renombrarse cuando se importan usando el signo igual con el nuevo nombre a la izquierda y el nombre del módulo renombrado a la derecha:
import corto = mipaquete.directorio.nombreDeModuloMuyLargo
En lugar de importar todos los símbolos de un módulo, existe la posiblidad de importar sólo algunos símbolos específicos. Esto es útil cuando sólo vamos a usar uno o dos símbolos de un módulo concreto. La sintáxis para ello es poner el signo dos puntos con el nombre del módulo a la izquierda y la lista de símbolos que se importan del mismo a la derecha, separados por comas.
// Sólo importamos writef y writefln de std.stdio import std.stdio: writef, writefln
También podemos importar símbolos selectivamente al tiempo que los renombramos:
// Sólo importamos writef y writefln renombrando el último a escribelinea import std.stdio: writef, escribelinea = writefln
En ocasiones queremos poder acceder a un símbolo del propio módulo en el que está el código que lo usa, pero este puede haberse visto ocultado por un símbolo en el alcance local. En ese caso tan sólo tenemos que usar la notación "punto símbolo" (.símbolo) que seria equivalente a hacer nombredemodulo.símbolo desde otro módulo que importara el actual:
int simbolo;
void funcionconlocal(int simbolo) {
writefln( simbolo ); // escribe el parámetro simbolo
writefln( .simbolo ); // escribe el símbolo del módulo
En ocasiones el código se necesita una estructuración que vaya más allá de los módulos. En ese caso se puede hacer una jerarquía de módulos bajo un concepto mayor llamado "paquete". Para declarar que un módulo está en un paquete debemos incluir la ruta que debe tener el módulo dentro de la jerarquía del paquete, separando los distintos niveles de la misma y el nombre del módulo (al final) mediante puntos. Generalmente la jerarquía de los paquetes suele corresponderse con una organización de directorios y subdirectorios equivalente, aunque esto no es estrictamente necesario.
---- fichero conexiones.d, que está en el directorio mibiblioteca/red module mibiblioteca.red.conexiones //etc ---- fichero que está en el directorio mibiblioteca/red/servidores module mibiblioteca.red.servidores.http //etc
Cuando escribimos "import nombremodulo" el compilador primero buscará "nombremodulo.d" en el directorio actual. De no encontrarlo, si hemos proporcionado uno o más parámetros -Idirectorio al compilador "nombremodulo.d" se buscará en dichos directorios. Finalmente si tampoco se encuentra en ellos se buscará en el -Idirectorio que esté especificado en el fichero dmd.conf.
Cuando se trata de paquetes, si el paquete está organizado por directorios sólo es necesario especificar el nombre raíz donde se encuentra el paquete, no los subdirectorio. Por ejemplo si escribimos import mibib.red.conexion y el raíz del paquete mibib está en /usr/include/dmd/mibib (en el caso de un sistema Unix/Linux/Mac) basta con especificar como parámetro al compilador o en el dmd.conf -I/usr/include/dmd/mibib y el compilador buscará sólo el subdirectorio "red" y dentro del mismo el fichero "conexion.d".
Debido a la difusión del lenguaje, la mayoría de las APIs de sistemas operativos y librerías de sistemas están escritas en C u ofrecen una interfaz para el mismo. D puede acceder a bibliotecas de C. Para ello, además de enlazar con el fichero binario de la biblioteca mediante parámetros al compilador, debemos incluir en nuestro código D una declaración de los símbolos y funciones de C a los que queramos acceder. Como los tipos de datos de D suelen tener una correspondencia muy directa con los de C este proceso suele ser bastante sencillo; sin embargo para conversiones de ficheros de cabecera .h más complicados se dispone de la herramienta htod que realiza la conversión de tipos y sintáxis de forma automática, tomando como entrada un fichero .h de C y generando un fichero .d que podemos incluir en nuestros proyectos.
Para las declaraciones de símbolos y funciones de C, debemos poner la instrucción "extern(C):". Tomando un ejemplo de la descripción del htod, si en un fichero de cabecera .h tuviéramos el siguiente código C:
unsigned u; #define MYINT int void bar(int x, long y, long long z);
La conversión del mismo en D sería:
extern(C): uint u; alias int MYINT; void bar(int x, int y, long z);
Es importante destacar que dentro de un fichero fuente D no podemos incluir código C (al contrario de lo que sucede en C++.)
En la versión actúal de D no existe la posibilidad de enlazar contra bibliotecas C++.
En D existe un tipo de dato llamado "delegado" que puede usarse para pasar referencias a un método de una clase como parámetros para otras funciones y métodos. Son, en concepto, similares a los punteros a método de C++ pero con una sintaxis tanto de declaración como de creación y uso mucho más sencilla:
class Foo {
[...]
bool esPar(int N) { return (n % 2 == 0); }
bool esImpar(int N) { return !(n % 2 == 0); }
[...]
}
bool validadora( bool delegate(int) dlg, int valor ) {
return dlg(valor);
}
void main() {
Foo f = new Foo();
bool delegate(int) dlgespar;
bool delegate(int) dlgesimpar;
espar = f.esPar;
esimpar = f.esImpar
bool unoespar = validadora(espar, 1);
bool unoesimpar = validadora(esimpar, 1);
Como puede verse en el ejemplo anterior la declaración de una variable que apunte a un delegado es similar a la de una función pero poniendo "delegate" en lugar del nombre y el nombre de la variable apuntadora al final.
Hablando de funciones y métodos, D permite tener funciones anidadas y funciones anónimas. Las primeras son funciones que están definidas dentro de otra función. Son muy útiles para estructurar nuestro código de una forma más jerárquica.
Las funciones anónimas son funciones (normalmente sencillas) sin nombre que suelen utilizarse como argumento para una función que espera recibir una función como argumento.
En C y C++ para que una función pueda llamar a otra esta debe haber sido declarada con anterioridad a la misma. Para ello puede estar el cuerpo completo de la función, o sólo un "prototipo" que indica los tipos aceptados y el valor devuelto, por ejemplo esto no funcionaría:
void llamaotra(int prueba) {
funcionprimera( prueba ); // error, funcionprimera no está definida
}
void funcionprimera(int param) { return param; }
Por lo que tendríamos que declarar funcionprimera antes de implementarla, o directamente implementar "más arriba" de llamaotra.
El compilador de D es algo más inteligente y hace que esto sea innecesario; cualquier función puede llamar a cualquier otra que esté definida en el programa, independiéntemente de su posición en el código.
En C++, como en C, se utiliza un preprocesador que analiza el código realizando modificaciones e inclusiones sobre el mismo antes de que se inicie la compilación. El preprocesador de C/C++ es antiguo, y además algunos programadores parecen disfrutar escribiendo código enrevesado usando las instrucciones que el mismo proporciona, lo cual lleva a código bastante ilegible y dificil de seguir y depurar.
D elimina completamente el preprocesador y en lugar de esos añade una serie de directivas para el compilador pero incluidas en el lenguaje que nos permiten darle indicaciones sobre que código compilar según unos parámetros determinables en tiempo de compilación.
Estas directivas son:
Con esta intrucción indicamos el uso desde nuestro programa de otros módulos en C (ver más adelante el apartado sobre módulos y paquetes para ver una descripción más completa) y por lo tanto sustituiría al #include de C/C++. Al contrario de lo que sucede con el #include no hay ningún problema en que varios módulos incluyan a un mismo símbolo a través de importaciones, por lo que el chapucero código siguiente, tan necesario en C/C++ para proyectos complicados que involucran muchas inclusiones, es innecesario en D:
#ifndef _TENEMOS_MODULO_ //código del módulo #define _TENEMOS_MODULO_ #endif //Nada
Otro uso muy común del preprocesador de C/C++ es la supresión o no de determinados bloques de código según parámetros pasados al compilador o incluidos en el entorno del mismo. Por ejemplo en C/C++ cuando queremos que un código se compile en Windows o Linux hacemos:
#ifdef linux
printf("estoy en linux!");
#ifdef windows
printf("estoy en windows!");
En este caso la definición "linux" o "windows" lo proporciona el compilador, pero igualmente se pueden suminitrar al mismo definiciones adicionales usando la linea de comandos del mismo.
En D para hacer esto mismo, pero con más elegancia, se usa la instrucción version:
version(linux){
printf("Estoy en linux!");
}
version(windows){
printf("Estoy en windows!");
}
Podemos definir los símbolos comprobables mediante la intrucción version usando parámetros al compilador (-version=shareware) o podemos generar nuevos símbolos dentro del código, siempre usando bloques evaluables en tiempo de compilación, como el incluido dentro de otro bloque version, usando asignaciones:
version(shareware) {
version = puedeleer;
}
version(comercial) {
version = puedeleer;
version = puedeescribir;
version = puedeimprimir;
}
version(puedeleer) {
//Implementación de la funcionalidad de lectura
}
version(puedeescribir) {
//Impplementación de la funcionalidad de escritura
}
version(puedeimprimir) {
//Implementacion de la funcionalidad de impresión
}
En el ejemplo anterior, al compilar especificaremos como parámetro al compilador "version=shareware" o "version=comercial" según la versión del programa que queramos que se genere.
La intrucción debug es muy similar a version pero en lugar de usarse para especificar características del entorno o funcionalidades se usa para incluir dentro del bloque código de depuración que nos ayude a detectar y corregir errores en versiones en desarrollo. Sustituye a otro uso tradicional del preprocesador en C/C++:
#define DEBUG //codigo normal #ifdef DEBUG //codigo de depuracion #endif
En D el código equivalente sería:
debug = si
//código normal
debug(si) {
//código de depuración
}
La intrucción debug además de aceptar como parámetro un posible identificador, acepta valores enteros. En ese caso se compilarán todos los bloques debug cuyo parámetro sea igual o menor al valor entero. Por ejemplo:
debug = 2
// código normal
debug(1) {
// código de depuración de nivel 1
}
debug(2) {
// código de depuración de nivel 2
}
debug(3) {
// código de depuración de nivel 3
}
En el ejemplo anterior, como debug vale 2, se ejecutarán los dos primeros bloques de depuración (los que tienen como parámetro 1 y 2), pero no se ejecutará el tercero. Esto es útil para incrementar escalar la salida de diagnóstico de nuestro programa en varios niveles, siendo las mayores las que más salida mostrarán.
El static if se usa para comprobar el valor de símbolos evaluables en tiempo de compilación, como constantes o alias. Es similar a version, pero permite una mayor flexibilidad al poder usarse en instanciaciones de templates.
const int bitsint = 16; static if (bitsint == 16) alias short INT; else static if (bitsint == 32) alias int INT;
El static assert funciona igual que el assert pero comprobado sólo valores evaluables en tiempo de compilación. En caso de que la condición evalue a un valor falso, el programa no se compilará.
Los arrays (o matrices, o vectores, como se quieran traducir) en C++ son exactamente los mismos que en C, es decir, un poco de azúcar sintáctico para un puntero a una memoria asignada. El azúcar sintáctico sin embargo no es mucho, y para realizar algunas operaciones básicas (como añadir o quitar elementos a un array o cambiar su tamaño) debemos hacer engorrosas llamadas para asignar e inicializar nueva memoria. La cosa empeora si pensamos que además las "cadenas" en C están implementadas como un array de caracteres sin ninguna propiedad especial.
C++ soluciona parte de estas limitaciones a través de su librería estándar de plantillas (STL) que proporciona plantillas de clase genéricas para diversas estructuras de datos con múltiples operaciones, además varias clases avanzadas para cadenas.
En D, además de existir una biblioteca de plantillas de clase avanzadas (la DTL, actualmente en desarrollo) los arrays cuentan con una serie de características que los hacen mucho más prácticos y manejables que los de C/C++:
En general todas estas características hacen que los arrays en D se manejen de la forma más intuitiva y no haya que estar recordando constantemente que su implementación es poco más que un puntero con longitud (que lo es.)
Una consecuencia directa de estos arrays mejorados es que las cadenas nativas, a pesar de seguir siendo arrays de caracteres, son ahora infinítamente más manejables.
D también cuenta con un tipo de dato derivado del array llamado array asociativo que consiste en una estructura de datos (nativa) que asocia una clave con un valor, de forma que después conociendo la clave podamos recuperar el valor. Tanto la clave como el valor pueden ser de cualquier tipo y la forma general de declarar un array asociativo es muy sencilla:
tipoValor[tipoClave];
Por ejemplo para declarar un array asociativo en el que las claves fueran enteros y los valores objetos de tipo "MiClase":
MiClave[int];
O uno con claves de tipo cadena y valores de tipo int:
int[ char[] ];
Quien tenga algo de experiencia programando seguramente reconocerá la utilidad de este tipo de estructuras de datos, probablemente uno de los más utilizados en programación (con permiso de las listas/vectores), por ello casi todos los lenguajes de programación ofrecen al programador la posibilidad de usar este tipo. Java, C# y C++ no son una excepción, pero a diferencia de D en estos lenguajes el tipo de array asociativo (y sus variantes) están implementados como clases en la biblioteca estándar por lo que por un lado el rendimiento nunca va a ser el mismo que el de un tipo nativo (optimizable hasta la muerte por el compilador, que sabe manejarlo mejor que una clase) ni por otro lado será igual de cómodo, aunque esto sea menos importante (en el caso de estos lenguajes tendremos que declarar un objeto, construirlo con new y después utilizar métodos para acceder a sus elementos. C++ usando sobrecarga de operadores permitirá usar las instancias con notación de array, pero la declaración y construcción deberán seguir realizándose como objetos de clase; objetos de clase de plantilla, para ser exactos.)
Aparte de estos tipos nativos (arrays y arrays asociativos) D también contará con una librería de estructuras de datos genéricas que proporcionará estructuras de datos más avanzadas llamada la "D template library" en intenso desarrollo en el momento de escribir este artículo.
Una de las operaciones más comunes con arrays es recorrerlos mediante un bucle for como en el siguiente ejemplo:
for (int i = 0;i < 10; ++i) {
printf("Array[%d]: %d", i, miarray[i]);
}
D añade una palabra clave (heredada de otros lenguajes) que nos permite recorrer los arrays (u otros contenedores) obteniendo en cada iteración el siguiente elemento en lugar de un índice que luego tendríamos que usar para direccionar cada elemento dentro del bucle. Esto elimina, en la mayor parte de los casos, la necesidad de tener que usar incómodos iteradores, compárese por ejemplo:
TipoContenedor::iterator sl;
for( sl = instanciaCont->begin(); sl != instanciaCont->end(); ++sl)
{
(*sl)->imprimirValor();
}
o...
TipoContenedorIterator it(*instaciaCont);
TipoDato *p;
while( (p = it.current()) != 0)
{
++it;
p->imprimirValor();
}
Con la construcción equivalente en D usando foreach:
foreach(TipoDato t; instanciaCont)
{
t.imprimirValor();
}
En D, por supuesto, también pueden usarse iteradores para formas menos comunes de recorrer los bucles permitiendo toda la potencia de los iteradores "tradicionales" pero rara vez son necesarios porque habitualmente las propias implementaciones de los contenedores permiten recorrerlos de distintas formas:
// Recorrer invertido:
foreach(TipoDato t; instanciaCont.reverse())
{
t.imprimirValor();
}
// Recorrer invertido los valores dobles y pares:
foreach(TipoDato t;
instanciacont.filter(pares).transform(doblar).reverse())
{
t.imprimirValor();
}
En el último ejemplo hemos utilizado dos métodos de las instancias de el tipo de dato que estábamos utilizando que aceptan un argumento función o delegado (método) para realizar, en el primer caso una selección de los elementos pares únicamente y el segundo dobla los elementos que queden después de la primera transformación. Como puede verse esto no hace nada que no pueda hacerse con iteradores, pero la sintaxis es mucho más clara y concisa (de nuevo, si realmente necesitamos de iteradores también dispondremos de ellos en D.)
Los métodos mostrados en los últimos ejemplos están implementados en las estructuras de datos de la librería DTL que probablemente formará parte en el futuro, como librería de estructuras de datos, de la librería estándar de D.
En las últimas versiones de desarrollo se añadió una palabra clave nueva llamada foreach_reverse que recorre un contenedor del final al inicio por lo que la llamada a reverse() del ejemplo anterior no sería realmente necesaria si usáramemos foreach_reverse en lugar de foreach en este caso.
En la versión 1.0 de D el mecanismo de RTTI es sencillo, los objetos disponen de una propiedad "classinfo" la cual tiene las siguientes propiedades, que se pueden ver en el object.d (en general es aconsejable ver ese fichero fuente para ver las capacidades por defecto del objeto raíz de D):
Los lenguajes interpretados cuentan con la ventaja de que es muy fácil escribir unas líneas de código en un editor y ejecutar el intérprete sobre el mismo, o inclúir en los sistemas Unix/Linux/Mac como primeras líneas el nombre del intérprete para permitir su ejecución como si de un ejecutable se tratase.
Esta segunda posibilidad está incorporada en D mediante el parámetro al compilador "-run" que compila, enlaza y ejecuta el código que se le pase, sin generar ningún ejecutable en el disco. Ello nos permite crear scripts rápidos que se ejecuten como si de un lenguaje interpretado se tratase simplemente teniendo como primeras líneas:
#!/usr/bin/dmd -run [Resto de código D]
El compilador de D de Digital Mars es además sorprendentemente rápido, por lo que incluso ejecutándose de esta forma el resultado en ocasiones puede ser más rápido que usando un lenguaje de script real (como Python o Perl.)
La versión actual de D no soporta reflexión dentro del lenguaje, aunque algunas librerías de terceros lo están implementando fuera del mismo, por ejemplo Flectioned.
Nota: este artículo se irá ampliando con secciones sobre:
Sobrecarga de operadores
Propiedades
Nueva sintaxis para genérica
Programación por contratos (unittest, in{}, out{} y demás)
Mantenibilidad y fiabilidad
Signals y slots
Ampliar cadenas => writefln, readln, secuencias de escape, etc
Comentarios (de documentación y anidados)
http://www.dsource.org/projects/tutorials/wiki/TutorialFundamentals
más recursos: dsource, digitalmars.com/d, prowiki.org,
http://www.prowiki.org/wiki4d/wiki.cgi?D__Tutorial, h2d, dui, descent, dwt,
Phobos
Tango
Hilos (sincronizacion)
Tipo de herencia, métodos virtuales y mixins
Referencia contenido: http://en.wikibooks.org/wiki/A_Beginner%27s_Guide_to_D