D/Typy złożone

< 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 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).