Zmienne i stałe

edytuj

Komputery operują na danych umieszczonych w pamięci. Pamięć jest podzielona na wiele komórek o stałym rozmiarze (bajtów) do których można odwoływać się poprzez adres, numer komórki.

Zmienne są fragmentem pamięci komputera o określonym rozmiarze do którego możemy się łatwo odwoływać poprzez nazwę, inaczej identyfikator.

Identyfikatory

edytuj

Identyfikatory w języku D buduję się z liter, cyfr oraz znaku podkreślenia. Pierwszy znak identyfikatora nie może być cyfrą. W D można używać znaków narodowych dla identyfikatorów, jednak jeśli kod będzie edytowany przez ludzi z różnych krajów nie jest to najlepszy pomysł. (Znaki narodowe mogą się przydać natomiast przy nauce języka, u młodych uczniów).

Przykłady poprawnych identyfikatorów:

v
x3
prędkość
sc_d2
p55
V                  // UWAGA: patrz opis poniżej
_syscall           //          j.w.

nie są poprawne następujące identyfikatory:

4ped               // BŁĄD: zaczyna się od cyfry
ilosc.znakow       // BŁĄD: zawiera kropkę

Język D rozróżnia małe i duże litery (ang. case sensitive) w identyfikatorach, tak więc identyfikator v jest inny niż V. Należy również zaznaczyć, iż identyfikatorów zaczynających się od podkreślenia nie powinno się używać w programach, ponieważ są one używane przez biblioteki oraz kompilator do specjalnych celów, co może powodować konflikty.

Przy wybieraniu identyfikatora (nazwy) staraj się aby była ona samowyjaśniająca się, tzn. odpowiadała temu czemu służy i nie wprowadzała w błąd. Dobrze aby nie była zbyt długa (lepiej opisać jej znaczenie w komentarzu), ani zbyt krótka (jako trudny do odszyfrowania skrót). Zwykle dobrymi nazwami są pojedyncze litery jako nazwy zmiennych (o ile nie używa się ich w dużej ilość w jednym miejscu), w szczególności są pewne konwencje co do pojedynczych liter. Dobre są również pojedyncze słowa. Przykłady

a, c, d   // parametry
x, y, z   // szukana niewiadoma
i, j, k   // indeks pętli
tekst     // tekst
wyświetl  // funkcja wyświetlająca
xgg1      // NIEDOBRZE: co to jest?
f         // NIEDOBRZE: funkcja, która nie wiadomo co robi,
          //            chyba że to jakaś ogólna funkcja
zapisz    // jeśli zapisz jest funkcją, która wykonuje coś innego
          // niż zapisywanie, NIE JEST TO DOBRA nazwa


Część nazw jest zarezerwowana jako słowa kluczowa i nie można ich używać jako identyfikatorów. Słów kluczowych jest kilkadziesiąt i poznamy wszystkie w toku całego podręcznika, jednak juz tutaj podamy ich część (bez wyjaśnienia) ponieważ może Ci przyjść pomysł użycia ich jako identyfikatorów, a nie jest to dozwolone (o czym zapewne poinformowałby kompilator):

 in, out, body, double, do, this, version, super,
 new, function, default, debug, alias, auto

Deklarowanie zmiennych

edytuj

Zmienne w języku D deklaruje się używając następującej składni:

 typ nazwa_zmiennej;

jeśli chcemy zaś zadeklarować naraz więcej zmiennych tego samego typu można to zrobić wypisując zmienne po przecinku:

 typ zmienna1, zmienna2, zmienna3;

Zmienne w języku D mogą być deklarowane praktycznie w dowolnym miejscu programu, jednak muszą się one pojawić przed użyciem (wyjątkiem są zmienne globalne).

Aby wyjaśnić lepiej mechanizm działania zmiennych skupimy się na typie int (od ang. integer - liczba całkowita), jest to typ liczb całkowitych, a zmienna zadeklarowana z takim typem może zmieścić dowolną liczbę w zakresie od ok. -2 mld. do +2 mld. Typ ten zajmuje 4 bajty (32 bity) pamięci. Za chwilę poznamy dokładnie wszystkie wbudowane typy oraz ich zastosowania.

 int x;

Definiuje to zmienną o nazwie x oraz typie int. Zmienna ta zostanie zainicjalizowana liczbą 0 (jest to wartość stałej int.init). Możemy również zainicjalizować tą liczbą inną wartością, np. 100:

 int x = 100;

albo kazać kompilatorowi jej nie inicjalizować:

 int x = void;         // UWAGA: zmienna x może wtedy zawierać śmieci i mieć przypadkową wartość

w języku D wartość którą inicjalizujemy może być pewnym wyrażeniem (np. wyrażeniem matematycznym), na przykład:

 int x = 24*60*60;     // prawidłowo zainicjalizuje się jako 86400

