Jste zde

Chyby v návrhu: nevyužívání typového systému

Dnes se v našem seriálu o chybách v návrhu podíváme na situace, při nichž bychom plnohodnotným využíváním typového systému zpřehlednili kód a učinili jej méně náchylným k chybám.

(De)motivační příklad

Mrkněte na následující kusy kódu a řekněte mi, jak dlouho bude program čekat, než bude moct pokračovat (možnost příchodu signálů ignorujte):

// C (POSIX)
 
#include <unistd.h>
 
int main() {
    sleep(1000);
}
// C (Windows)
 
#include <windows.h>
 
int main() {
    Sleep(1000);
}
# Python
 
import time
 
time.sleep(1000)

Není to jednoduché, co?

V čem je problém?

Principiální problém je zde ten, že není na první pohled patrné, o jaké jednotky se jedná. Jsou to sekundy? Milisekundy? Něco jiného? Tento nedostatek způsobuje jednak ztíženou čitelnost kódu (je potřeba se podívat do manuálu) a jednak se jedná o snadný zdroj chyb (místo 1 člověk napíše 1000, protože předpokládá, že čas bude v milisekundách).

Pro ty, které by zajímala odpověď, tak tady je: V prvním a třetím případě se počká 1000 sekund, u druhé (Windows) varianty to bude 1 sekunda (předává se tam totiž počet milisekund).

Co s tím?

Řešením je při návrhu rozhraní plně využívat typový systém. Proč na všechno používat jen primitivní datové typy (čísla, řetězce), když si můžeme vytvářet vlastní, vysokoúrovňovější typy? Kód bude čitelnější a bezpečnější.

Pojďme se podívat, jak se s problémem uspání programu vypořádaly standardní knihovny jazyků C++ a Rust.

C++

Začněme s C++:

#include <chrono>
#include <thread>
 
using namespace std::chrono_literals;
 
int main() {
    std::this_thread::sleep_for(100ms);
}

Standardní funkce std::this_thread::sleep_for() přebírá jako parametr std::chrono::duration, reprezentující časový interval. V C++14 přibyly standardní literály pro časové intervaly, zpřístupněné using direktivou. Pro předání 100 milisekund stačí napsat 100ms, pro 100 sekund zase 100s apod. (seznam podporovaných literálů).

Rust

V Rustu je to řešeno následovně:

use std::thread::sleep;
use std::time::Duration;
 
fn main() {
    sleep(Duration::from_millis(100));
}

Standardní funkce std::thread::sleep() přebírá jako parametr std::time::Duration. Tento typ poskytuje konstruktory pro vytvoření časových intervalů z předaného počtu milisekund či sekund. V ukázce jsme vytvořili 100 milisekund. Opět je zde plně využit typový systém, který při překladu zabrání tomu, aby někdo omylem předal do sleep() pouze číslo:

sleep(100); // error: expected struct `std::time::Duration`, found integral variable

Kód není sice tak stručný jako v případě C++, ale svůj účel plní.

Na co si dát pozor

Při vytváření rozhraní je potřeba použít skutečné typy, nikoliv typové aliasy. Vezměme si např. následující příklad z C++:

using Seconds = int; // typedef int Seconds; v Céčku
 
void sleep(Seconds secs) {
    // Implementace.
    // ...
}

Tento kód nám sice umožní napsat

sleep(Seconds(100));

ale stejně tak nám umožní toto, což nechceme povolit:

sleep(100);

Jelikož v C a C++ není z hlediska typové kontroly žádný rozdíl mezi aliasem a aliasovaným typem, je potřeba vždy vytvořit typ nový. Jinak budeme tam, kde jsme byli.

Až tedy budete v budoucnu navrhovat rozhraní, pokuste se při tom plně využít typový systém. Např. pokud funkce přebírá velikost souboru, která ale může být v kB, MB atd., reprezentujte velikost vlastním typem. Nespoléhejte se na to, že každému bude jasné, že se funkci má předat počet bajtů (int).

Přidat komentář