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