Inicjalizację można przeprowadzić używając dowolnych wyrażeń o ile jest to wykonywane w bloku funkcyjnym, na przykład:

 int s = 6;
 int x = 2*s;

Inicializacja zmiennych globalnych jest też możliwa w podobny sposób, ale zwykle musi być wykonywalna na etapie kompilacji.

Jak już widzisz, zmiennych typu int używa się jak zmiennych w matematyce. Można na nich wykonywać podstawowe operacje matematyczne, oraz podstawiać wyniki tych operacji do innych (albo tych samych!) zmiennych. Na przykład, nasz pierwszy program używający zmiennych może wyglądać tak:

 import std.stdio;
 void main() {
  int a = 6;          // utworzenie zmiennej o nazwie a i wartości początkowej 6
  int b = 7;          // analogicznie o nazwie b i wartości 7
  int c;              // utworzenie zmiennej c i inicjalizacja przy pomocy 0
  c = a+b;            // przypisanie zmiennej c wartości będącej sumą zmiennej a i b
  writefln(c);        // wyświetlenie wyniku
 }

Przykład powyżej można napisać na kilka różnych sposobów. Wypiszemy tutaj kilka wariantów, aby oswoić się ze składnią.

 import std.stdio;
 void main() {
   int a = 6, b = 7;   // utworzenie dwóch zmiennych: a, b
   int c = a+b;        // utworzenie zmiennej c z wartością początkową równą a+b
   writefln(c);
 }

albo po prostu:

 import std.stdio;
 void main() {
   int a = 6, b = 7;
   writefln(a+b);         // wynik dodawania nie zostanie zapisany do zmiennej
                          // tylko od razu przekazany funkcji writefln
 }

Stałe i niezmienniki

edytuj

Stałe

edytuj

Stałe to zmienne, których nie możemy w sposób jawny modyfikować. Mimo paradoksalnej nazwy, przyjeło się je nadal określać jako zmienne, ponieważ zachowują się one praktycznie tak jak zmienne z punktu widzenia języka. Używanie stałych ma dwojaki cel: ustrzega nas przed ewentualnym błędem, który byśmy popełnili zmieniając zawartość takiej zmiennej oraz pozwala lepiej i łatwiej zoptymalizować kod.

Stałe deklaruje się dopisując modyfikator (atrybut zmiennej) const:

 const int n = 10;

możemy teraz używać zmiennej n jak każdej innej zmiennej, jednak nie możemy jej zmienić:

 writefln(5*n); // wypisz 50
 n = 20;        // BŁĄD: kompilator się na to nie zgodzi!

Nawet próba zmiana zmiennej na tą samą wartość jest błędem:

 const int n = 10;
 n = 10;       // BŁĄD
 n = n;        // też BŁĄD

Ważne jest, aby stałą zainicjalizować w momencie deklaracji, ponieważ bardzo podobna konstrukcja jest nie poprawna:

 const int n;       // stwórz stałą
 n = 10;            // przypisz jej wartość 10

Kompilator zaprotestuje w obu linijkach. W pierwszej ponieważ nie zainicjalizowaliśmy stałej, a w drugiej ponieważ zmieniamy stałą (która mogła bybyć równa 0, gdyby inicjalizować ją podobnie jak zwykłe zmienne int).

Przykładem stałych mogą być np. pi, e (podstawa logarytmów naturalnych), wielkość zbioru danych na którym operuje program, czy jakieś parametry ustalone przez użytkownika, które w czasie wykonania programu się nie zmieniają.

Nazwę typu możemy pominąć, jeśli wyrażenie inicjalizujące po prawej stronie znaku przypisania jest znanego typu:

 const n = 10; // równoważne const int n = 10;

n zostanie stworzona jako typu int, ponieważ 10 ma typ int. Jest to tak zwane automatyczne wnioskowanie (ang. inference) typu. Więcej o tej cennej własności opowiemy na końcu tego rozdziału.

Stałe mogą być też inicjalizowane dynamicznie, co ma szczególnie zastosowanie w funkcjach (i metodach):

auto f(int a, int b) {
 const s = a + b;
 // s = 3;  // BŁĄD.
 return s * s;
}

Tutaj użyliśmy const jako tymczasowe miejsce do przechowania jakieś wartości w funkcji, której nie chcemy zmieniać do końca wywołania tej funkcji. Ułatwia to też często czytanie i analizowaniu kodu, ponieważ nawet przy długiej funkcji, wiemy od razu, że taka zmienna nie będzie zmieniała swojej wartości (nigdzie w funkcji nie ma instrukcji która by nadpisywała wartość tej funkcji).

Niezmienniki

edytuj

