D (linguaggio)

Da Wikipedia, l'enciclopedia libera.
Template-info.svg

Il linguaggio di programmazione D nasce nel dicembre 1999 dalla mente di Walter Bright.

È un linguaggio orientato agli oggetti, e potrebbe definirsi un'evoluzione del linguaggio C e C++. Fra le caratteristiche si annoverano: gestione più semplice delle classi e dei template rispetto al C++, un garbage collector come in Java, supporto a RTTI (Runtime type information), introduzione del tipo intero a 128 bit (non ancora utilizzabile), una gestione a moduli presa da Python al posto dei file header, chiamata alle API di Windows, chiamata alle funzioni scritte in C (usando la parola chiave extern), array associativi (oltre ai classici in stile puntatore, statici, dinamici) e molte altre cose.

Al momento è possibile utilizzarlo per i sistemi Windows, Linux x86 e PPC, Mac OS X, AIX e FreeBSD tramite un frontend del compilatore GCC chiamato GDC. Su Windows è spesso preferibile usare DMD.

Origine del nome e storia[modifica | modifica sorgente]

Il D è un linguaggio di programmazione ideato dalla DigitalMars, il suo obiettivo è raggiungere la potenza e le alte prestazioni dei linguaggi di basso livello come C e C++ ma con la grande produttività e semplice portabilità di linguaggi di alto livello come C#, Java, Perl, Python, Ruby e simili. In particolare la sintassi e le caratteristiche tecniche del compilatore riprendono C/C++, C# e Java. La versione più recente del compilatore è la 2.064 del 5º novembre 2013 (nello stesso giorno è stata rilasciata la release 1.076 intesa come manutenzione della versione 1 del linguaggio)

Caratteristiche del compilatore[modifica | modifica sorgente]

Il compilatore D è strutturato in modo diverso rispetto ai linguaggi principali, ereditando punti di forza creando un linguaggio estremamente originale. Tra le più importanti caratteristiche, ricordiamo

  • Veloce nella compilazione, più di quello Java e C++
  • Utilizzo della garbage collector simile a .Net e java, (ma generalmente molto meno efficiente)
  • Si basa su una libreria nativa detta Phobos, ricca di funzioni (supporta sockets, thread, zip, base64, funzioni linux, funzioni windows, md5), inoltre supporta la libreria C direttamente. Spesso oggi molti progetti utilizzano una libreria esterna detta Tango, creata e mantenuta dalla comunità degli utilizzatori di D.
  • Il compilatore riconosce l'HTML presente nel codice sorgente ignorandolo e permettendo comunque la compilazione (molto utile per documentare un programma, realizzando una forma di literate programming)
  • Nonostante le somiglianze con C# e Java non necessita di installare separatamente degli interpreti, tutto il necessario sta nel binario senza che assuma dimensioni spropositate.
  • Il codice supporta i codici stile sh, quindi i file sorgenti possono cominciare su unix con #! /usr/bin/dmd -run per esempio

Alcune caratteristiche[modifica | modifica sorgente]

Nested Functions[modifica | modifica sorgente]

Come in Pascal una funzione può contenere a sua volta le dichiarazioni di altre funzioni, rispettando comunque le regole generali dello scope per il loro uso. Un esempio:

 void foo() {
   void A() {
     B();   // ok
     C();   // error, C undefined
   }
   void B() {
       void C() {
           void D() {
               A();      // ok
               B();      // ok
               C();      // ok
               D();      // ok
           }
       }
   }
   A(); // ok
   B(); // ok
   C(); // error, C undefined
 }

La funzione più interna ha accesso a tutte quelle esterne, ma una funzione esterna non può accedere alle funzioni interne di una sua funzione interna. Anche l'ordine delle funzioni è fondamentale. Un altro esempio:

 void test() {
    void foo() { bar(); }    // error, bar not defined
    void bar() { foo(); }    // ok
 }

