Spustit prezentaci

Doporučené postupy v programování

Základní stavební prvky

Proměnné a konstanty · Názvy · Základní datové typy

Lubomír Bulej, David Majda

KDSS MFF UK

Proměnné a konstanty

Deklarace · Inicializace · Oblast platnosti · Použití

Deklarace proměnných

Explicitní deklarace

Implicitní deklarace

Když pomineme informaci o typu proměnné (té se budeme věnovat později), může nutnost explicitní deklarace proměnných vzbuzovat dojem přílišné byrokracie. Často tomu tak dokonce je. Na druhou stranu ovšem i tento aspekt souvisí s potřebou úrovně organizace odpovídající náročnosti projektu.

Explicitní deklarace dokumentuje úmysl, vytváří místo, ke kterému je možné vztáhnout dokumentaci, ale také už během překladu umožňuje odhalit chyby vznikající v důsledku nekonzistentního pojmenovávání proměnných, např. při použití proměnných acctNo a acctNum. Navíc také naprosto jasně vymezuje rozsah platnosti proměnné.

Implicitní deklarace zjednodušuje psaní a omezuje byrokracii zvláště při psaní běžného kódu, ale také může způsobit řadu problémů, právě proto, že nedokumentuje úmysl a hranice platnosti proměnné nejsou na první pohled zřejmé.

Na explicitní deklaraci pravděpodobně není až takový problém nutnost explicitně proměnnou zavést, potažmo nutnost proměnnou deklarovat na začátku funkce – tohle už v dnešních jazycích není až takový problém. Byrokracie spočívá hlavně v nutnosti specifikovat typ proměnné v situacích, kdy by ho mohl správně "domyslet" překladač.

Deklarace proměnných

Typová informace

Ideální kombinace?

V případě statického typování je typ svázán s proměnnou a při přiřazování hodnot je kontrolována typová kompatibilita, což je dobrá vlastnost. Některé implicitní typové konverze mohou situaci trochu zamlžovat, ale obecně je striktní typová kontrola žádoucí. Znalost typů během překladu navíc umožňuje překladači generovat efektivnější kód.

V případě dynamického typování je typ svázán s hodnotou a proměnná je jenom pojmenované místo, které může hodnotu obsahovat. Typ proměnné je odvozen od hodnoty, kterou obsahuje a za běhu programu se může měnit.

Přestože se dynamické typování v řadě případů hodí např. pro rychlé prototypování, nebo práci s databázemi, pro bězné programování poskytuje až příliš mnoho volnosti a pro rozsáhlejší projekty je nutné dodržovat striktní disciplínu.

Vlastně se ukazuje, že dynamické typování slouží často jako berlička v situacích, kterým by mnohem více slušela typová inference. Pokud při psaní kód potřebujeme několik proměnných pro mezivýsledky a pomocné výpočty, explicitní deklarace typu obtěžuje a navíc duplikuje typovou informaci. Dynamické typování tuto byrokracii odstraní, ale odstraní i striktní typovou kontrolu, což je něco co nechceme.

Při použití typové inference by prostě překladač odvodil typy proměnných od typů atributů nebo návratových hodnot funkcí vystupujících ve výrazech, takže by nebylo nutné typ deklarovat explicitně, to vše při zachování typové kontroly.

Příklad: typová inference v C#

Použití var typu pro lokální proměnné

Nesprávně inicializovaná data představují jednu z nejčastějších příčin chyb.

Inicializace proměnných

Nesprávná inicializace

Chyby způsobené nesprávnou inicializací se špatně hledají

Chyby vzniklé v důsledku špatné inicializace mají tendenci projevovat se nejrůznějšími způsoby, což činí jejich hledání obtížné. Navíc se mohou objevit po změně v kódu, která s místem chyby vůbec nesouvisí.

Zvláštní lahůdkou v této kategorii jsou ukazatele. S jejich inicializací typicky souvisí také alokace paměti. Neinicializovaný ukazatel se dá typicky snadno odhalit u statických proměnných (tam bude automaticky inicializován na 0, tedy NULL), ale u proměnných alokovaných na zásobníku nebo heapu tento luxus není.