Niezmienniki są to dane, które nie mogą zostać w żaden sposób zmienione. Różnica pomiędzy stałymi a niezmiennikami jest taka, że stałe mogą zostać zmienione z innej referencji nieoznaczonej słowem const, a niezmienniki nie.

Do oznaczania danych jako niezmienniki służy słowo kluczowe immutable.

Domyślnie niezmiennikami są na przykład ciągi znaków. Ten fakt może z początku sprawić problem, ponieważ kompilacja poniższego kodu skończy się błędem:

 char[] x = "tekst";

Kompilator poinformuje nas wtedy o niemożności konwersji typu immutable(char[]) do char[]. W takim przypadku należy użyć własności tablic dup, która zwróci zwykłe dane (nie immutable):

  char[] x = "tekst".dup;

Inną opcją rozwiązania tego problemu jest użycie typu string zamiast char[].

 string x = "tekst";

Dzięki temu jeśli będziemy potrzebowali stałej, której na 100% nikt nie zmieni bo informuje o czymś elementarnym i poprawność tych danych ma wpływ na działanie całego programu, np. wbudowany w program klucz do szyfrowania/deszyfrowania danych (to tylko przykład - rzadko dobre rozwiązanie) będziemy mogli użyć niezmiennika i nie martwić się o te dane.

 immutable char[] klucz_128bit = "0123456789abcdef";

Dodatkowym udogodnieniem jest to, że definiując niezmiennik typu char[] nie musimy dodawać .dup na końcu ciągu.

Zasięg i czas życia

edytuj

Zmienne mogą się znajdować poza funkcjami, wtedy nazywam je zmiennymi globalnymi (poprawniej zmienne o zasięgu bieżącego modułu). Są dostępne ze wszystkich funkcji. Na przykład

 import std.stdio;
 
 void func() {     // tworzymy własną funkcję o nazwie func
   a = 5;          // zmieniającą zmienną o nazwie a ustawiając ją na 5
 }
 
 int a = 6;        // utworzenie zmiennej globalnej o nazwie a i wartości 6
 
 void main() {   // tu program rozpoczyna działanie
   writefln(a);    // wyświetli 6
   a = 7;          // zmieniamy wartość
   writefln(a);    // wyświetli 7
   func();         // wywołujemy naszą funkcję (która modyfikuje a)
   writefln(a);    // wyświetli 5
 }

Należy zauważyć, iż w funkcji func użyliśmy zmiennej a zanim w kodzie źródłowym pojawiła się informacja o tej zmiennej (ang. forward reference). W D jest to dozwolone. Tak samo, mogli byśmy napisać funkcje func za funkcją main (na samym dole; patrz następny przykład), i użyć identyfikatora func w funkcji main. Jest to znaczący postęp w stosunku do języka C w którym przed użyciem wszystkie identyfikatory muszą być znane.

Zmienne które definiuje się na "prywatne" potrzeby funkcji nazywamy zmiennymi lokalnymi i nie są dostępne z innych funkcji. A w przypadku gdy istnieje zmienna globalna o takiej samej nazwie zostanie ona "zasłonięta" (ang. shadowing).

 import std.stdio;
 
 int a = 5;       // zmienna globalnej
 
 void main() {
   int a = 6;    // zmienna lokalna (zasłania globalne a)
   writefln(a);  // wyświetli 6 (a nie 5)
   func();
   writefln(a);  // wyświetli 6 (ponieważ func modyfikuje zmienną globalną, a nie lokalną)
   a = 8;
   writefln(a);  // wyświetli 8
   func2();      // wyświetli 11
   writefln(.a); // wyświetli 11 (kropka służy do dostępu do zmiennych 
                 // globalnych w przypadku ich zasłonięcia)
 }
 
 void func() {
  a = 11;        // modyfikacja zmiennej globalnej (bo jedynie takie a "zna")
 }
 
 void func2() {
  writefln(a);  // wyświetli zmienną globalna
 }

Fragment kodu źródłowego w którym zmienna jest dostępna nazywamy zasięgiem (ang. scope) zmiennej. I tak np. zmienna a zdefiniowana w funkcji ma zasięg ograniczony do funkcji main. Natomiast zasięg zmiennej globalnej rozciąga się na cały plik (moduł).

Zasięg zwykle jest zdefiniowany przez strukturę blokową - zmienna jest dostępna w bloku w którym została zadeklarowana oraz w blokach wewnętrznych. W blokach wewnętrznych można zasłaniać zmienne spoza funkcji w której zostały zadeklarowane (np. zmienne globalne):

 import std.stdio;
 
 int d = 1;
 
 void main() {
  int a = 6;                  // zmienna lokalna
  int c = 7;                  // j.w.
 
  { // tworzymy nowy blok
    int b = 11;               // nowa zmienna lokalna, w bloku wewnętrznym
    //int c = 12;             // j.w. + zasłonięcie - BŁĄÐ: nie wolno
    int d = 13;               // j.w., ale zasłaniamy zmienną globalną (spoza funkcji main)
    writefln(a, " ", b, " ", d);      // wyświetli "6 11 13"
    a = 100;
    //c = 101;
    d = 102;
  } // koniec bloku. Tu zmienna b staje się już niedostępna (kończy się jej czas życia)
 
  writefln(a);                // wyświetli 100
  writefln(c);                // wyświetli 7         // (a nie 101!)
  writefln(d);                // wyświetli 1 (a nie 102!)
  //writefln(b);              // BŁĄD. zmienna b jest niedostępna
 }