Si può notare come la reciproca chiamata di funzione non è possibile, in quanto nella compilazione di foo() la funzione bar non è stata ancora definita. La soluzione in D è usare i delegate (che sono differenti dai delegate del C#):

 void test()
 {
    void delegate() fp;
    void foo() { fp(); }
    void bar() { foo(); }
    fp = &bar;
 }

Un'altra caratteristica è che una funzione annidata può accedere ai campi di quella esterna, se la funzione è static anche il campo deve essere static.

Nella versione sperimentale 2.07, sono state introdotte delle vere closure nel linguaggio.

Function literals[modifica | modifica sorgente]

Questo permette di creare un puntatore a funzione o un delegato direttamente in un'espressione, quindi invece di fare così:

 int function(char c) fp;    // dichiara un puntatore a funzione
 
 void test() {
    static int foo(char c) { return 6; }
 
    fp = &foo;
 }

si può fare così:

 int function(char c) fp;
 
 void test() {
    fp = function int(char c) { return 6; };
 }

Questo vale anche per i delegate, quindi si può utilizzare questo codice:

 double test() {
    double d = 7.6;
    float f = 2.3;
 
    void loop(int k, int j, void delegate() statement) {
    for (int i = k; i < j; i++)
        statement();
    }
 
    loop(5, 100, delegate { d += 1; } );
    loop(3, 10,  delegate { f += 1; } );
 
    return d + f;
 }

Array ridimensionabili[modifica | modifica sorgente]

Gli array sono implementati con nuove funzionalità rispetto a quelli in stile C e C++ (in C++ sono comunque disponibili funzionalità analoghe utilizzando i container della STL). Tramite il campo length è possibile accedere alla dimensione di un array e ridimensionarlo, senza ricorrere all'allocazione dinamica come new, malloc e realloc.

 int[] array;
 while (1) {
    c = getinput();
    if (!c)
       break;
    array.length = array.length + 1;
    array[array.length - 1] = c;
 }

Slicing[modifica | modifica sorgente]

Permette di creare un nuovo riferimento ad un subarray. La sua sintassi è:

 int[10] a;    // dichiara array di 10 int
 int[] b;
 
 b = a[1..3];  // a[1..3] e' un array di 2 elementi, a[1] e a[2]
 foo(b[1]);    // equivalente a foo(0)
 a[2] = 3;
 foo(b[1]);    // equivalente a foo(3)

Array associativi[modifica | modifica sorgente]

Detti anche mappe o dizionari in altri linguaggi, permettono di associare il valore di un elemento di un array ad un altro tipo di dato o oggetto, quindi potremo avere:

 int[char[]] b;    // associative array b of ints that are
                   // indexed by an array of characters.
                   // The KeyType is char[]
 b["hello"] = 3;   // set value associated with key "hello" to 3
 func(b["hello"]); // pass 3 as parameter to func()
 ...
 int* p;
 p = ("hello" in b);
 if (p != null)
 printf(*p);
 b.remove("hello");
 ...

Strong typedefs[modifica | modifica sorgente]

Consideriamo il seguente alias:

alias int myint;

È possibile dichiarare una funzione che accetta int come argomento e poi passargli un myint senza che il compilatore generi un errore: ossia il compilatore è in grado di risolvere i riferimenti alias in tipi di dato fondamentali.

Se invece si vuole che se una funzione accetti solo e sempre un int, e che quindi i typedef debbano essere considerati come tipi di dati diversi, si può utilizzare, al posto di aliasl'istruzione typedefs. Ad esempio:

typedef int myint;

Farà in modo che utilizzando myint in una funzione che accetta un int dia un errore di compilazione. È anche possibile creare dei typedef con valori visualizzati costanti:

typedef int myint = 9718;
myint i; //inizializzato a 9718

Mixin[modifica | modifica sorgente]

Un mixin permette di ottenere un set di dichiarazioni da un template specificato e conservarle da un'altra parte.

 template Foo() {
    int x = 5;
 }
 
 mixin Foo;
 
 struct Bar {
    mixin Foo;
 }
 
 void test() {
    printf("x = %d\n", x);        // prints 5
    {   Bar b;
    int x = 3;
 
    printf("b.x = %d\n", b.x);    // prints 5
    printf("x = %d\n", x);        // prints 3
    {
        mixin Foo;
        printf("x = %d\n", x);    // prints 5
        x = 4;
        printf("x = %d\n", x);    // prints 4
    }
    printf("x = %d\n", x);        // prints 3
    }
    printf("x = %d\n", x);        // prints 5
 }

Questo vale anche per le funzioni:

 template Foo() {
    void func() { printf("Foo.func()\n"); }
 }
 
 class Bar {
    mixin Foo;
 }
 
 class Code : Bar {
    void func() { printf("Code.func()\n"); }
 }
 
 void test() {
    Bar b = new Bar();
    b.func();        // calls Foo.func()
 
    b = new Code();
    b.func();        // calls Code.func()
 }

posso specificare il tipo del template

 template Foo(T) {
    T x = 5;
 }
 
 mixin Foo!(int);        // create x of type int

ed ecco un ultimo esempio:

 template duffs_device(alias id1, alias id2, alias s) {
    void duff_loop() {
    if (id1 < id2) {
        typeof(id1) n = (id2 - id1 + 7) / 8;
        switch ((id2 - id1) % 8)
        {
        case 0:        do {  s();
        case 7:              s();
        case 6:              s();
        case 5:              s();
        case 4:              s();
        case 3:              s();
        case 2:              s();
        case 1:              s();
                  } while (--n > 0);
        }
    }
    }
 }
 
 void foo() { printf("foo\n"); }
 
 void test() {
    int i = 1;
    int j = 11;
 
    mixin duffs_device!(i, j, delegate { foo(); } );
    duff_loop();    // executes foo() 10 times
 }

Static If[modifica | modifica sorgente]

Lo static if è stato progettato per sostituire le direttive preprocessore. Questa istruzione valuta un'espressione a tempo di compilazione, senza creare un nuovo scope. Un esempio:

 const int i = 3;
 int j = 4;
 
 static if (i == 3)    // ok, at module scope
    int x;
 
 class C {
    const int k = 5;
    static if (i == 3)    // ok
    int x;
    else
    long x;
 
    static if (j == 3)    // error, j is not a constant
    int y;
 
    static if (k == 5)    // ok, k is in current scope
    int z;
 }
 
 template INT(int i) {
    static if (i == 32)
    alias int INT;
    else static if (i == 16)
    alias short INT;
    else
    static assert(0);    // not supported
 }
 
 INT!(32) a;    // a is an int
 INT!(16) b;    // b is a short
 INT!(17) c;    // error, static assert trips

Di conseguenza, esiste anche un'assert statico. In D esistono modi per eseguire semplici funzioni a tempo di compilazione, usando la normale sintassi, senza ricorrere a template come in C++.

Identity Expressions[modifica | modifica sorgente]

Permette di controllare le identità di due oggetti con l'operatore is o !is (che è not is). Quindi controlla, ad esempio per gli oggetti, se sono la stessa istanza, per gli array se contengono gli stessi elementi, ecc. Viene utilizzato prevalentemente per il controllo di omogeneità tra 2 oggetti.

Implicit Type Inference[modifica | modifica sorgente]

Usando auto può essere reso implicito, in un'assegnazione, un tipo di variabile.

 static x = 3;      // x is type int
 auto y = 4u;       // y is type uint
 auto s = "string"; // s is type char[6]
 
 class C { ... }
 
 auto c = new C();  // c is a handle to an instance of class C

Contract Programming[modifica | modifica sorgente]

La programmazione per contratto è ottima per evitare bug. Viene controllata una condizione in entrata alla funzione e in uscita, quindi si controlla un requisito per eseguire il corpo, e si ritorna il risultato solo se la condizione è vera. È anche presente nel linguaggio Eiffel

 long square_root(long x)
    in {
        assert(x >= 0);
    }
    out (result) {
        assert((result * result) == x);
    }
    body {
        return math.sqrt(x);
    }

Esiste anche la possibilità di definire invarianti di classe, che vengono controllati all'esterno dei metodi. Cioe' delle asserzioni sugli attributi che devono essere sempre verificate (ma questo tipo di invarianti di classe non vengono ancora gestiti al meglio in presenza di eredità).

Unit test[modifica | modifica sorgente]

Sono utili per assicurare il funzionamento del codice. Infatti evita, quando usata per testare classi, di dover creare un file sorgente con in main(), istanziare la classe, utilizzarla... È possibile, in D, procedere in questo modo:

 class Sum {
    int add(int x, int y) { return x + y; }
 
    unittest {
        Sum sum = new Sum;
        assert(sum.add(3,4) == 7);
        assert(sum.add(-2,0) == -2);
    }
 }
 
 void main() {
 }

Per eseguire tali unit test si deve fornire al compilatore il parametro -unittest

Scope Statement[modifica | modifica sorgente]

Serve per eseguire una funzione all'uscita di uno scope (indipendentemente dal fatto che ci siano o meno errori), all'uscita con errori, e all'uscita senza errori, ecco un esempio (da notare che vengono eseguiti all'inverso, dal basso verso l'alto):

Un esempio semplice è questo:

 writef("1");
 {
    writef("2");
    scope(exit) writef("3");
    scope(exit) writef("4");
    writef("5");
 }
 writefln();

che stampa 4321

Questo è un esempio un po' più complesso:

 class Foo {
    this() { writef("0"); }
    ~this() { writef("distrutto"); }
 }
 
 try {
    scope(exit) writef("2");
    scope(success) writef("3");
    auto Foo f = new Foo();
    scope(failure) writef("4");
    throw new Exception("msg");
    scope(exit) writef("5");
    scope(success) writef("6");
    scope(failure) writef("7");
 }
 catch (Exception e) {
 }
 writefln();

che stampa 0412

Struct member alignment control[modifica | modifica sorgente]

Permette di specificare l'allineamento dei membri di una struttura, con una sintassi simile a questa:

 align (1) struct S {
    byte a;    // placed at offset 0
    byte[3] filler1;
    byte b;    // placed at offset 4
    byte[3] filler2;
 }

Collegamenti esterni[modifica | modifica sorgente]