D/Podstawowe procedury wejścia i wyjścia

< D

Procedury wejścia i wyjścia

edytuj

Procedury wejścia i wyjścia (ang. input/ouput, IO) są sposobem na komunikację programu ze światem zewnętrznym. Może to być interakcja z klawiaturą i ekranem, albo zapis plików na dysk, czy też komunikacja przez sieć. Są one bardzo ważne, ponieważ bez nich nie bylibyśmy w stanie się dowiedzieć jaki są wyniki działania programu. Do procedur wejścia wyjścia można też zaliczyć sterowanie innymi urządzeniami. Procedury wejścia i wyjścia nie są formalnie częścią języka, a raczej biblioteki standardowej, lecz ich możliwości w pewien sposób świadczą o języku. Są one również utożsamiane z językiem z powodu bardzo częstego ich użycia. W tym rozdziale zajmiemy się procedurami obsługi ekranu a potem klawiatury.

Standardowe wejście i wyjście programu

edytuj

W większości systemów operacyjnych standardowe wejście i wyjście to synonim na klawiaturę i ekran (terminal znakowy). Są to pliki, i jako takie można na nich przeprowadzać operacje czytania i zapisywania, standardowymi procedurami plikowymi które poznamy później. Jednak z powodu częstości w której się używa procedur obsługi klawiatury i ekranu, stworzone do tego cele specjalne procedury ukrywające szczegóły obsługi tych plików oraz dodatkowo ułatwiające skomplikowane operacje (takie jak formatowanie, rozpoznawanie typów) - wbrew pozorom proste wyświetlenie liczby to operacja bardzo skomplikowana (np. funkcja writefln i kluczowe funkcje pomocnicze mają łącznie ok. 1500 linii kodu)

Standardowe wyjście (stdout) oraz literały tekstowe, writef i formaty

edytuj

Aby łatwo używać standardowe wyjście (ang. standard output, stdout), czyli domyślnie ekranu, do wypisywania tekstów oraz tekstowej reprezentacji innych typów (np. liczb) stworzono funkcje writefln i writef znajdujące się w module std.stdio. Funkcję writefln już używaliśmy do wypisywania łańcuchów znaków oraz liczb.

Argumenty funkcji writefln są w pewnym sensie mieszanego typu. Funkcja writefln mianowicie może przyjąć zero, jeden, lub więcej argumentów, i każdy może być innego typu. Funkcja ta wzoruje się na znanej z języka C, funkcji printf (która jest dostępna w D, ale nie jest polecana) - jednej z lepszej, zwięzłej i bardzo elastycznej formie wyświetlania wielu wartości w krótkim wyrażeniu.

Aby nie wprowadzać zamieszanie, najpierw opiszemy elementy odziedziczone z printf.

Funkcji writef przekazujemy jako pierwszy argument łańcuch znaków, z ewentualnymi znakami formatującymi i modyfikatorami typu.

W najprostszej postaci jest to sam tekst:

writef("Hello");

wypisze to nam tekst Hello, bez przechodzenia do nowej linii (potocznie bez entera na końcu)

Aby dodać enter należy dodać znak specjalny oznaczający nową linie, jest to znak '\n', na przykład na końcu:

writef("Hello\n");

Kompilator podczas analizy kodu zinterpretuje te dwa znaki (backslash i n) jako pojedynczy znak. Do najważniejszych znaków specjalnych należą:

znak specjalny znaczenie
\n nowa linia
\t tabulacja
\b cofnięcie o jeden znak (backspace)
\\ wypisywanie samego znaku \ (backslash)
\" wypisanie znaku podwójnego apostrofa " - \" (ponieważ w przeciwnym wypadku kompilator uznał by to za koniec napisu).

Ponieważ wstawianie entera na końcu jest bardzo często praktyką i częściowo zaciemnia nasz kod, lepiej jest użyć funkcji writefln, która po wyświetleniu żądanego tekstu, dodatkowo wyświetla jeden znak nowej linii

writefln("Hello");  // równoważne z writef("Hello\n");

Znaki specjalne możemy wstawiać w dowolnych miejscach w łańcuchu znaków. Na przykład

writef("Pierwsza\nDruga\n");

spowoduje wypisywanie słowa Pierwsza, przejścia do nowej linii, wypisania Druga oraz znowu przejścia do nowej linii.

Łańcuch formatujący może zawierać informacje o typach i formatowaniu kolejnych argumentów, na przykład

writefln("1/3 = %f", 1.0/3.0);

spowoduje wyświetlenie "1/3 = 0.333333". %f jest tutaj właśnie formatem. Zaczyna się on od znaku procenta, kończy na literze f oznaczającej liczbę zmiennoprzecinkową (float), inaczej rzeczywistą.