Zmienne o ograniczonym zasięgu, można tworzyć również w wyrażeniach warunkowych oraz pętlach. Ich zasięg będzie ograniczony zawartością bloku warunkowego czy też treści pętli.

Typy zmiennych wbudowanych

edytuj

Język D posiada kilkanaście typów wbudowanych. Przedstawimy tutaj po kolei ich cechy i zastosowania.

Typ logiczny

edytuj

Typ bool jest typem przeznaczonym do przechowywania zmiennych boolowskich, są to zmienne logiczne o wartościach prawda (ang. true) lub fałsz (ang. false).

 bool czyZapisywac = true;
 bool blokada = false;

Zmienne typu bool są używane w instrukcjach warunkowych, oraz są wynikiem działania operatorów relacji oraz operatorów logicznych. Operatorów logicznych jest kilka:

 bool a, b;
 bool a_i_b = a && b;    // logiczny operator && - logiczne i (ang. and)
 bool a_lub_b = a || b;  // logiczny operator || - logiczne lub (ang. or)
 bool nie_a = ! a;       // jednoargumentowy operator ! - logiczne nie (ang. not)
 bool a_albo_b = a ^ b;  // logiczny (i bitowy) operator ^ - alternatywa wykluczająca (ang. xor - exclusive or)

Wartość odpowiedniego wyrażenia, będzie zgodna z znanymi z logiki regułami. a_i_b będzie miało wartość true, jeśli a i b, mają równocześnie wartość true, w przeciwnym razie będzie miało wartość false (jedna, lub obie wartości były false). a_lub_b będzie miało wartość true, jeśli a lub b (lub obie zmienne), ma wartość true, w przeciwnym wypadku będzie miało wartość false (obie zmienne miały wartość false). Natomiast, nie_a będzie mieć wartość true, jeśli a miało wartość false, w przeciwnym wypadku odwrotnie, nie_a będzie mieć wartość false, jeśli miało wartość true. a_albo_b będzie miało wartość true, jeśli a i b, mają różne wartości, tzn. true i false, albo false i true, w przeciwnym wypadku będzie mieć wartość false (oznaczające że obie zmienne miały taką samą wartość, tzn. true i true, albo false i false).

Na przykład:

 bool a = true;
 bool b = false;
 bool c = a && b;        // c będzie miało wartość false

Operatory and, or, not oraz xor, można również używać na zmiennych typów całkowitych, lecz mają one trochę inne znaczenie, oraz inny zapis.

Typy całkowite

edytuj

Do typów całkowitych (integer) zalicza się następujące typy:

typy rozmiar (typ.sizeof)
byte, ubyte zmienne o rozmiarze 1 bajta
short, ushort zmienne o rozmiarze 2 bajtów
int, uint zmienne o rozmiarze 4 bajtów
long, ulong zmienne o rozmiarze 8 bajtów
cent, ucent zmienne o rozmiarze 16 bajtów (zarezerwowane na przyszłość)

Czasami również zalicza się tutaj typy:

typy rozmiar (typ.sizeof)
char 1 bajt
wchar, dchar 2 oraz 4 bajty

jednak ponieważ służą one do przechowywania znaków, a nie liczb, zajmiemy się w następnym podpunkcie.

Jak widać ilość typów zmiennych jest bardzo duża, różnią się one wielkością oraz każdy typ ma typ o identycznym rozmiarze tylko z dopisanym u. Z racji różnic w wielkości zmiennych mogą one przechowywać wartości całkowite z różnych zakresów, oraz zajmować więcej lub mniej miejsca w pamięci komputera. Wybór typu zmiennej jest ważny z kilku powodów. Po pierwszej jeśli zmiennych danego typu będzie bardzo dużo, to najlepiej wybrać typ możliwie najmniejszych który spełni swoje zadania celem redukcji zużycia pamięci. Po drugie typy o mniejszym rozmiarze zwykle są szybsze (zarówno ich przesyłanie z pamięci do procesora jak i wykonywanie operacji na nich).

