Коментари, които лъжат, и такива, които помагат
Накратко: Добрият коментар казва защо кодът е такъв; коментар, който преразказва кода, остарява и започва да лъже, затова се заменя с добро име или функция.
Проблем
Коментарът, на който всички вярват
Магазинът дава безплатна доставка от определена сума нагоре. Миналата пролет прагът беше 50 € и коментарът над DeliveryFee казва точно това. После маркетингът го вдигна на 100 €. Числото в кода е сменено. Коментарът не е.
Тази есен колега пише банера в кошницата. Чете коментара, не кода, и слага 50:
// Безплатна доставка за поръчки от 50 € нагоре.static decimal DeliveryFee(decimal total){ if (total >= 100) { return 0; } return 4.90m;}
static string Banner(decimal total){ if (total >= 50) { return "Доставката е безплатна!"; } return $"Още {50 - total:F2} € до безплатна доставка";}decimal total = 60;Console.WriteLine($"Кошница: {total:F2} €");Console.WriteLine(Banner(total));Console.WriteLine($"Доставка: {DeliveryFee(total):F2} €");Кошница: 60.00 €Доставката е безплатна!Доставка: 4.90 €Клиент с кошница за 60 € чете „Доставката е безплатна!“ и после плаща 4,90 € за доставка. Поддръжката гледа DeliveryFee и не намира грешка: функцията прави точно каквото пише в кода. Грешката е в текст, който никой не пуска.
Виж го
Кодът продължи, коментарът остана
Ето какво стана, стъпка по стъпка:
// Безплатна доставка за поръчки от 50 € нагоре.static decimal DeliveryFee(decimal total){ if (total >= 100) { return 0; } return 4.90m;}
static string Banner(decimal total){ if (total >= 50) { return "Доставката е безплатна!"; } return $"Още {50 - total:F2} € до безплатна доставка";}
decimal total = 60;Console.WriteLine($"Кошница: {total:F2} €");Console.WriteLine(Banner(total));Console.WriteLine($"Доставка: {DeliveryFee(total):F2} €");Стъпка по стъпка, с думи
- Първата версия: коментарът (ред 1) и проверката (ред 4) казват едно и също: 50 €.
- Прагът става 100 €. Някой сменя числото на ред 4, а коментара не го пипа. Компилаторът проверява кода. Коментарите не ги проверява никой.
- Колега пише банера. Чете коментара, не кода, и на ред 13 слага 50.
- Кошница за 60 €. Банерът пита своя праг: 60 ≥ 50, значи „Доставката е безплатна!“.
- Таксата пита истинския праг: 60 < 100, значи доставката е 4,90 €.
- Клиентът чете „безплатна“ и плаща 4,90 €. Лъжата е на ред 1. Бъгът, който тя роди, е на ред 13.
Компилаторът проверява всеки ред код. Тестовете проверяват какво прави кодът. Коментара не го проверява никой: той е текст до кода, и при всяка промяна на кода може да остане назад. Такъв коментар се казва остарял коментар (stale comment). Той е по-лош от липсващ, защото звучи уверено.
Идеята
Нека кодът го каже сам
Прагът е правило на магазина, а правилото е на две места: в кода на таксата и в главата на всеки, който е чел коментара. Дай му име, и двете функции питат името:
const decimal FreeDeliveryFrom = 100;
static decimal DeliveryFee(decimal total){ if (total >= FreeDeliveryFrom) { return 0; } return 4.90m;}
static string Banner(decimal total){ if (total >= FreeDeliveryFrom) { return "Доставката е безплатна!"; } return $"Още {FreeDeliveryFrom - total:F2} € до безплатна доставка";}decimal total = 60;Console.WriteLine($"Кошница: {total:F2} €");Console.WriteLine(Banner(total));Console.WriteLine($"Доставка: {DeliveryFee(total):F2} €");Кошница: 60.00 €Още 40.00 € до безплатна доставкаДоставка: 4.90 €Коментарът вече не трябва: името казва същото. Разликата е, че името не може да остане назад. Ако прагът стане 150 €, сменяш един ред и Banner получава новия праг. Ако някой изтрие константата, програмата спира да се компилира.
Това е правилото на урока за имената (Имена в кода): преди да напишеш коментар, провери дали име не може да каже същото.
Пробвай
Предвиди изхода
Хотелът продава нощувки онлайн. Какво печата програмата?
// Цената с ДДС 20%.static decimal WithVat(decimal net){ return net * 1.09m;}
Console.WriteLine($"{WithVat(100):F2} €");Коментарът казва 20%, а кодът умножава по 1.09. Нощувките в България са с ДДС 9%, и някой е сменил числото, без да пипне коментара. Програмата слуша кода.
Виж го
Коментари, които повтарят кода
Всеки коментар тук казва същото като реда под него:
// Цената на продукта.decimal price = 25.00m;// Количеството.int quantity = 3;// Умножаваме цената по количеството.decimal total = price * quantity;// Отпечатваме сумата.Console.WriteLine($"Общо: {total:F2} €");Общо: 75.00 €Четеш всичко два пъти и не научаваш нищо ново. По-лошото е, че свикваш да прескачаш коментарите, и прескачаш и онзи, който е важен. Изтрий ги. Програмата е същата, а изходът също.
Виж го
Коментар вместо име, коментар вместо функция
Тук коментарите обясняват неща, които кодът крие:
int n = 7; // брой поръчки на клиентаdecimal s = 420; // оборот на клиента в евро// Клиентът е VIP: поне 5 поръчки или поне 500 € оборот.if (n >= 5 || s >= 500){ Console.WriteLine("VIP отстъпка: 10%");}VIP отстъпка: 10%Коментарът до n е там, защото името не казва нищо. Дай му името от коментара: orders. Коментарът над if описва правило. Правилото с име е функция:
static bool IsVip(int orders, decimal spent){ return orders >= 5 || spent >= 500;}int orders = 7;decimal spent = 420;if (IsVip(orders, spent)){ Console.WriteLine("VIP отстъпка: 10%");}VIP отстъпка: 10%Изходът е същият, а коментари няма. Редът if се чете като изречение. Ако правилото стане „поне 10 поръчки“, сменяш тялото на IsVip и всяко място, което я вика, получава новото правило. Как се отделя такава функция, е в урока Функции.
Виж го
Закоментиран код и TODO без собственик
static void Confirm(string orderId){ SendEmail(orderId); // SendSms(orderId); // if (smsEnabled) // { // SendSms(orderId); // } // TODO: оправи}Имейл за поръчка #1063Изпраща ли се SMS? Никой не знае дали е спрян нарочно, временно или по грешка, и никой не смее да го изтрие. Така закоментираният код живее години. Изтрий го: старият код е в историята на git и се връща с една команда.
TODO без име и без срок е желание, което никой не притежава. Добрият TODO казва кой, кой тикет и кога:
static void Confirm(string orderId){ SendEmail(orderId); // TODO(maria, SHOP-412): добави SMS, когато новият доставчик мине тестовете.}Имейл за поръчка #1063Още по-добре е задачата да е в тикета, а в кода да остане само връзката.
Идеята
Какво казва добрият коментар
Кодът казва какво прави. Добрият коментар казва нещо, което кодът не може да каже:
// Куриерът взима 30% от таксата, закръглено надолу до цент.// Така е в договора с него (т. 4.2), затова тук няма Math.Round.static long CourierShareCents(long feeCents){ return feeCents * 30 / 100;}long fee = 495; // SHOP-388: куриерът праща таксата в центове, не в евро.Console.WriteLine($"Куриер: {CourierShareCents(fee) / 100m:F2} €");Куриер: 1.48 €Тук има три такива неща:
- защо: закръгляне надолу изглежда като грешка, докато не прочетеш, че така е в договора. Без коментара някой ще го „поправи“;
- къде да научиш повече: тикетът
SHOP-388разказва историята, която в кода не се събира; - мерната единица, която типът не казва: центове, не евро. В урока за миризмите ще видиш как това отива в самия тип.
Още едно: предупреждение като „Бавно: всяко извикване е заявка към банката, около 300 ms. Не го викай в цикъл.“ Такъв коментар пести на следващия човек един ден в търсене.
И добрият коментар може да остарее. Но остарява рядко, защото не повтаря кода: договорът не се сменя всеки път, когато сменяш функцията.
По-точно, за любопитните
git blame показва кога е писан всеки ред. Ако кодът под коментара е по-нов от самия коментар, бъди нащрек: някой е пипнал кода и може да не е стигнал до коментара.
Виж го
Документация за този, който вика
Който вика DeliveryFee от друг модул, не иска да чете тялото ѝ. Иска да знае какво да подаде и какво ще получи. За това всеки език има документационен коментар, който редакторът показва, когато задържиш мишката над името:
const decimal FreeDeliveryFrom = 100;
/// <summary>Таксата за доставка на поръчка.</summary>/// <param name="total">Сумата на стоките в евро, без доставката.</param>/// <returns>0 от FreeDeliveryFrom нагоре, иначе таксата на куриера.</returns>static decimal DeliveryFee(decimal total){ if (total >= FreeDeliveryFrom) { return 0; } return 4.90m;}Console.WriteLine($"Доставка при 60 €: {DeliveryFee(60):F2} €");Console.WriteLine($"Доставка при 120 €: {DeliveryFee(120):F2} €");Доставка при 60 €: 4.90 €Доставка при 120 €: 0.00 €/// над метода, с <summary>, <param> за всеки параметър и <returns> за резултата. Visual Studio и Rider ги показват при всяко извикване, а компилаторът може да ги събере в XML файл до програмата.Документацията казва договора: какво значи параметърът и в какви единици е, какво се връща и кога има грешка. Не казва как е направено отвътре. Числата от кода не се повтарят в нея: вместо „от 100 €“ пише името на константата, за да не остане назад.
По-точно, за любопитните
<GenerateDocumentationFile>true</GenerateDocumentationFile> в проекта компилаторът предупреждава за публичен метод без документация (CS1591) и за <param> с име на параметър, който го няма (CS1572). Така поне имената в документацията не могат да остареят тихо.Попълни липсващото:
const decimal FreeDeliveryFrom = 100;
/// <summary>Таксата за доставка на поръчка.</summary>
/// < name="total">Сумата на стоките в евро, без доставката.</param>
/// <returns>0 от FreeDeliveryFrom нагоре, иначе таксата на куриера.</returns>
static decimal DeliveryFee(decimal total)
{
if (total >= FreeDeliveryFrom)
{
return 0;
}
return 4.90m;
}<param name="total"> описва параметъра total.Пробвай
Намери грешката
Магазинът праща имейл „Как ти се стори поръчката?“ ден след доставката. Последната поръчка на клиента е от 24.10, а имейлът пита за тази от 12.10:
var orders = new List<string> { "2026-10-19 #1057", "2026-10-24 #1063", "2026-10-12 #1041" };Console.WriteLine(Reminder(orders));Как ти се стори поръчка 2026-10-12 #1041?Кой ред взима грешната поръчка?
Избери реда с грешката.
Този ред взима първата поръчка, защото коментарът казва „от най-новата към най-старата“. Кодът сортира по дата нагоре: първата е най-старата. Някой е сменил подредбата за месечния отчет и е оставил коментара. Поправката е в две стъпки: вземи последната поръчка и замени коментара с име, което казва подредбата:
static List<string> OldestFirst(List<string> orders){ var sorted = new List<string>(orders); sorted.Sort(); return sorted;}
static string Reminder(List<string> orders){ string latest = OldestFirst(orders)[^1]; return "Как ти се стори поръчка " + latest + "?";}var orders = new List<string> { "2026-10-19 #1057", "2026-10-24 #1063", "2026-10-12 #1041" };Console.WriteLine(Reminder(orders));Как ти се стори поръчка 2026-10-24 #1063?Упражнение
Изчисти коментарите
Реши задачата в редактора си.
Отстъпка за лоялни клиенти. Във функцията има коментар, който лъже, коментар, който повтаря кода, закоментиран ред и TODO без собственик:
// Отстъпка за лоялни клиенти: 5%.static decimal LoyaltyTotal(decimal total, int orders){ // ако клиентът има повече от 10 поръчки if (orders >= 20) { // total = total * 0.95m; total = total * 0.90m; // намаляваме total } // TODO: закръгляне return total;}Console.WriteLine($"Сума: {LoyaltyTotal(200, 25):F2} €");Console.WriteLine($"Сума: {LoyaltyTotal(200, 12):F2} €");Сума: 180.00 €Сума: 200.00 €Изчисти я: махни шума и мъртвия код, замени числата с имена, а TODO или направи, или му дай собственик и тикет. Изходът трябва да остане същият.
Още задачи
- Документирай подредбата. Напиши документационен коментар за
OldestFirstот задачата с поръчките: какво приема, какво връща и в какъв ред. - Банерът на 99,99 €. Пусни банера от картата „Нека кодът го каже сам“ с кошница за 99,99 € и за 100 €. Провери, че двете функции са съгласни и на самия праг.
- Твоят проект. Отвори последния си проект и намери три коментара. За всеки реши: повтаря ли кода, казва ли защо, или лъже?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: static: общото за всички · Списъци · Имена в кода · Функции: една работа, малко параметри, без скрити странични ефекти
- Продължава в: Миризмите: дълъг метод, голям клас, дублиране, примитиви вместо типове, дълга верига if · Упражнение: разхвърлян клас, изчистен стъпка по стъпка
- Задачи по темата: Чист код и refactoring