Конвенции за стил на кода
Coding Style Conventions
Какво са "Конвенции за стил на кода"? "Грозен" и "красив" код. Примерен стил.
Тази тема е една от няколкото, които не покриват материал, който можете да срещнете в задачи (тоест алгоритъм или структура данни). Все пак според нас тя ще ви помогне много за всяка задача, която пишете, както и в по-дългосрочен план.
По-дълъг код отнема по-малко време за писане?
Нещо, което почти никога не бива преподавано на начинаещи програмисти (и, според нас, e голяма грешка) е това, как да форматираме кода си, така че да избягваме някои чести имплементационни грешки, като същевременно го направим много по-лесен за четене и разбиране.Ако все още не сте, то със сигурност скоро ще се сблъскате с тъпа грешка, която сте допуснали при имплементацията, и в последствие ви е коствала над десет минути за дебъгване. Чрез няколко прости правила при писането на кода на програмата, в тази тема ще ви научим как да намалите шанса за част от тези грешки. Дори дребни неща като това къде слагате къдравите скоби, как именувате променливите си или това дали подравнявате кода си, могат много да помогнат за неговото дебъгване и като цяло изобщо да не се стига до него.
? | Много от модерните редактори идват с вграден "auto completion" -- тоест автоматично завършване на имена на променливи, функции, и т.н. Това значително помага за бързината на писане на код с дълги имена. Свиквайки да го ползвате, вие ще можете да пишете също толкова бързо кода си, както бихте го правили ако ползвахте еднобуквени имена. |
Конвенции за стил на кода
Какво всъщност представляват "Конвенциите за стил на кода"? Накратко - това е как форматираме програмата си. Няколко от основните неща са:- Как подравняваме кода
- Къде и кога слагаме къдрави скоби
- Как да се справяме с константите из кода
- Как кръщаваме променливите, функциите и т.н.
- Как разграничаваме променливи от константи, класове от функции и т.н.
"Красив" и "грозен" код
Всъщност, може би най-лесният начин за демонстрация на това е, както обикновено, чрез пример. За целта ще покажем три кода от истинско състезание на една и съща задача. Забележете, че времената на второто и третото решение са значително по-ниски, въпреки ползването на (относително) добър стил.Решение на Swetko: 15 минути и 15 секунди (C++)
#include<vector>
#include<string>
#include<algorithm>
using namespace std;
#define VI vector < int >
#define VS vector < string >
#define pb push_back
#define cs c_str()
#define sz size()
#define ALL(a) (a).begin(),(a).end()
///////////////////////////////////////////
char s[21]= "abkdeghilmnzoprstuwy";
class TagalogDictionary
{public:
vector <string> sortWords(vector <string> words)
{int q1,q2,q3,c1,c2,c3;
vector < VI > w;
VI v(128);
for(q1=0;q1<20;q1++)v[s[q1]]=q1+1;
for(q1=0;q1<words.sz;q1++)
{
string e1=words[q1];
VI e2;
for(q2=0;q2<e1.sz;q2++)
if(q2<e1.sz-1 && e1[q2]=='n' && e1[q2+1]=='g')
{e2.pb(v['z']);q2++;}
else e2.pb(v[e1[q2]]);
w.pb(e2);}
sort(ALL(w));
VS ans;
for(q1=0;q1<w.sz;q1++)
{string e1;
for(q2=0;q2<w[q1].sz;q2++)if(s[w[q1][q2]-1]=='z')
e1=e1+"ng";
else e1.pb(s[w[q1][q2]-1]);
ans.pb(e1);}
return ans;}};
Решение на dskloet: 8 минути и 31 секунди (Java)
import java.util.*;
import java.math.*;
public class TagalogDictionary {
String[] abc =
{"a", "b", "k", "d", "e", "g", "h", "i", "l", "m", "n", "ng", "o", "p",
"r", "s", "t", "u", "w", "y"};
public String[] sortWords (String[] words) {
Arrays.sort(words, new Comp());
return words;
}
class Comp implements Comparator<String> {
public int compare(String s1, String s2) {
String[] t1 = tokens(s1);
String[] t2 = tokens(s2);
for (int i = 0; i < t1.length && i < t2.length; i++) {
int cmp = index(t1[i]) - index(t2[i]);
if (cmp != 0) return cmp;
}
return t1.length - t2.length;
}
}
String[] tokens(String s) {
ArrayList<String> result = new ArrayList<String>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c != 'n') {
result.add("" + c);
continue;
}
if (i == s.length() - 1 ||
s.charAt(i + 1) != 'g') {
result.add("" + c);
continue;
}
result.add("ng");
i++;
}
String[] r = new String[result.size()];
return result.toArray(r);
}
int index(String s) {
int i = 0;
for(String l: abc) {
if (l.equals(s)) return i;
i++;
}
return i;
}
}
Решение на Petr: 7 минути и 6 секунди (C#)
using System;
using System.Collections.Generic;
public class TagalogDictionary {
class Word : IComparable<Word>
{
public string a;
string b;
public Word(string a, string b)
{
this.a = a;
this.b = b;
}
public int CompareTo(Word other)
{
return string.CompareOrdinal(b, other.b);
}
}
public string[] sortWords(string[] words)
{
Dictionary<char, char> map = new Dictionary<char, char>();
map['a'] = 'A';
map['b'] = 'B';
map['k'] = 'C';
map['d'] = 'D';
map['e'] = 'E';
map['g'] = 'F';
map['h'] = 'G';
map['i'] = 'H';
map['l'] = 'I';
map['m'] = 'J';
map['n'] = 'K';
map['o'] = 'M';
map['p'] = 'N';
map['r'] = 'O';
map['s'] = 'P';
map['t'] = 'Q';
map['u'] = 'R';
map['w'] = 'S';
map['y'] = 'T';
List<Word> l = new List<Word>();
foreach (string x in words)
{
string y = "";
for (int i = 0; i < x.Length; ++i)
if (i < x.Length - 1 && x[i] == 'n' && x[i + 1] == 'g')
{
++i;
y += 'L';
}
else
y += map[x[i]];
l.Add(new Word(x, y));
}
l.Sort();
Word[] newWords = l.ToArray();
return Array.ConvertAll<Word, string>(newWords, delegate(Word w) {
return w.a; });
}
}
Примерен стил
Ако погледнете ваш код от преди година, най-вероятно ще го намерите далеч по-различен от скорошен такъв. Това е много типично, особено в началото на програмистската ви кариера. С времето адаптирате по-хубави практики, като кодът ви става по-гъвкав и лесен за четене. Това е един вид "еволюция" на стила ви за писане. След време се доближавате все повече до някои от отвърдените добри практики, като все по-малко се променя с течение на времето.Ако искате да "пропуснете" този период на еволюция (който отнема обикновено 3-5 години), тук ще ви предложим примерен стил, който можете да следвате. Макар и някои от нещата да ви се струват ненужни или глупави, с времето ще се убедите, че от тях има смисъл. Що-годе към този стил ще гледаме да се придържаме и в кода, който прилагаме към темите на сайта.
Забележете, че това да пишете хубав код не е изискване да сте добър състезател. Някои от най-добрите състезатели в света пишат ужасен код по състезания. Примерно Светко (Светослав Колев), чиито код дадохме по-горе за пример, е един от най-добрите състезатели, които България е имала (три медала от IOI). Искаме да ви научим на хубав стил, защото това няма да ви навреди, а в някои случаи ще ви помогне.
CamelCase
За имена на променливи, функции, класове и т.н. ще ползваме така наречения CamelCase. При него началото на всяка (евентуално с изключение на първата) от думите, от които се състои името, е с главна буква, а останалите букви са малки. НапримерnumGolfBalls
или RedBlackTree
. Тъй като главните букви в имената приличат на гърбици на камила, оттам и името на стила.Защо?
Хубаво е да можем лесно да разграничаваме различните думи в едно име. Например
numgolfballs
е по-трудно за осмисляне от numGolfBalls
. Нещо повече, понякога може да се получат двусмислици - например ihaveagirlfriendwhoishot
може да е както i_have_a_girlfriend_who_is_hot
, така и i_have_a_girlfriend_who_i_shot
. ? | Понякога в професионален код се позволява ползването и на двата стила - но единият се ползва за един тип променливи и методи, докато другия - за друг. Реално се използва за още по-лесно осмисляне на функцията на променливите. В състезания рядко се пишат толкова сложни програми, затова не го правете. |
Впрочем, това е другата най-разпространена алтернатива на CamelCase - separate_by_underscores (различните думи се разграничават с подчертавка). Ползвайте или единия тип, или другия, но не и двата едновременно! Смесването им води до неконсистентен и донякъде грозен код.
Значещи имена
Ползвайте значещи имена за променливите, и особено за функциите, които пишете. Използвайте по възможност цели думи (или достатъчно ясни съкращения на цели думи). В по-кратки програми можете да ползвате и еднобуквени имена за променливи, стига буквата да е добре избрана.Защо?
В сравнително малка програма (до 30-50 реда) може и да успеете да помните всеки масив за какво е, но в по-голяма ще ви се налага да проверявате по няколко пъти "кое какво беше".
Например нека имате задача за баскетбол и ви трябват два масива - един за пазене на информация за топки (balls), и един за кошове (baskets). Далеч по-добре е те да се казват
balls[]
и baskets[]
, отколкото, например, b1
и b2
или b
и bb
. Ако не искате да хабите много време за писане на дълги имена, поне ползвайте смислени (логични) еднобуквени такива. Ако, например, имате три вида топки - червени, сини и зелени - ползвайте r
, b
и g
(от "red", "blue" и "green"), вместо, примерно, a
, b
, и c
.Език
Ползвайте имена на един единствен език, за предпочитане английски.Защо?
Макар и да сме Българи и да са ни учили да пишем на Български език, само един на хиляда хора по света говори нашия език. В кода най-често ще ви се налага да пишете на (неофициално) приетия за интернационален език - Английски.
? | В някои състезания като TopCoder входът директно ви се подава в променливи - които най-често са думи на Английски език. За да запазите кода си едноезичен, или ще се налага да прекръстите тези променливи, или да пишете собствения си код също на Английски. |
broiGolfTopki
или ChervenoChernoDyrvo
. Но когато правите своя избор, правете го консистентен - или всички променливи да са на Английски, или всички на шльокавица.Имена на променливи и функции
Имената на променливите и функциите започват с малка буква; всяка следваща дума в името започва с главна буква с останали малки. ПримерноnumGolfBalls
.Защо?
Хубаво е да има начин да разграничаваме имена на променливи от имена на класове и структури. С въведената конвенция, това става много лесно.
Имена на класове и структури
Имената на класовете и структурите започват с главна буква; всяка следваща дума в името започва с главна буква с останали малки. ПримерноRedBlackTree
.Защо?
Отново - за лесно разграничаване между {променлива, функция} и {структура, клас}.
Имена на константи
Имената на константи са с изцяло главни букви; различни думи в едно име са разделени с подчертавки. ПримерноMAX_NODES
или MIN_SIDE_LEN
. За предпочитане е да са изведени в началото на сорса.Защо?
Константите са обикновено нещо, което можем да ползваме за размер на масиви или ограничение на цикъл. Изкарвайки ги най-отгоре, лесно можем да видим или променим тяхната стойност (тя се дефинира само веднъж -- защо не там?).
Блуждаещи константи
Избягвайте ползването на числови константи из самия код - изведете ги като именувани такива в началото на файла. Например вместо да умножавате по3.1415926535
, направете константа const double PI = 3.1415926535;
и в кода умножавайте по PI
.Защо?
Докато константи като числото Пи може да са очевидни, други със сигурност няма да са, което ще прави кода неясен. Например какво ще разберете ако някъде в кода се умножава по
0.318309886
? Най-вероятно нищо. А тази константа всъщност е 1/pi, или бърз начин да делите на числото Пи.Основната причина за ограничаването на блуждаещи числа в кода, обаче, е друга. Нека например имате квадратна матрица, с размер 100, и правите цикли за да я инициализирате. Това като код ще изглежда нещо от сорта на:
double ma3x[100][100];
double getProbability(int perc) {
double prob = (double)perc / 100;
for (int i = 0; i < 100; i++) {
for (int c = 0; c < 100; c++) {
ma3x[i][c] = prob;
}
}
return 0;
}
Вместо това, дефинирайки размера на масива като именувана константа най-отгоре и ползвайки нея навсякъде в кода, където това е нужно, води до буквално едносимволна промяна за целия код.
const int MAX = 100; // Това е единственото място, което трябва да променим
double ma3x[MAX][MAX];
double getProbability(int perc) {
double prob = (double)perc / 100;
for (int i = 0; i < MAX; i++) {
for (int c = 0; c < MAX; c++) {
ma3x[i][c] = prob;
}
}
return 0;
}
Променливите са предмети, функциите са действия
Използвайте предмети за имена на променливи (масиви, класове), и действия за имена на функции. Напримерint numStudents
или double probability
, докато int countStudents()
или double getProbability()
.Защо?
Така смислово може да се разграничават функции от променливи (които иначе са неразличими откъм име - и двете са с еднакъв тип CamelCase). Обикновено променливите всъщност са някакъв предмет (абстрактен или не), докато функциите са действия, така че това би било логично да направите и по принцип.
Подравняване
Подравнявайте смисловите компоненти на кода заедно и консистентно.Защо?
Подравняване,
? | В някои езици, като например Python, индентацията е начинът, по който се определя тялото на функция, цикъл или условие. Така не се налага ползването на къдрави скоби въобще и в същото време програмистите са "задължени" да пишат хубав код. |
Когато кодът е добре подравнен, лесно можете да видите кои стейтмънти (присвоявания, промени, декларации) принадлежат към кои компоненти (функции, цикли, условни оператори).
В следващия пример е сравнително трудно да се види кое се върши в кой цикъл.
const int MAX_N = 128;
int numNodes;
int graph[MAX_N][MAX_N];
bool checkForPath(int startNode, int endNode) {
int numComparisons = 0;
for (int k = 0; k < numNodes; k++) {
if (graph[startNode][endNode]) {
return true;
}
for (int i = 0; i < numNodes; i++) {
if (graph[startNode][endNode])
return true;
for (int c = 0; c < numNodes; c++) {
if (graph[i][k] != 0 && graph[k][c] != 0) {
graph[i][c] = 1;
if (i == startNode && c == endNode)
return true;
}
numComparisons++;
}
}
}
fprintf(stderr, "Wasted %d comparisons!\n", numComparisons);
return false;
}
const int MAX_N = 128;
int numNodes;
int graph[MAX_N][MAX_N];
bool checkForPath(int startNode, int endNode) {
int numComparisons = 0;
for (int k = 0; k < numNodes; k++) {
if (graph[startNode][endNode]) {
return true;
}
for (int i = 0; i < numNodes; i++) {
if (graph[startNode][endNode])
return true;
for (int c = 0; c < numNodes; c++) {
if (graph[i][k] != 0 && graph[k][c] != 0) {
graph[i][c] = 1;
if (i == startNode && c == endNode)
return true;
}
numComparisons++;
}
}
}
fprintf(stderr, "Wasted %d comparisons!\n", numComparisons);
return false;
}
Подравняване чрез шпации
Накарайте редактора си да замества таблуации с фиксиран брой шпации - например 2 или 4.Защо?
Макар и само препоръчително, това правило има известен смисъл - така ще гарантирате, че кодът ви ще се показва по еднакъв начин в различни среди. Примерно в някои редактори една табулация е с размер на две шпации, на някои с четири, а на трети - осем. Използвайки шпации вместо табулации, кодът ви ще бъде еднакъв и в трите редактора. Всъщност вие ще продължите да ползвате табулация, когато искате да индентирате даден ред или блок, просто редакторът ви "незабелязано" ще ползва фиксиран брой шпации, вместо знакът за табулация.
Кратки редове
Ограничавайте дължината на редовете си до 80, 100 или най-много 120 символа на ред.Защо?
Много дългите редове са трудни за четене и осмисляне. Стигането до дълъг ред означава, че вършите много неща в него, което обикновено е лоша практика и предпоставка за грешки.
Кратки функции
Избягвайте писането на дълги и особено на много дълги функции. Освен ако не е наистина наложително (няма как да бъде разбита на по-малки такива), старайте се тялото на функцията ви почти никога да не надхвърля 40-50 реда, като рядко да е над 20-30.Защо?
Обикновено писането на дълги функции означава, че вършите много неща в тях или имате много копиран код, което не са хубави практики. Повечето на брой, но по-кратки функции, са по-лесни за промяна, дебъгване, а дори в някои случаи и за оптимизиране. Чрез имената на функциите можете да "подсказвате" какво всъщност правите вътре, като така до известна степен се коментира кода дори без писане на коментари.
Къдрави скоби
Отварящата скоба е на същия ред, разделена със шпация от елемента, на който принадлежи. Затварящата е на отделен ред, със същата индентация, с която е елемента, който затваря.Защо?
Така лесно ще знаете коя скоба кое затваря, и откъде докъде се простира даден блок код - просто ходите нагоре по кода, докато стигнете до друг видим символ със същата индентация.
За това къде да са скобите има много варианти и стандарти - това е само един от тях. Той е и един от най-разпространените, така че често ще го срещнете. Предимството му е, че е сравнително балансиран между добро отделяне на блока от останалия код и използване на допълнителни редове. Друга алтернатива, която е (или поне доскоро беше) моят личен фаворит е Allman (ANSI, BSD) стилът. Няколко от основните стилове можете да видите ето тук.
Един стейтмънт на ред
Не слагайте по повече от един стейтмънт на ред. Потенциално изключение правят стейтмънти, които са много кратки, подобни и тясно свързани. В този случай ги разделяйте със запетая, вместо точка и запетая.Защо?
Макар и понякога много примамливо, слагането на повече от един смислов елемент на ред може да доведе до грешки. Дори и в момента на писане на кода тяхната употреба да е вярна, по-нататъшни промени могат да доведат до грешки.
Отделянето на операции чрез запетаи е почти идентично на това с точка и запетая, но кара операциите да се третират като блок (все едно са групирани в къдрави скоби).
Пример за грешка:
Нека имаме парче код, което проверява дали дадена стойност е надхвърлила определен лимит, и ако да - да я запазваме в друга променлива и намаляме, след което печатаме дебъг информация за това.
if (someVariable >= SOME_LIMIT) {
lastVariable = someVariable; someVariable -= SOME_LIMIT;
fprintf(stderr, "DEBUG: Reducing variable to %d\n", someVariable);
}
if (someVariable >= SOME_LIMIT)
lastVariable = someVariable; someVariable -= SOME_LIMIT;
if (someVariable >= SOME_LIMIT)
lastVariable = someVariable, someVariable -= SOME_LIMIT;
Локалност
Декларирайте локалните променливи близо до мястото, където ги използвате за пръв път, вместо в началото на функцията.Защо?
Да се декларират всички променливи в началото на функцията е нещо, наследено от C. В C++, обаче, няма никакъв смисъл това да се прави, тъй като езикът позволява те да бъдат декларирани навсякъде в тялото на функцията. Колкото по-късно декларирате променливата, толкова по-малък е шансът да я използвате погрешка по-рано в кода, или да промените стойността ѝ (ако е инициализирана).
Същото важи за променливи, които ползвате за итератори в цикли. Например защо да декларирате
int i
и после да го ползвате във for (i = 0; i < n; i++)...
, след като можете да направите това директно във for-цикъла? Нещо повече, така не се налага да мислите предварително какви променливи ще ви трябват - ще си ги създавате в процеса на писане. Също така няма да се налага да се връщате до началото на функцията, за да добавяте нови променливи, когато ви потрябват такива!Коментари
Оставяйте кратки коментари преди сложно парче код. Ако в дадена функция (алгоритъм) трябва да свършите няколко неща, преди да почнете имплементацията напишете по един коментар за всяка негова част, за да не забравите, че трябва да я свършите, докато се занимавате с останалите.Защо?
Коментарите, макар и "излишен" код, могат да бъдат полезни при имплементиране на парчета код с по-сложна логика.
? | // sometimes I believe compiler ignores all my comments |
Например, пишейки някаква функция, преди да почнем с "реалната" имплементация, бихме могли да си оставим следните подсказки.
int someFunction(int someArgument1, int someArgument2) {
// Handle corner cases
// Handle primes
// Handle even non-primes
// Handle odd non-primes
// Memoize
}
Коментиран код
Коментирайте код в тялото на функцията с//
, докато цели функции с /*...*/
.Защо?
/*...*/
е удобно за коментиране както на малък брой редове, така и за голям. //
,
? | //////////////////////// this is a well commented line |
Ако искате да коментирате цяла функция, обаче, няма да можете да сторите това, ако вътре в нея има друг
/*...*/
коментар. От друга страна, ако всички "вътрешни" коментари са от тип //
, то ще можете.Използвайте sizeof() върху променливи
Използвайтеsizeof()
върху променливи, не върху типове. Например за да вземете броя байтове на масив int someArray[MAX][MAX]
, ползвайте sizeof(someArray)
вместо sizeof(int) * MAX * MAX
.Защо?
sizeof()
работи правилно както за типове, така и за променливи, едномерни масиви и дори многомерни масиви. Подавайки самата променлива като аргумент, а не нейния тип, помага да избегнете следната потенциална грешка: ако по някое време решите да смените типа на масива (на, примерно, short
или double
), sizeof(someArray)
ще връща отново правилен размер, докато sizeof(int) * MAX * MAX
вече ще бъде грешно.Допълнителни материали
- Coding Conventions (en.wikipedia.org)
- Naming Conventions (en.wikipedia.org)
- Programming Style (en.wikipedia.org)
- Много детайлни напътствия, изполвани в Google (www.googlecode.com)
- Различни стилове за индентация + анкета (blog post)
- Някои от основните стилове за индентация (en.wikipedia.org)
- What is the best comment in source code you have ever encountered (www.stackoverflow.com)
За да предложите корекция, селектирайте думата или текста, който искате да бъде променен,
натиснете Enter и изпратете Вашето предложение.
натиснете Enter и изпратете Вашето предложение.
Страницата е посетена 7299 пъти.