Магічне число (програмування)
Поняття «Магічне число» в програмуванні має три значення:
- Сигнатура даних
- Виділені унікальні значення, які не повинні збігатися з іншими значеннями (наприклад, UUID)
- Погана практика програмування.
Сигнатура даних
Магічне число, або сигнатура, — цілочисельна або текстова константа, яка використовується для однозначної ідентифікації ресурсу або даних. Таке число саме по собі не має ніякого сенсу і може викликати здивування, зустрівшись у коді програми без відповідного контексту або коментаря, при цьому спроба змінити його на інше, навіть близьке за значенням, може спричинити абсолютно непередбачувані наслідки. З цієї причини такі числа іронічно називають магічними. В даний час ця назва міцно закріпилася як термін. Наприклад, будь-який відкомпільований клас мови Java починається з шістнадцятирічного «магічного числа» 0xCAFEBABE
. Інший відомий приклад — будь-який виконуваний файл ОС Microsoft Windows з розширенням .exe починається з послідовності байтів 0x4D5A
(що відповідає ASCII-символів MZ — ініціали Марка Збиковски, одного з творців MS-DOS). Менш відомим прикладом є неініціалізований вказівник у Microsoft Visual C++ (починаючи з 2005 версії Microsoft Visual Studio), який в режимі налагодження має адресу 0xDEADBEEF
.
В UNIX-подібних операційних системах тип файла зазвичай визначається за сигнатурою файла, незалежно від розширення його назви. Для інтерпретації сигнатури файла в них призначена стандартна утиліта file
.
Погана практика програмування
Також «магічними числами» називають погану практику програмування, коли у тексті програми зустрічається числове значення, сенс якого не очевидний. Наприклад, такий фрагмент, написаний на Java, буде поганим:
drawSprite(53, 320, 240);
Людині, яка не є автором програми, важко зрозуміти, що таке 53, 320 або 240. Але якщо цей код переписати, все стає на свої місця:
final int SCREEN_WIDTH = 640;
final int SCREEN_HEIGHT = 480;
final int SCREEN_X_CENTER = SCREEN_WIDTH/2;
final int SCREEN_Y_CENTER = SCREEN_HEIGHT/2;
final int SPRITE_CROSSHAIR = 53;
...
drawSprite(SPRITE_CROSSHAIR, SCREEN_X_CENTER, SCREEN_Y_CENTER);
Тепер зрозуміло: ця інструкція виводить у центр екрана спрайт — перехрестя прицілу. В більшості мов програмування всі значення, які використовуються для таких констант, будуть пораховані ще на етапі компіляції і підставлені в місця їх використання. Тому така зміна вихідного тексту не погіршує швидкодії програми.
Крім того, магічні числа — потенційне джерело помилок у програмі:
- Якщо одне і те саме магічне число використовується в програмі більше одного разу (або потенційно може використовуватися), то для зміни його значення потрібно буде виправити кожне входження (замість одного виправлення значення іменованої константи). Якщо не будуть виправлені всі входження, виникне принаймні одна помилка.
- Принаймні в одному зі входжень магічне число може бути написане з помилкою спочатку, і це досить складно виявити.
- Магічне число може залежати від неявного параметра або іншого магічного числа. Якщо ці залежності, не виділені явно, не будуть задоволені, виникне принаймні одна помилка.
- Під час модифікації входжень одного магічного числа можна помилково змінити інше магічне число, незалежне, але з таким самим числовим значенням.
Магічні числа і кросплатформність
Іноді магічні числа шкодять кросплатформності коду[1]. Річ у тому, що в Сі в 32- і 64-бітних ОС гарантується розмір типів char
, short
і long long
, тоді як розмір int
, long
, size_t
і ptrdiff_t
може змінюватися (у перших двох — залежно від уподобань розробників компіляторів, в останніх двох — залежно від розрядності цільової системи). У старому або невміло написаному коді можуть зустрічатися «магічні числа», що означають розмір певного типу — під час переходу на машини з іншою розрядністю вони можуть призвести до невловимих помилок.
Наприклад:
const size_t NUMBER_OF_ELEMENTS = 10;
long a[NUMBER_OF_ELEMENTS];
memset(a, 0, 10 * 4); // неправильно — мається на увазі, що long дорівнює 4 байтам, використовується магічне число елементів
memset(a, 0, NUMBER_OF_ELEMENTS * 4); // неправильно — мається на увазі, що long дорівнює 4 байтам
memset(a, 0, NUMBER_OF_ELEMENTS * sizeof(long)); // не зовсім правильно — дублювання назви типу (якщо зміниться тип, то доведеться змінити й тут)
memset(a, 0, NUMBER_OF_ELEMENTS * sizeof(a[0])); // правильно, оптимально для динамічних масивів ненульового розміру
memset(a, 0, sizeof(a)); // правильно, оптимально для статичних масивів
Числа, які не є магічними
Не всі числа потрібно переносити в константи. Наприклад, у такому коді на Delphi:
for i:=0 to Count-1 do ...
сенс чисел 0 і 1 цілком зрозумілий.