Kiedy już wybierzemy typ należy uważać, aby nie przekroczyć dozwolonego zakresu - nastąpi tzw. przekręcenie, na przykład dla zmiennej typu ubyte największa możliwa wartość zmiennej to 255, jeśli dodamy do zmiennej z wartością 200 inną zmienną z wartością 60 to kompilator zapewne nas ostrzeże ponieważ wynik 260 nie jest możliwy do zapisania w takiej zmiennej, a dla procesora będzie to naprawdę oznaczać liczbę 0 (początek zakresu). W przypadku int taki koniec jest o wiele większy jednak w takim wypadku kompilator nie ostrzeże nas o tym.

Zmienne standardowo są zmiennymi ze znakiem, i w przybliżeniu potrafią reprezentować tyle samo liczb ujemnych co dodatnich.

Zmienne z dopisanym "u" na początku są to zmienne bez znaku (ang. unsigned, ang. sign - znak). Zmienne takie mogą reprezentować dwa razy więcej liczb dodatnich niż zmienne bez znaku, ale równocześnie nie mogą reprezentować liczb ujemnych.

typ rozmiar (typ.sizeof) zakres (typ.min .. typ.max) typ.init
byte 1 bajt (8 bitów) -128..127 0
ubyte 1 bajt (8 bitów) 0..255 0
short 2 bajty (16 bitów) -32768..32767 0
ushort 2 bajty (16 bitów) 0..65535 0
int 4 bajty (32 bitów) -2147483648..2147483647 0
uint 4 bajty (32 bitów) 0..4294967295 0
long 8 bajtów (64 bitów) -9223372036854775808..9223372036854775807 0L
ulong 8 bajtów (64 bitów) 0..18446744073709551615 0L
cent 16 bajtów (128 bitów) -2^127..2^127-1 0
ucent 16 bajtów (128 bitów) 0 .. 2^128-1 0

W większości naszych programów, nie będziemy przykładać większej wagi do typów całkowitych, i dla wygody będziemy używać standardowo typu int, który jest bardzo uniwersalny.

Często w programie będziemy musieli użyć konkretnej wartości liczbowej, używamy do tego literałów.

Literały całkowite zapisujemy jako ciąg cyfr dziesiętnych, poprzedzonych opcjonalnym znakiem minusa (negacji). Literały domyślnie są typu int:

 int x = -817263;

Jeśli chcemy aby literał był typu long należy dopisać literę L na samym końcu:

 long y = 172387618726L;

Jeśli chcemy literał o typie bez znaku, należy dopisać dodatkowo literę u.

 ulong z = 172387618726Lu;

Literały można wprowadzać również w systemie dwójkowy (ang. binary), ósemkowym (ang. octal) oraz szesnastkowym (ang. hexadecimal). W systemie ósemkowym możemy wykorzystywać cyfry 0..7, a całą liczbę poprzedzić zerem, np.

 int trzynaście = 015          // 1*8 + 5 = 13

W sysetmie szesnastkowym możemy wykorzystywać cyfry 0..9, oraz litery a..f (lub A..F) jako cyfry 10..15, a całą liczbę należy poprzedzić dwoma znakami, mianowicie 0x (lub 0X).

 int pięćset = 0x01f4;         // 1*(16*16) + 15*16 + 4 = 256 + 4 + 240 = 500

W systemie dwójkowym możemy wykorzystywać jedynie cyfry 0 i 1, a liczbę należy poprzedzić dwoma znakami: 0b (lub 0B):

 ushort d = 0b1000011010100101

Przy wprowadzaniu długich literałów wygodnie może być rozdzielić kolejne tysiące, albo czwórki w systemie dwójkowym czy szesnastkowym. Służy temu znak podkreślenia, np.

 int x = -817_263;
 ulong y = 172_387_618_726L;
 ushort d = 0b_1000_0110_1010_0101
 uint kod = 0xA_D___E0__1522;

Dzięki temu można poprawić czytelność liczb, ale czasami również utrudnić, należy więc używać tej cechy z rozwagą.

Typy zmiennoprzecinkowe

edytuj

Poza liczbami całkowitymi wielokrotnie będziemy chcieli wykonywać obliczenia na liczbach rzeczywistych. Standardowo komputery używają tak zwanych liczb zmiennoprzecinkowych (ang. floating point numbers, floats), do przybliżenia liczb rzeczywistych. Są one przybliżone, i mogą reprezentować tylko pewien skończony podzbiór wszystkich liczb rzeczywistych (podobnie jak typy całkowite), ale w większości zastosowań, jest to przybliżenie wystarczające w praktycznych zastosowaniach.

Język D posiada kilka typów zmiennoprzecinkowych. W większości przypadków używa się typu double, ponieważ jest on dużo dokładniejszy niż float.