Inicializace proměnných

Jak předcházet problémům s inicializací proměnných?

Princip lokality

Věci, které spolu souvisí, mají být v kódu u sebe.

Hlavní důvod proč deklarovat a inicializovat proměnné blízko místa použití je "Principle of Proximity", tj. související věci mají být pohromadě. To pak např. umožňuje vzít část kódu a přesunout ho jinam, aniž by se zapomnělo na inicializaci proměnné.

Příklad: inicializace proměnných

Oddělená deklarace,
inicializace a použití

// declare all variables
int accountIndex;
double total;
boolean done;

// initialize all variables
accountIndex = 0;
total = 0.0;
done = false;
...

// code using accountIndex
...

// code using total
...

// code using done
while (!done) {
   ...  

Příklad: inicializace proměnných

Oddělená deklarace,
inicializace a použití

// declare all variables
int accountIndex;
double total;
boolean done;

// initialize all variables
accountIndex = 0;
total = 0.0;
done = false;
...

// code using accountIndex
...

// code using total
...

// code using done
while (!done) {
   ...    

Deklarace a inicializace
blízko místa použití

int accountIndex = 0;
// code using accountIndex
...

double total = 0.0;
// code using total
...

boolean done = false;
/ code using done
while (!done) {
  ...     

Inicializace proměnných na jednom místě vytváří dojem, že se všemi proměnnými se pracuje po celou dobu. Přitom např. proměnná done se používá až na konci. Než se začne vykonávat kód, který proměnnou done používá, mohla být proměnná (chybně) změněna. Navíc při změně programu (už jen kvůli ladění) může dojít k přidání cyklů kolem kódu, který proměnnou done používá a ta pak bude správně inicializována pouze jednou.

Oblast platnosti proměnných (scope)

Na úrovni programovacího jazyka

Oblast platnosti ~ prostoru pro vznik chyb

Omezení prostoru pro vznik chyb ~ omezení oblasti platnosti

Oblast platnosti proměnných (scope)

Měřitelné charakteristiky na úrovni zdrojového kódu

lokalita přístupu (variable span)
průměrný počet řádků mezi přístupy k proměnné
doba života (variable live time)
počet řádků mezi prvním a posledním přístupem

Minimalizace "variable span" a "variable live time"

Princip lokality, který jsme použili u inicializace se dá použít obecně. V případě proměnných se snažíme mimo jiné omezit viditelnost jednotlivých proměnných tak, aby bylo jasné, které věci spolu souvisí a mají tedy zůstat pohromadě.

Minimalizací metrik VS a VLT se obecně zmenšuje se prostor pro chyby při práci s proměnnými. V důsledku toho se kód stává jaksi planárnějším a tedy čitelnějším a lépe pochopitelným – související části kódu se vejdou na obrazovku a není potřeba ho při změnách nebo ladění držet v hlavě.

Příklad: doba života proměnných

Příliš dlouhé doby života

 1    // initialize all variables
 2    recordIndex = 0;
 3    total = 0;
 4    done = false;
      ...
26    while (recordIndex < recordCount) {
27      ...
28      recordIndex = recordIndex + 1;
      ...

64    while (!done) {
        ...
69      if (total > projectedTotal) {
70        done = true;

Příklad: doba života proměnných

Doba života

recordIndex ... 27
total ... 67
done ... 67

Průměrná doba

54

Příliš dlouhé doby života

 1    // initialize all variables
 2    recordIndex = 0;
 3    total = 0;
 4    done = false;
      ...
26    while (recordIndex < recordCount) {
27      ...
28      recordIndex = recordIndex + 1;
      ...

64    while (!done) {
        ...
69      if (total > projectedTotal) {
70        done = true;

Příklad: doba života proměnných

Krátké doby života

25    recordIndex = 0;
26    while (recordIndex < recordCount) {
27      ...
28      recordIndex = recordIndex + 1;
      ...

62    total = 0;
63    done = false;
64    while (!done) {
        ...
69      if (total > projectedTotal) {
70        done = true;

Příklad: doba života proměnných

Doba života

recordIndex ... 4
total ... 8
done ... 8

Průměrná doba

7

Krátké doby života

25    recordIndex = 0;
26    while (recordIndex < recordCount) {
27      ...
28      recordIndex = recordIndex + 1;
      ...

62    total = 0;
63    done = false;
64    while (!done) {
        ...
69      if (total > projectedTotal) {
70        done = true;

Existuje nějaké číslo, které striktně oddělí špatný kód od dobrého? Pevná čísla nejsou známa, ale je dobře se snažit je minimalizovat.

Mimochodem, zkuste aplikovat uvedený postup na globální proměnné. Jaké byste očekávali výsledky?

Jak minimalizovat VS a VLT?

Dejte proměnným minimální viditelnost na úrovni jazyka

Seskupte příkazy pracující se stejnými proměnnými

Proměnné používané v cyklu inicializujte těsně před jeho začátkem

Hodnoty do proměnných přiřazujte těsně před použitím

Tato doporučení jsou v důsledkem minimalizace průměrných hodnot veličin variable span a variable live time.

Při seskupování souvisejících příkazů je často lepší je abstrahovat do procedury/funkce.

Postup při stanovení nutného rozsahu platnosti proměnné se postupuje podobně jako při konfiguraci zabezpečení. Začne se s ničím resp. maxinálně restriktivním nastavením a toto se na potřebných místech uvolňuje po nejmenších možných krocích tak, aby systém dělal co chceme.

Příklad: seskupování příkazů

Dvě skupiny proměnných, promíchané

void SummarizeData (...) {
  ...
  GetOldData (oldData, &numOldData);
  GetNewData (newData, &numNewData);

  totalOldData = Sum (oldData, numOldData);
  totalNewData = Sum (newData, numNewData);

  PrintOldDataSummary (oldData, totalOldData, numOldData);
  PrintNewDataSummary (newData, totalNewData, numNewData);

  SaveOldDataSummary (totalOldData, numOldData);
  SaveNewDataSummary (totalNewData, numNewData);
  ...
}       

Přestože tento kód na první pohled nevypadá špatně, vyžaduje po čtenáři aby držel v hlavě šest proměnných. Jak tento pohled podporují dříve zmíněné metriky?

Příklad: seskupování příkazů

Dvě skupiny proměnných, oddělené

void SummarizeData (...) {
  ...
  GetOldData (oldData, &numOldData);
  totalOldData = Sum (oldData, numOldData);
  PrintOldDataSummary (oldData, totalOldData, numOldData);
  SaveOldDataSummary (totalOldData, numOldData);
  ...
  GetNewData (newData, &numNewData);
  totalNewData = Sum (newData, numNewData);
  PrintNewDataSummary (newData, totalNewData, numNewData);
  SaveNewDataSummary (totalNewData, numNewData);
  ...
}       

Tento kód vyžaduje po čtenáři aby držel v hlavě pouze tři proměnné v daném okamžiku. Navíc z kódu viditelně vystupuje princip práce, který je kandidátem pro abstrakci.

Proč se oblastí platnosti vůbec zabývat?

Zásada minimalizace stavového prostoru

Čím menší je stavový prostor programu menší, tím lépe.

Pravidlo optimalizace pro čtenáře

Šetřte čas čtenáře/upravujícího na úkor pisatele.

Aplikace na rozsah platnosti

Na stavový prostor programu můžeme z několika směrů. Z hlediska stavu výpočtu je stavový prostor programu definován všemi možnými hodnotami všech proměnných. Stejně ho vnímají např. verifikační nástroje. Omezováním platnosti proměnných vytváříme jakési podprostory, na které je možné pohlížet jako na černou skříňku.

Z pohledu programátora (a rovněž překladače) se dá na stavový prostor pohlížet jako na množinu věcí (funkcí, proměnných, ...) platných v každém bodě programu. Programátorovi minimalizace tohoto počtu usnadňuje pochopení programu, překladači zase umožňuje generovat lepší kód.

Minimalizace rozsahu platnosti vyžaduje úsilí na straně pisatele (místo toho aby byly všechny proměnné globální a snadno přístupné), ale výsledkem je nejenom program, který je pro čtenáře lépe pochopitelný, ale celkově lépe navržený program.

Obecná doporučení k používání proměnných

Používejte proměnnou pouze k jednomu účelu

Vyhněte se skrytým významům hodnot proměnných

Systematicky odstraňujte nepoužité proměnné

Časté je využívání dolních bitů ukazatelů, protože ty dnes bývají zarovnané na 4 bajty.

Hodí se to například u interpretů jazyků, kde jsou potřeba rychlé operace s celými čísly. Podle dolních bitů se rozhodne, zda se v horní části daných 4 bajtů nachází číslo, nebo ukazatel na nějaký složitější objekt. V případě čísla se tak ušetří jedna dereference (za cenu, že takové číslo nemůže zabrat celých 32 bitů).

Pokud jste se někdy divili, proč má JavaScript nebo Ruby o jeden bit menší celá čísla, než byste čekali, teď už víte proč.

V konečném důsledku je jak přetěžování účelu nebo významu proměnné špatné proto, že název proměnné je buď příliš obecný a nic neříkající, což je špatně, nebo se hodí pouze pro jeden typ použití a jeden význam, což je (pokud bychom přetěžování připustili) také špatně.

Nejlepší je vnímat proměnné jako názvy pro koncepty, bez nutné spojitosti s místem v paměti, kde má být proměnná uložena. To má na starosti překladač, který může (a často to dělá) usoudit, že proměnná žádné místo v paměti nepotřebuje.

Příklad: přetížení účelu

Použití jedné víceúčelové proměnné

// compute roots of a quadratic equation
temp = sqrt (b^2 - 4 * a * c);
root [0] = (-b + temp) / (2 * a);
root [1] = (-b - temp) / (2 * a);

// swap the roots
temp = root [0];
root [0] = root [1];
root [1] = temp;

Příklad: přetížení účelu

Použití jedné víceúčelové proměnné

// compute roots of a quadratic equation
temp = sqrt (b^2 - 4 * a * c);
root [0] = (-b + temp) / (2 * a);
root [1] = (-b - temp) / (2 * a);

// swap the roots
temp = root [0];
root [0] = root [1];
root [1] = temp;

Použití více jednoúčelových proměnných

// compute roots of a quadratic equation
discriminant = sqrt (b^2 - 4 * a * c);
root [0] = (-b + discriminant) / (2 * a);
root [1] = (-b - discriminant) / (2 * a);

// swap the roots
oldRoot = root [0];
root [0] = root [1];
root [1] = oldRoot;

Příklad s víceúčelovou pomocnou proměnnou obsahuje vazbu mezi blokem kódu pro výpočet a blokem kódu pro prohození kořenů. Při abstrakci těchto bloků do funkcí je nutné ještě znovu deklarovat pomocnou proměnnou. Navíc při modifikaci kódu může dojít ke změně typu pomocné proměnné, přičemž nový datový typ už se pro oba účely hodit nemusí.

Použití samostatné proměnné pro každý účel tyto problémy eliminuje. Navíc je možné tyto proměnné deklarovat a inicializovat blíže místu použití, což zvyšuje čitelnost a pochopitelnost kódu.

Symbolické konstanty

Co je konstanta?

Umožňuje nahrazení literálů v kódu

Symbolické konstanty

Co je konstanta?

Umožňuje nahrazení literálů v kódu

Proč používat konstanty?

David Majda: Zažil jsem programátora, kterého při programování systému pro správu elektronického obchodu nenapadlo definovat konstantu pro DPH. Několik měsíců nato se sazba DPH změnila z 22% na 19%. Co bylo horší, toho člověka ani nenapadlo použít alespoň pro převrácenou hodnotu výraz 1 / 1.22 a pro jistotu všude psal 0.819672.

Příklad: použití konstant

Kód používající magická čísla

if (characterType & 0x03) ...
if (reportType == 16) ...
if (fileName.length() < 255) ...

Příklad: použití konstant

Kód používající magická čísla

if (characterType & 0x03) ...
if (reportType == 16) ...
if (fileName.length() < 255) ...

Kód s použitím konstant

final int CHAR_TYPE_LETTER = 0x01;
final int CHAR_TYPE_DIGIT = 0x02;
final int CHAR_TYPE_ALPHANUMERIC = (CHAR_TYPE_LETTER | CHAR_TYPE_DIGIT);

final int MAX_FILENAME_LENGTH = 255;

enum ReportType {
  DAILY, WEEKLY, MONTHLY;
}

if (characterType & CHAR_TYPE_ALPHANUMERIC) ...
if (reportType == ReportType.DAILY) ...
if (fileName.length() < MAX_FILENAME_LENGTH) ...

Musí být každý literál konstanta?

Výjimky pro čísla


0 Inicializace součtů, akumulátorů, indexů při for-cyklu
1 Inicializace součinů, indexů při for-cyklu, posuny o jedničku, odečítání od konce
  • endChar = str.charAt(str.length() - 1)
2 Půlení intervalů, průměry


Ostatní typy literálů

Konstanty a princip lokality

Konstanty porušují princip lokality

Z jiného pohledu...

Každé porušení principu lokality znamená, že programátor bude muset pobíhat po zdrojovém kódu sem a tam, aby vyhledal všechny potřebné informace ke kusu kódu, na kterém právě pracuje. To narušuje koncentraci a zdržuje, a tedy snižuje produktivitu.

Dnes situaci vylepšují "chytrá" IDE, kde se po najetí nad nějaký objekt zobrazí jeho definice nebo se na ni dá rychle odskočit a vrátit se zpět.

Názvy proměnných a konstant

Volba názvu · Vlastnosti dobrého názvu · Doporučení

"You can't give a variable a name the way you give a dog a name – because it's cute or it has a good sound."
– Steve McConnell

Příklad: názvy proměnných

Odstrašující příklad

x = x - xx;
xxx = aretha + SalesTax (aretha);
x = x + LateFee (x1, x) + xxx;
x = x + Interest (x1, x);

Příklad: názvy proměnných

Odstrašující příklad

x = x - xx;
xxx = aretha + SalesTax (aretha);
x = x + LateFee (x1, x) + xxx;
x = x + Interest (x1, x);

Vhodně zvolené názvy

balance = balance - lastPayment;
monthlyTotal = newPurchases + SalesTax (newPurchases);
balance = balance + LateFee (customerId, balance) + monthlyTotal;
balance = balance + Interest (customerId, balance);

Název proměnné by měl přesně a úplně popisovat, co proměnná reprezentuje.

Názvy proměnných

Nejprve úplný a přesný popis...

... teprve potom pohodlí

Rozumná výchozí technika pro vytváření názvů proměnných je prostě slovy říct, co konkrétní proměnná představuje. Někdy je to to nejlepší jméno, protože se dobře čte a neobsahuje zkratky a mnohoznačnosti. Težko se s něčím splete a dá se dobře zapamatovat, protože jméno je podobné konceptu, který označuje.

Vlastnosti dobrého názvu

Orientace na problém

Optimální délka

Příklad: optimální délka názvu

Příliš dlouhé

Příliš krátké

Tak akorát

Najít optimální délku názvu proměnné je jako v té pohádce o chytré horákyni. Název nemůže být příliš dlouhý, protože se hůře píše a zakrývá vizuální strukturu kódu. Nemůže být ani příliš krátký, protože pak nedává velký smysl.

Jména jsou často tvořena podstatnými jmény, někdy s prefixem ve formě přídavných jmen. Jméno je pak možné krátit vypouštěním méně důležitých modifikátorů.

Doporučení pro obecné názvy

Modifikátory pro vypočtené hodnoty

Používání obvyklých antonym

Doporučení pro obecné názvy

Pokud možno nepoužívat zkratky

Doporučení pro řídící proměnné cyklů

Triviální cykly

Ostatní cykly

Příklad: vnořené cykly

Nevhodné názvy indexových proměnných

float frubbish = 0.0;

for (int i = 0; i < foo.length; i++) {
  for (int j = 0; j < bar.length; j++) {
    for (int k = 0; k < zap.length; k++) {
      frubbish += frubbishDelta (foo[i], bar[k], zap[j]);
    }
  }
}       

Příklad: vnořené cykly

Nevhodné názvy indexových proměnných

float frubbish = 0.0;

for (int i = 0; i < foo.length; i++) {
  for (int j = 0; j < bar.length; j++) {
    for (int k = 0; k < zap.length; k++) {
      frubbish += frubbishDelta (foo[i], bar[k], zap[j]);
    }
  }
}       

Názvy omezující možnost vzniku chyby

float frubbish = 0.0;

for (int fooIndex = 0; fooIndex < foo.length; fooIndex++) {
  for (int barIndex = 0; barIndex < bar.length; barIndex++) {
    for (int zapIndex = 0; zapIndex < zap.length; zapIndex++) {
      frubbish += frubbishDelta (
        foo[fooIndex], bar[barIndex], zap[zapIndex]);
    }
  }
}       

Doporučení pro pomocné proměnné

Pomocné proměnné a mezivýsledky

Nic neříkající název "pomocné" proměnné

temp = sqrt (b^2 - 4 * a * c);
root [0] = (-b + temp) / (2 * a);
root [1] = (-b - temp) / (2 * a);

Správně pojmenovaná "opravdová" proměnná

discriminant = sqrt (b^2 - 4 * a * c);
root [0] = (-b + discriminant) / (2 * a);
root [1] = (-b - discriminant) / (2 * a);

Doporučení pro booleovské proměnné

Typické názvy

Název musí evokovat "černobílé" vidění

Název by měl být pozitivní

Doporučení pro stavové proměnné

Stavové/příznakové proměnné

Volba vhodného názvu

Doporučení pro konstanty

Stejný princip jako u proměnných...

... jak se liší konstanta od proměnné?

Doporučení pro výčtové typy

Názvy prvků výčtového typu

Název výčtového typu

Čemu se v názvech vyhnout

Různým názvům s podobným významem

Názvům obsahujícím čísla

Podobným názvům s různými významy

K názvům s podobným významem: analogie z algebry – maximální vs. největší prvek

Názvové konvence

K čemu jsou dobré?

Názvové konvence

Kdy je vhodné zavést názvové konvence?

Pro co se názvové konvence zavádějí?

Základní datové typy

Logické proměnné · Výčtové typy · Ukazatele

Logické proměnné

Často opomíjený způsob použití

Test s nejasným účelem

if ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) ||
  (elementIndex == lastElementIndex)) { ...

Logické proměnné

Často opomíjený způsob použití

Test s nejasným účelem

if ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) ||
  (elementIndex == lastElementIndex)) { ...

S použitím booleovských proměnných

finished = (elementIndex < 0) || (MAX_ELEMENTS < elementIndex);
repeatedEntry = (elementIndex == lastElementIndex);

if (finished || repeatedEntry) { ...

Výčtové typy

Zvyšují čitelnost a zjednodušují modifikaci

Zvyšují spolehlivost kódu

Jazyky bez výčtových typů

Při absenci podpory v jazyce

Nevhodný prefix, bez možnosti iterace

#define LL_INFO    0
#define LL_WARNING 1
#define LL_ERROR   2

Jazyky bez výčtových typů

Při absenci podpory v jazyce

Nevhodný prefix, bez možnosti iterace

#define LL_INFO    0
#define LL_WARNING 1
#define LL_ERROR   2

Správný prefix, s možností iterace

#define LOG_LEVEL_FIRST   0
#define LOG_LEVEL_INFO    0
#define LOG_LEVEL_WARNING 1
#define LOG_LEVEL_ERROR   2
#define LOG_LEVEL_LAST    2

Ukazatele

Potíž s ukazateli

Ukazatele

Potíž s ukazateli

Základní strategie při práci s ukazateli

  1. nedělat chyby při práci s ukazateli – jednoduchý a maximálně čitelný kód
  2. detekovat chyby co nejdříve – defenzivní techniky