Istnieje wiele innych formatów. Do sprawnego posługiwania się procedurami wyjścia wystarczy znajomość kilku, tu podajemy je dla kompletności:

format znaczenie
%d liczba całkowita (wyświetlana w postaci liczby dziesiętnej)
%f, %F liczba rzeczywista (float, double, real): standardowo 6 cyfr dziesiętnych po przecinku oraz przynajmniej jedna cyfra przed przecinkiem.
%e, %E format naukowy liczb rzeczywistych: standardowo w formacie: 1.231213e+11, tj. jedną cyfrą przed przecinkiem, 6 cyframi po, oraz 2 wykładniku)
%g, %G liczba rzeczywista w formacie automatycznie dobieranym pomiędzy %f, a %e
%s łańcuch znaków UTF8 lub tekstowa reprezentacja innych danych: (true/false dla bool, %d dla całkowitych, %g dla rzeczywistych, kolejne elementy tablicy dla tablic dynamicznych i statycznych, opis obiektu poprzez metodę .toString() jeśli istnieje)
%% sekwencja służąca do wyświetlania znaku % (procent)
%x, %X liczba całkowita (wyświetlana szesnastkowo, małymi lub dużymi literami)
%o liczba całkowita (wyświetlana w postaci ósemkowej)
%b liczba całkowita (wyświetlana w postaci dwójkowej)
%a, %A liczba rzeczywista (wyświetlana szesnastkowo, z wykładnikiem przy podstawie 2)

W jednym łańcuchu może być więcej danych o formatach argumentów. Na przykład

int a = 42, b = 22;
writefln("a=%d b=%d a+b=%d", a, b, a+b);

wyświetli "a=42 b=22 a+b=64".

Należy pamiętać, aby formaty zgadzały się z typami podanych argumentów, oraz formatów nie było więcej niż podanych argumentów - w przeciwnym wypadku funkcja writefln zgłosi wyjątek i w domyślnej sytuacji zatrzyma program.

W D jednak formatów może być mniej niż argumentów. Wtedy zostaną one wyświetlone po wyświetleniu wszystkich już obsłużonych zmiennych w sposób domyślny, a w przypadku stałych literałów tekstowych (napisów) zostaną one zinterpretowane znowu jako łańcuch formatujący w którym znowu można używać znaków %, które tym razem będą dotyczyć argumentów za tym łańcuchem. Na przykład:

writefln("a=%d b=%d", a, b, " a+b=%d", a+b); // równoważne poprzedniemu
writefln("a=%d b=%d ", a, b, "a+b=%d", a+b); // j.w., tylko spacja w innym miejscu

albo

writefln("a=", a, " b=", b, " a+b=", a+b);    // j.w., ale zauważ spacje

Warto tutaj, nadmienić, że właściwość ta (interpretacja ciągu znaków w kolejnych argumentach jako łańcuch formatujący), została usunięta w wersji 2.029 biblioteki standardowej. Jedynie pierwszy łańcuch może być interpretowany jako łańcuch formatujący. Jest to podyktowane względami bezpieczeństwa, inaczej wyrażenie writefln("Napis ", s, " ma ", s.length, " znaki."), zadziałało by dla s="kot"; ale skończyły się błędem dla zmiennej s="%d", ponieważ writefln, starało by się skonwertować następny argument (" ma "), na liczbę. W szczególności s = "%d%d%d%d", starało by się odczytać 4 kolejne argumenty, które nie istnieją na liście argumentów, potencjalnie przerywając program, lub powodując inne problemy. Dodatkowo, w wersji 2.006, wprowadzono wymaganie, że pierwszy argument musi być łańcuchem formatującym (nie musi zawierać formatów). W szczególności pustą linię należy wyświetlić przy użyciu writeln(); lub writefln(""); Puste wywołanie, writefln();, jest obecnie niedostępne.

Formaty mogą mieć dodatkowe parametry (wpisywane pomiędzy %, a znak formatu), takie jak ilość cyfr po przecinku, czy szerokość pola w którym będzie wyświetlana zmienna:

float a = 1.0/3.0;
writefln("%.3f", a);       // wyświetli 3 liczby po przecinku tj. 0.333
writefln("%.6f", a);       // wyświetli 6 liczb po przecinku tj. 0.333333
int b = 12;
writefln("%6d", b);        // wyświetli 12, ale z dopisanymi 4 spacjami
  // po lewej, tak aby łącznie napis zajął 6 znaków, tutaj "    12"
  // przydatne przy tworzeniu tabelek, czy dobrze wyglądających raportów
float c = 124.22;
writefln("%8.2f", c);      // wyświetli 2 znaki po przecinku, oraz dopisze z 
  // lewej strony tyle spacji aby łącznie cały napis zajął 8 znaków.
  // tutaj "   124.22", przydatne np. w obliczeniach finansowych.