typy opis (wg IEEE 754 oraz specyfikacji D) rozmiar (typ.sizeof) bity mantysa cecha znak typ.init dokładność dziesiętna
float Liczba zmiennoprzecinkowa pojedynczej precyzji 4 32 23 8 1 float.nan ok. 7 cyfr znaczących
double Liczba zmiennoprzecinkowa podwójnej precyzji 8 64 52 11 1 double.nan ok. 16 cyfr znaczących
real Największa dostępna precyzja na danej architekturze (np. extended double) 10/12 80 ? ? ? real.nan około 20, gwarantowane około 16

W przeciwieństwie do liczb całkowitych, liczby zmienne zmiennoprzecinkowe (i zespolone) nie są inicjalizowane zerem, tylko wartością NaN (ang. Not a Number - nie liczba). Jest to wartość, która nie ma sensu. Ma ona tą cechę, że jeśli się użyje jej w jakiejkolwiek operacji, wynik będzie również miał wartość NaN (ponieważ przynajmniej jeden argument był NaN) - mówimy, że NaN się propaguje. Wartości NaN powstaje czasami kiedy obliczamy wyrażenia nie zawierające NaN. Na przykład 0.0/0.0, daje w wyniku NaN, ponieważ wyrażenie to matematycznie nie jest określone, podobnie obliczenie pierwiastka z liczby ujemnej, czy obliczenie funkcji odwrotnej do sinusa, dla argumentu większego niż 1. Z powodu propagacji NaN-ów, wystarczy jeden taki błąd w trakcie obliczeń, a i tak pojawi się on zapewne w wynikach. Tak więc obecność NaN w wynikach świadczy albo o błędach w algorytmie, nie uwzględnianiu problemów związanych ze skończoną dokładnością, albo brakiem inicjalizacji zmiennych. W przypadku zmiennych rzeczywistych, język D zakłada, że lepiej pozostawić to programiście.

 float a;        // a ma wartość NaN

Literały zmiennoprzecinkowe można zapisywać na kilka sposobów:

 double a = 1.0;      // normalnie: 1.0
 double b = 1.;       //            1.0
 double c = .5;       //            0.5
 double d = 1.0e3;    // styl naukowy: 1.0 * 10^3 = 1000.0
 double e = -5.1e-5;  //               -0.000051
 double f = 5e11;     //               dużo

Podobnie jak literały całkowite, literały zmiennoprzecinkowe mogą używać znaku _ do rozdzielania różnych części:

 double f = 1_000_000_000_000.0;   // 1 bilion, 1.0e12;

Standardowo literały są typu double. Jeśli chcemy aby literał był typu float należy dopisać małą lub dużą literę f na końcu, a aby był typu real to należy dopisać dużą literę L.

 float z = 1.0f;
 real x = 1.213172387162837162876178362L;

Literały rzeczywiste mogą być wprowadzane w systemie szesnastkowym, podobnie jak literały całkowite. Używa się do tego na początku 0x, a do wpisania eksponenty używa się małej lub dużej litery p (ponieważ litera e należy do zbioru liczb szesnastkowych).

 double p = 0x1.07F1EEA12ADA6p512; // wykładnik jest dziesiętny!

Jeśli chcemy użyć wartości NaN bezpośrednio, bez przypisywania tworzenia nowej zmiennej, możemy użyć atrybutu nan każdego z powyższych typów, np

  import std.math : sin;
  
  /* Reszta kodu */

  float y = sin(float.nan);

Zmienna y, będzie zawierać też wartość float.nan, ponieważ funkcja sin propaguje tą wartość.

Typy urojone i zespolone

edytuj
 ifloat, idouble, ireal       // urojone (ang. imaginary)
 cfloat, cdouble, creal       // zespolone (ang. complex)

D zawiera wbudowane w język zmienne urojone i zespolone. Jeśli nie wiesz czym są liczby urojone i zespolone, możesz zignorować ten podpunkt, ale zachęcamy do poczytania o liczbach zespolonych na Wikipedii, lub podręczniku do matematyki.

Typy urojone mają analogiczną dokładność, jak typy rzeczywiste, ale reprezentują liczby na osi urojone, a nie rzeczywistej. Typy zespolone to para zmiennej rzeczywistej i urojonej o danych precyzjach, a więc zajmują dwa razy więcej miejsca w pamięci. W przypadku zmiennych zespolonych, dostęp do części rzeczywistej i urojonej odbywa się przez dopisanie, .re lub .im na końcu.

Literały urojone powstają podobnie jak literały rzeczywiste, z tym że na samym końcu dopisujemy literę i. Literały zespolone to suma literału rzeczywistego oraz urojonego rozdzielone znakiem + albo -. Na przykład:

 ifloat a = 3.1i;
 cfloat b = 1.0 - 2.0i;

Przykłady prostych obliczeń:

 float a = 4.2;
 ifloat b = 2.0i;
 ifloat c = a*b;        // 8.2i
 cfloat d = 2.0 + 4.0i;
 cfloat e = d*c;        // -32.8 + 16.4i
 float f = e.re;        // -32.8
 float g = e.im;        // 16.4;

