D/Typy złożone

Z Wikibooks, biblioteki wolnych podręczników.
< D

Typy złożone[edytuj]

Język D zawiera w sobie kilka typów prostych, dzięki którym możemy reprezentować wartości logiczne, liczby oraz znaki. W większości wypadków jednak to zdecydowanie za mało. Dlatego w praktycznie wszystkich językach programowania dostępne są typy złożone, które umożliwiają reprezentację bardziej złożonych danych, jak na przykład współrzędnych punktów, adresów, wymiarów itd. Jednym z rodzajów typów złożonych są tablice, z których korzystałeś już wcześniej. W tym dziale poznasz inne, jak na przykład struktury, unie czy typy wyliczeniowe. Dowiesz się również, jak definiować własne typy oraz aliasy.

Definiowanie typów[edytuj]

Do definiowania własnych typów służy słowo kluczowe typedef:

typdef int mojint;
mojint a = 4;

Typ zdefiniowany instrukcją typedef jest przez kompilator uważany za inny, niż typ, który przyporządkowaliśmy nowemu. Dzięki temu możemy zdefiniować dwie funkcje:

void funkcja(int a){ /* funkcja ze zwykłym intem */ }
void funkcja(mojint a){ /* funkcja z moim intem */ }

int i = 4;
funkcja(i)  // wykona się funkcja ze zwykłym intem
mojint j = 4;
funkcja(j)  // wykona się funkcja z moim intem

Dla typów zdefiniowanych instrukcją typedef możemy ustalać wartości domyślne:

typedef int mojint = 7;
mojint a;   // a otrzymuje wartość 7

Aliasy do typów[edytuj]

Aliasy z pozoru są łudząco podobne do typów zdefiniowanych przy użyciu typedef. Jest jednak zasadnicza różnica - słowo kluczowe alias tworzy jedynie odnośnik, a nie nowy typ. Dlatego taka sytuacja:

alias int mojInt;

void funkcja(int a){ /* funkcja ze zwykłym intem */ }
void funkcja(mojint a){ /* funkcja z moim intem */ }

jest nie możliwa, ponieważ kompilator poinformuje nas o dwóch identycznych funkcjach. Aliasem jest na przykład string, pod którym kryje się typ invariant(char)[].

Aliasy do funkcji i zmiennych[edytuj]

Aliasy możemy też tworzyć do funkcji i zmiennych, np.:

alias std.math.sqrt pierwiastek;

real x = pierwiastek(2.0);

Bywa to dużym ułatwieniem, kiedy często korzystamy z funkcji lub zmiennych o skomplikowanych nazwach.

Typy wyliczeniowe[edytuj]

Typy wyliczeniowe (ang. enum, enumeration - wyliczenie) to typy, które mają zdefiniowaną pewną liczbę stałych wartości. Używa się ich najczęściej wtedy, gdy chcemy zawęzić zakres wartości zmiennej do kilku możliwości. Typy wyliczeniowe deklarujemy w następujący sposób:

enum Kolor {
  CZARNY,
  BIAŁY,
  CZERWONY,
  ZIELONY,
  NIEBIESKI,
  ZOLTY
};
Kolor kolor = Kolor.CZARNY;

Typy wyliczeniowe często są stosowane w konstrukcji switch:

switch (kolor) {
  case Kolor.CZARNY:
    writefln("Czarny");
    break;
  // pozostałe wartości
}

Typy wyliczeniowe oparte są domyślnie na typach liczbowych, dlatego wartościom typu wyliczeniowego można przypisywać wartości liczbowe (domyślnie każda wartość, której my nie przypisaliśmy liczby, otrzymuje liczbę o jeden większą od poprzedniej):

enum Kolor {
  CZARNY = 4,
  BIAŁY = 21,
  CZERWONY,                 // 22
  ZIELONY = 1
};

Standardowo zmienne typu enum, będą miały taki sam rozmiar jak int, tj. 4 bajty. Możliwa też jest konwersja z enumów do intów i z powrotem. Czasami możemy zarządać innej reprezentacji enumów, np. 1 bajtowej:

enum Kolor : ubyte {
  CZARNY,
  BIAŁY,
  CZERWONY,
  ZIELONY
}

Jeżeli chcemy, możemy stworzyć typ wyliczeniowy, którego wartości będą przechowywane w innym typie, niż liczbowy, np.

enum Kolor : string {
  CZARNY = "czarny",
  BIAŁY = "biały",
};

W takim wypadku musimy każdej opcji przyporządkować wartość określonego typu, ponieważ np. do ciągu znaków "czarny" nie można dodać 1.

Struktury[edytuj]

Struktura jest to typ danych umożliwiający przechowywanie wielu wartości różnych typów w jednej zmiennej. Typ strukturalny tworzymy za pomocą słowa kluczowego struct:

struct Struktura {
  int a;
  byte b;
  ushort c = 4; // Jeżeli przy tworzeniu zmiennej typu Struktura nie zainicjujemy c, ustawi się ono na 4.
}

Struktura s = {1, 4, 2};
Struktura s2 = {a:1, c:3, b:5}; // Możemy ustawiać parametry także w ten sposób
Struktura s3 = {1} // Nie musimy podawać wszystkich wartości - tutaj a=1, b=0 (nie podaliśmy) a c=4 (wartość domyślna).

Unie[edytuj]

Unie z pozoru są bardzo podobne do struktur, ale różnią się jedną zasadniczą cechą - unia może przechowywać tylko jedną wartość jednocześnie. Zmienne unii zachodzą na siebie w pamięci. Ilustruje to przykład:

import std.stdio;

union Unia {
  byte liczba;
  char znak;
}

void main() {
  Unia unia;
  unia.liczba = 0x24;
  writefln(unia.znak);         // Wypisze nam znak o kodzie 0x24, czyli $
}

Zwróćmy uwagę na to, że inicjowaliśmy liczbę, a wypisaliśmy znak. Na ekranie pojawił się znak o kodzie równym liczbie. Powodem tego zjawiska jest właśnie to, że liczba i znak znajdują się na tym samym miejscu w pamięci. Ta własność unii czasami bywa przydatna - umożliwia na przykład sprytną konwersję adresu IP do postaci szesnastkowej:

import std.stdio;

struct ByteAddress {
  ubyte a;
  ubyte b;
  ubyte c;
  ubyte d;
};
union IP {
  ByteAddress byteaddress;
  int fourByteAddress;
};

void main() {   
  IP ip;
  ip.byteaddress.a = 142;
  ip.byteaddress.b = 21;
  ip.byteaddress.c = 2;
  ip.byteaddress.d = 255;

  writefln("%X", ip.fourByteAddress);   // Wypisze: FF02158E
}

Wprowadzone do struktury byteaddress cztery liczby składające się na adres IP zostały odczytane w formie jednej zmiennej całkowitej i wypisane w systemie szesnastkowym na ekran. Jako, że liczba całkowita zajmuje w pamięci te same cztery bajty, co zmienne ubyte, to na ekran wypisana została zmienna złożona z tych czterech bajtów.

Użycie unii jest szeroko uważane za mało przejrzyste. Możne prowadzić do subtelnych błędów. W wielu wypadkach powoduje generacje bardzo nieoptymalnego kodu, oraz może powodować nie prawidłowe działanie garbage collectora. Używaj unii jedynie w absolutnej konieczności. W wielu wypadkach można użyć rzutowań (cast).