Zdefiniować można też sposób wyświetlania liczb (zgodnie z tabelką):

int z = 255*15;
writefln(" 0x%X\n 0x%x", z, z);
//Wyświetli 255*15 w systemie szesnastkowym, kolejno dużymi i małymi literami:
//0xEF1
//0xef1
//0x na początku ma charakter kosmetyczny

int z = 255*15;
writefln("%o", z);
//Wyświetli 255*15 w formacie ósemkowym (7361)

Standardowe wyjście błędów (stderr)

edytuj

Wypisywanie danych na stderr wygląda praktycznie tak samo jak na stdout, wystarczy poprzedzić metodę write(f)(ln) nazwą wyjścia i kropką.

stderr.writeln("Straszny błąd!");

Główną różnicą między stderr i stdout jest brak buforowania danych – wszystko jest natychmiastowo wypisywane na ekran. Obecnie brakuje nawet synchronizacji między wątkami ale ma być to wkrótce dodane. (stan na 29 czerwca 2016r.)

Standardowe wejście (stdin)

edytuj

Najprostszym sposobem na pobranie danych z standardowego wejścia (czyli domyślnie klawiatury), jest użycie funkcji readln z modułu std.stdio, na przykład:

string linia = readln();

string to typ do przechowywania ciągu znaków (inaczej, char[]). Zmienna linia, będzie zawierała jedną linie tekstu (tzn. kawałek tekstu, który był zakończony naciśnięciem klawisza nowe linii, Entera). Funkcji tej można użyć wielokrotnie, do wczytania wielu linii.

Funkcja ta zwróci specjalną wartość null, kiedy plik się skończy (np. naciśnięto Ctrl-D z klawiatury, lub standardowe wejście było przekierowane z pliku lub innego programy, przy użyciu potoku).

Poniższy program, wypisuje po kolei wszystkie wczytane linie z dopiskiem "echo", i po zakończeniu pliku, dodatkowo wypisuje ilość przetworzonych linii:

import std.stdio;
void main() {
  int licznik_linii = 0;
  string linia = readln(); // wczytaj (albo spróbuj wczytać) pierwszą linię
  while (linia !is null) { // dopóki linia jest różna od null, powtarzaj
    licznik_linii = licznik_linii + 1;  // zwiększ licznik o 1
    writefln("Echo: %s", linia); // wyświetl wczytaną linie
    linia = readln(); // wczytaj następną linię do zmiennej linia
  }
  writefln("Przetworzono %d linii", licznik_linii);
}

Wczytywanie liczb

edytuj

Najprostszym sposobem wczytania liczby z standardowego wejścia, jest wczytanie całej linii, a następnie skonwertowanie (zinterpretowanie) jej do odpowiedniego typu.

Można to wykonać, przy użyciu funkcji szablonowej to, dostępnej w module std.conv.

import std.conv;

int liczba = to!(int)(str);
double liczba = to!(double)(str);

Przykładowy program wczytujący ze standardowego wejścia liczbę oraz wyświetlający kwadrat tej liczby, może wyglądać tak:

import std.stdio;
import std.conv;

void main() {
  writefln("Wprowadź liczbę całkowitą:")
  string str = readln();
  int liczba = to!(int)(str);
  writefln("Kwadrat wprowadzonej liczby: %d", liczba*liczba);
}

Zmienna str, musi być prawidłowa i możliwa do skonwertowania, inaczej nastąpi błąd wywołania. W szczególności, w str, nie mogą znajdywać się zbędna znaki spacji.

Nawiasy okrągłe po znaku wykrzyknika, w tym przypadku można pominąć, i wiele osób uznaje to za bardziej czytelne.

int liczba = to!(int)(str);
int liczba = to!int(str);  // równoważne

Inną metodą, jest bezpośrednie użycie, funkcji dostępnych w module std.conv, takich jak:

int toInt(char[]);
uint toUint(char[]);
double toDouble(char[]);

Funkcje te jednakże oprócz zwracania wyniku, również modyfikują swój algorytm. Ma to na celu umożliwienie, wczytywania kolejnych liczb z stringu:

string str = "148 62"
int a = toInt(str); // wczytaj pierwszą liczbę, tj. a = 148
int b = toInt(str); // wczytaj kolejną liczbę, tj. b = 62

Funkcja ta ignoruje znaki spacji przed liczbą, ignoruje również nieprawidłowe znaki po liczbie. tzn. str = "148 62x", też zostanie skonwertowane tak samo.

Inną metodą, jest użycie metody readf (podobna do scanf z języka C), oraz innych metod obiektu din z modułu std.cstream (jest to obiekt posiadający również metody klasy Stream, z modułu std.stream). Moduł ten dostępny jest dopiero od wersji 2 biblioteki standardowej i języka D. Można go użyć np. w następujący sposób.