Podobnie jak liczby zmiennoprzecinkowe zmienne urojone są inicjalizowane wartością NaN (np. ifloat.init = float.init * 1.0fi), a zmienne zespolone otrzymują taką wartość dla obu części (np. cdouble.init = double.init + double.init * 1.0i).

 ifloat a;          // Re(a) ma wartość NaN
 cfloat b;          // b.re i b.im mają wartość NaN

Typ void

edytuj

Typ void (ang. pustka), to tak naprawdę brak typu.

Używa się go na przykład do wskazania, że funkcja nie zwraca żadnych rezultatów (czyli jest formalnie procedurą), albo jako void*, może służyć jako wskaźnik do bliżej nie określonych danych. Użycie void* nie jest zalecane. Więcej o wskaźnikach w kolejnych rozdziałach.

 void odswiez() {            // procedura, która coś robi,
                             // lecz nie zwraca wyników;
    ...                      // więcej o funkcjach w odpowiednim rozdziale
 }

 void* s = VideoMemmoryAddress();    // wskaźnik na "coś" w pamięci -
                                     // dosyć niebezpieczna konstrukcja!

Typu void można również użyć do wykonania operacji arytmetycznych bez zapisywania jej do jakiejkolwiek zmiennej poprzez rzutowanie, na przykład:

 1+2;             // BŁĄD: 1+2 powinno zostać zapisane do jakiejś zmiennej
                  //       lub przekazane do funkcji
 cast(void)(1+2); // OK

Więcej o rzutowaniach (bardziej sensownych) na końcu rozdziału.

Modyfikatory

edytuj

Innymi ważnymi modyfikatorami stałych poza const są modyfikatory volatile, static, auto oraz extern.

Modyfikator auto służy do automatycznego wnioskowania typu, na przykład:

 int a = 5;
 auto x = a+4;   // tworzy zmienną typu int, ponieważ a+5 (int+int) jest typu int
auto y = x/2.0; // tworzy zmienną typu double ponieważ x/2.0 (int/double) jest typu double

modyfikator ten niezmiernie się przydaje kiedy wyrażenie po prawej stronie jest skomplikowanego typu (w typach czasami można się pogubić), i kłopotliwe było by ich jawne wypisywanie (również ponieważ ewentualna modyfikacja typów zmusza do zmiany wszystkich ich wystąpienia oraz wiedzy jaki to ma być typ). Modyfikator ten w żaden sposób nie wpływa na szybkość wykonywania kodu, ponieważ typy są wnioskowane w czasie kompilacji (D jest językiem statycznie typowanym)

scoped

edytuj

Niegdyś w D istniał modyfikator scope, na dzień dzisiejszy jego zadanie przejął szablon std.typecons.scoped, dalej scoped.

Ponieważ w języku D obsługa pamięci jest zazwyczaj automatyczna i jest wykonywana przez tzw. garbage collector (ang. odśmiecacz pamięci), to po wyjściu z zasięgu zmienna jest "zapominana" przez program, lecz nadal istnieje (nie jest już dostępna). Garbage collector cyklicznie sprawdza pamięć w poszukiwaniu takich zmiennych i je usuwa. Czasami jednak chcemy aby zmienna została zniszczona w momencie wyjścia z zasięgu w sposób automatyczny (zmienne te są tworzone zwykle na stosie), a w przypadku obiektów został wywołany natychmiast destruktor - a nie jak w przypadku garbage collectora, w bliżej nie określonej przyszłości. Służy temu właśnie szablon scoped. Przykład:

 import std.typecons;
 // ...
 void moja_funkcja() {
   auto x = new scoped!X(); // utworzenie zmiennej x będąc obiektem typu X
   // ...
 } // zniszczenie zmiennej x i wywołanie destruktora

przy braku szablonu scoped zmienna stała by się niedostępna, jednak nadal by istniała w pamięci, a jej destruktor nie został by wywołany.

static

edytuj

Modyfikator static oznacza wiele różnych rzeczy w zależności od kontekstu, i co innego, niż w językach C czy C++.

Tutaj tylko napiszemy, że w kontekście zmiennej wewnątrz funkcji, modyfikator static, powoduje że mimo wyjścia z zasięgu zmienna nie jest niszczona, i przy następnym wejściu w dany zasięg, zmienna będzie posiadała poprzednią wartość.

 void kwadrat(int x) {
   static int ilosc_wykonan = 0; // lub poprostu static int ilosc_wykonan; albo static ilosc_wykonan = 0;
   ilosc_wykonan = ilosc_wykonan + 1;
   writefln("Funkcja kwadrat została wykonana ", ilosc_wykonan, " razy do tej pory").
   int k = x*x;
   return k;
 }

Powyższa funkcja, przy pierwszy wykonaniu, zaincjalizuje zmienna ilosc_wykonan wartością 0. Następnie powiększy ją o jeden, do wartości 1, i wyświetli jej wartość. Przy następnym wykonaniu, inicjalizacja zostanie pominięta, a powiększenie o jeden, spowoduje przypisanie wartości 2. Tak więc zmienna ilosc_wykonan służy tutaj za prosty licznik ilości wywołań funkcji kwadrat. Zmienna k, będzie za każdym razem inicjalizowana na nowo, ponieważ nie jest statyczna.

Słowo static, może być ponadto użyte przed metodami, funkcjami, importami modułów, i pętlami oraz wyrażeniami warunkowymi. Są to zastosowania specjalne, o których wspomnimy dalej w podręczniku.

extern

edytuj

Modyfikator extern (z ang. zewnętrzny) w D ma rzadkie zastosowanie. Służy on do poinformowania kompilatora o istnieniu danego identyfikatora i jego typu, ponieważ na przykład znajduje się w bibliotece o której kompilator nie wie. Ponieważ jednak język D ma rozbudowany system modułów oraz umożliwia użycie symboli przed ich definicją/deklaracją w większości przypadków nie trzeba stosować tego modyfikatora (zwykle jest to potrzebne w C, przy używaniu bibliotek i plików nagłówkowych). Modyfikator ten przydaje się przy współpracy z innymi językami programowania (umożliwia on zmian tzw. konwencji wywołań funkcji). W rozdziale Łączenie z innymi językami poznamy jego zastosowania.

Nadmienić należy jedynie, że zastosowanie modyfikatora extern nie powoduje alokacji pamięci dla zmiennej, a jedynie poinformowanie kompilatora. Taka konstrukcja jest więc jedynie deklaracją, nie zaś definicją. W D deklaracja i definicja to jedno i to samo, ponieważ odbywa się w tej samej linii - z tego powodu w całym tym rozdziale odwoływaliśmy się do tego faktu jako deklaracja. Formalnie poprawniej było by nazwać wszystkie konstrukcje z tego rozdziału właśnie definicjami. Definicja konstruuje zmienną (zajmuje miejsce i przydziela jakąś wartość), natomiast deklaracja jedynie mówi o typie. Oczywiście definicja jest jednocześnie deklaracją, lecz nie odwrotnie.

volatile

edytuj

Czasami się zdarza, że kompilator optymalizując kod widzi, iż w kilku kolejnych instrukcjach zmienna jest często wykorzystywana, więc ma taką samą wartości można ją trzymać w rejestrze (który jest szybszy niż pamięć), bo się przyda w fragmencie kodu który jest często wywoływany (np. pętla). Dodatkowo kompilator może założyć, że zmienna się nie zmienia poza miejscami w których do niej zapisujemy. Jednak takie założenie może być błędne, np. jeśli zmienna może być modyfikowana przez inny proces (w aplikacji wielowątkowej, albo znajduje się w pamięci współdzielonej) czy też jest to wartość odczytywana z jakiegoś portu urządzenia. Modyfikator volatile (ang. ulotny) nakaże za każdym razem odczyt tej zmiennej z pamięci.

 volatile int temperatura_z_czujnika;

register

edytuj

W języku D nie ma modyfikatora register.

W językach takich jak C czy C++ to słowo kluczowe jest wskazówką dla kompilatora, aby umieścić zmienna w rejestrze procesora, który jest dużo szybszy od pamięci. W języku D zadanie optymalnego ułożenia zmiennych jest pozostawiona kompilatorowi. Jest ku temu kilka powodów: aktualnie dostępne algorytmy przydziału rejestrów są wystarczająco dobre, w językach C/C++ słowo to i tak było często ignorowane ponieważ nie zawsze jest możliwe spełnienie żądań programisty, aktualne procesory maja coraz więcej rejestrów, zawsze można napisać ręcznie kod asemblerowy w wydajnościowo krytycznej sekcji kodu.

Rzutowania jawne i niejawne

edytuj

Rzutowanie polega na zmianie zmiennej jednego typu na drugiego. Może być ono jawne lub niejawne w zależności od tego czy programista zapisze jakiś fragment kodu zajmujący się tym, czy też wszystko obsłuży kompilator. Rzutowanie niejawne ma bardzo prostą postać:

int a = 2;
double d = a; //d = 2, przypisaliśmy do typu zmiennoprzecinkowego wartość całkowitą, ale kompilator się tym zajął.

Rzutowanie jawne jest za to niezbędne przy konwersji double → int, dzięki temu programista musi świadomie potwierdzić że wie o utracie części ułamkowej:

double d = 2.9;
int a = cast(int) d; //a = 2