import std.cstream;
import std.stdio;
void main() {
  writefln("Wprowadź dwie liczby całkowite:")
  int a, b;
  din.readf(&a, &b);
  // lub din.readf("%d %d", &a, &b); // zauważ podobieństwo, do funkcji writefln.
                                     // Przed zmiennymi, należy jednak użyć znaku &.
  writefln("Suma wprowadzonych liczb: %d", a+b);
}

W dalszej części podręcznika, kilkakrotnie będziemy się odwoływać do konwersji i wczytywania danych. Będziemy do tego używać, albo funkcji szablonowej to (na razie nie musisz wiedzieć czym są szablony) z modułu std.conv, albo metody readf obiektu din z modułu std.cstream.

Używanie formatów do własnych potrzeb

edytuj

Mogą pojawić się sytuacje gdy chcemy łatwo wypisać zawartość naszej struktury lub klasy na ekran, można to zrobić na 2 sposoby. Pierwszym, o mniejszych możliwościach ale prostszym, jest definicja w obrębie klasy odpowiedniej metody toString().

class klasa
{
  void toString(scope void delegate(const(char)[]) sink) const
  {
    sink("bananas!");
  }
}

void main()
{
  auto k = new klasa();
  writeln(k);
}

Jedyna linia, która może budzić wątpliwości to ta:

void toString(scope void delegate(const(char)[]) sink) const

Nie musisz się tym na razie przejmować, wystarczy Ci wiedza że jest to metoda wywoływana gdy próbujemy użyć formatu „%s”, który jest domyślnie stosowany w zapisie bez formatu. (poprawnie obsługuje liczby, znaki itp.)

Po wykonaniu na stdout pojawi się zwykły napis „bananas!”.

Trudniejsze formaty
edytuj

A co jeśli chcemy obsłużyć np. format inny niż „%s” lub precyzję jak przy typach zmiennoprzecinkowych? („%.2f”) Wtedy definicja metody toString() zmienia trochę swoją formę i przyjmuje dodatkowe parametry:

void toString(scope void delegate(const(char)[]) sink,
                  FormatSpec!char fmt) const
{
    //fmt.spec - specyfikator
    //fmt.precision - precyzja (%.*f)
    //Więcej informacji można znaleźć w dokumentacji D
}

Prosty przykład obsługujący wyświetlanie długości lub szerokości geograficznej z różną precyzją:

import std.stdio;
import std.format;

class coord
{
public:
    /* Kolejno stopnie, minuty i sekundy */
    int degrees;
    int minutes;
    int seconds;

    /* Próba wypisania danych */
    void toString(scope void delegate(const(char)[]) sink,
                  FormatSpec!char fmt) const
    {
        /* Jeśli format nie jest równy 'i', 'I' i 's', należy rzucić wyjątkiem */
        if(fmt.spec != 'i' && fmt.spec != 'I' && fmt.spec != 's')
            throw new Exception("Unknown format specifier: %" ~ fmt.spec);
        
        /* Precyzja z jaką wyświetlamy dane, jeszcze nie znana. */
        int precision;
        
        /* Jeśli specyfikator to 's' skorzystamy z domyślnej precyzji - 3 */
        if(fmt.spec == 's')
            precision = 3;
        /* W innym wypadku użyjemy precyzji podanej przy formacie */
        else
            precision = fmt.precision;
        
        /* W zależności od precycji… */
        switch(precision)
        {
        case 1:
            /* Same stopnie */
            writef("%d°", degrees);
            break;
        case 2:
            /* Stopnie i minuty */
            writef("%d° %d\"", degrees, minutes);
            break;
        case 3:
            /* Stopnie, minuty i sekundy */
            writef("%d° %d\" %d'", degrees, minutes, seconds);
            break;
        default:
            /* Wyjątek – nieznana precyzja */
            throw new Exception("Wrong precision");
        }
    }
}

void main()
{
    auto c = new coord();
    /* Losowy kąt */
    c.degrees = -20;
    c.minutes = 5;
    c.seconds = -12;

    writefln("(%%s)   %s", c);    //Domyślna precyzja
    writefln("(%%.3i) %.3i", c);  //Określona precyzja - 3
    writefln("(%%.2i) %.2i ", c); //      -||-         - 2
    writefln("(%%.1i) %.1i ", c); //      -||-         - 1
}

Na stdout pojawi się to:

(%s)   -20° 5" -12'
(%.3i) -20° 5" -12'
(%.2i) -20° 5" 
(%.1i) -20°

Należy pamiętać że przykład jest uproszczony i m.in. nie zaokrągla stopni a jedynie ucina nadmiar informacji.