facebook pixel מדריך: כותבים קוד יותר טוב - מספרי קסם - www.4project.co.il
Main logo www.4project.co.il
כל הרכיבים לפרוייקט שלכם
עגלת קניות

העגלה ריקה

כותבים קוד יותר טוב - מספרי קסם


2025-08-24 16:25:37

הקדמה

זוהי סדרה קצרה של מדריכים שיסבירו לכם איך לכתוב קוד שיהיה קצת יותר טוב ויעיל.
הדגש הוא לא על הביצועים, אלא שיהיה קריא יותר, מובן לאחרים וגם לכם, לאחר פרק זמן כלשהו.

המדריכים הם לא ציטוטים של מה שכתוב בספרים, אלא מניסיון של מעל שני עשורים של כתיבת קוד במערכות Embedded, Kernel, ואפליקציות שונות גם בצד השרת וגם בצד הלקוח, בשפות תכנות שונות. גם בתור מפתח בחברות גדולות וסטארטאפים קטנים, וגם אחרי הרבה פרוייקטים בהם אני מפתח לבד.

מדריכים אלה לא מתיימרים ללמד אתכם לתכנת, אלא איך לכתוב את הקוד בצורה שתאפשר ליותר מאדם אחד לעבוד על אותו הפרוייקט, שהקוד יהיה מובן לך וגם לאחרים, יקטין את האפשרויות של באגים וטעויות, ויהיה יעיל וקל לתחזוקה והתפתחות עתידית של הפרוייקט.

ההתייחסות שלי תהיה בעיקר לשפת C/C++, המשמשת בפיתוח לכרטיסי Arduino, אבל כל הפרקים בסדרת מדריכים אלה יהיו רלוונטיים גם לשפות וסביבות פיתוח אחרות.


מהם מספרי קסם

קסמים בדרך כלל מזכירים משהו חיובי מהילדות, אבל זה לא המצב בתכנות. מספרי קסם (magic numbers) הם מספרים קבועים בקוד שלרוב לא ברור מאיפה הגיע הערך שלהם. סוג של מאגיה שחורה (black magic).

מספרים כאלה יכולים להופיע במקומות שונים בקוד. הנה כמה דוגמאות נפוצות:
הגדרת מערכים ומחרוזות:
קוד: בחר הכל
char name[16];
unsigned int score[10];


לולאות:
קוד: בחר הכל
for (int idx; idx<10; idx++) {
    total += score[idx];
}


שימוש כערכים לטובת בדיקה או השמה. המספרים לא חייבים להיות שלמים. כל מספר שכתבתם ישירות בקוד, הוא מספר קסם:
קוד: בחר הכל
if (total < 60) {
    // Do something
}

motorSpeed = 99.56;



למה זה לא טוב?

תחשבו בגדול. כל פרוייקט בסופו של דבר גדל ומתפתח מעבר לכמה שורות קוד.
דמיינו שמספרים אלה מפוזרים בהמון מקומות בקוד, ועכשיו אתם צריכים לשנות איזה ערך מכל סיבה שהיא.

למשל הבנתם שמהירות 99.56 גבוהה מדי למנוע ואתם רוצים שהערך יהיה 88.15 במקום, ויש לכם כמה מנועים כאלה בפרוייקט. עם כמה מקומות בקוד שמציבים את הערך למשתנים, משווים האם המהירות גבוהה מהערך הזה וכו'.

או לדוגמה הבנתם שיש שמות ארוכים ו-16 תווים שהגדרתם לא מספיקים, רוצים לשנות ל-32.

מה תעשו? פשוט תחפשו מספר 16 בקוד ותחליפו כל המופעים שלו ל-32? (Ctrl+H ברוב תוכנות העריכה). ואם יש עוד דברים שמספר 16 מופיע בהם וצריך להישאר ככזה?

אולי יש מקומות בקוד שמנסים להיות יעילים יותר ומעתיקים את השם בלולאה של 15, כי התו האחרון הוא תמיד NULL (ערך 0) ואין צורך להעתיק אותו?
או אולי הלכתם עוד צעד לטובת הייעול כי הקוד רץ במעבד של 32bit ויכול להעתיק 4 תווית בפעולה אחת, כך שאפשר לעשות רק 4 לולאות כדי להעתיק 16 תווים?
למקומות כאלה לא תגיעו בכלל עם חיפוש של מספר 16...

חיפוש והחלפה לא יעזרו לכם. גם אם השקעתם בתיעוד מפואר של הקוד, הוא לא יספיק. תמיד תחשבו על מקרה שמישהו אחר מנסה לשנות את הקוד. האם הוא יוכל להגיע לכל המקומות הנחוצים כדי לשנות את המספרים?

ואם לא עדכנתם את כל המקומות לטובת השינוי, תצפו לצרות כמו דריסות זכרון, מידע לא נכון, reset אקראי של הבקר בתזמון שלא קשור לשינוי ודברים קסומים אחרים שלא תמיד קל להגיע למקור הבעיה.


הפתרון

הפתרון הוא פשוט - להגדיר את המספרים כמספרים קבועים (constants) או כ-define#.
בצורה זו ההגדרה של המספר תהיה רק במקום אחד וכל שאר הקוד ישתמש בהגדרה זו.
אם אתם רוצים להיות מסודרים עוד יותר, תשימו את כל ההגדרות במיקום אחד, בקובץ אחד או במקום שלוגית יהיה רלוונטי לקוד בו הם משמשים. אסביר על כך קצת יותר במדריך לחלוקת הקוד.

הנה דוגמה להגדרה של מספרים קבועים:
קוד: בחר הכל
const unsigned int nameLength = 16;
const float maxSpeed = 88.15;


וזו צורת הכתיבה להגדרות define#:
קוד: בחר הכל
#define NAME_LENGTH 16
#define MAX_SPEED 88.15


חוץ משימוש באותיות גדולות בשמות ההגדרה, התוצאה בשתי צורות ההגדרה תהיה זהה. אסביר על שמות המשתנים והפונקציות במדריך נפרד.
התצורה של מספרים קבועים נחשבת למודרנית ומועדפת יותר, מכיוון שאתם מגדירים לא רק את הערך אלא גם את סוג המספר (float, int).

לא לדאוג, מספרים קבועים לא תופסים מקום בזכרון RAM. הקומפיילר יודע שאלה לא משתנים, אלא קבועים, שאי אפשר יהיה לשנות את הערכים שלהם בקוד, כך שאין צורך בזכרון בשבילם.

מה שחשוב זה שההגדרה תהיה במקום אחד ולא מפוזרת בכל הקוד כמספרי קסם.

השימוש בהגדרות אלה בקוד זהה לעבודה עם משתנים:
קוד: בחר הכל
char name[nameLength];
unsigned int score[SCORE_ARRAY_LENGTH];
for (int idx; idx<SCORE_ARRAY_LENGTH; idx++) {
    total += score[idx];
}

motorSpeed = maxSpeed;


טיפ:
הכירו את הפקודה ()sizeof, היא מאוד שימושית ללולאות מעבר על מערכים או מבני נתונים, או במקרים בהם מעורב גודל המשתנה. בעזרת פונקציה זו אפשר לכתוב קוד שישאר עדכני גם אם אתם משנים את סוג המשתנים (char, int, long) ולא רק את כמות הנתונים במערך.


הגדרות בכל מקום

די בטוח שהשתמשתם בהגדרות של מספרים קבועים בפרוייקטים שונים בלי לשים לזה לב.

תוכנית ה-blink היא בדרך כלל הדבר הראשון שמנסים לצרוב על כרטיס ארדואינו. כמה שורות קוד בודדות, אבל כמעט כל שורה משתמשת בהגדרת מספר כלשהו:
קוד: בחר הכל
// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
  delay(1000);                      // wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // turn the LED off by making the voltage LOW
  delay(1000);                      // wait for a second
}


מזהים את מספר הקסם בקוד הפשוט הזה? כן, ה-1000 הוא בהחלט מספר קסם שצריך להימנע ממנו בתוכניות קצת יותר רציניות מהבהוב לד.

ומה לגבי הגדרות של מספרים?
בקוד זה יש כמה שמוגדרים כ-define# בסביבת ארדואינו. שני אלה יהיו קבועים וזהים לכל כרטיסי Arduino:
  • LOW = 0
  • HIGH = 1

שתי ההגדרות הבאות יכולות לקבל ערכים שונים, תלוי במיקרובקר ומבנה הכרטיס:
  • OUTPUT
  • LED_BUILTIN
ה-OUTPUT משמש להגדרת צורת העבודה של קווי המיקרובקר ומוגדר כ-1 בכרטיסים המבוססים על בקרי ATmega32 וגם SAMD21, אבל יכול להיות שונה במיקרובקרים אחרים.

ה-LED_BUILTIN מגדיר את מספר הקו אליו מחובר הלד שנמצא על הכרטיס. בכרטיסים הראשונים המפתחים של ארדואינו בחרו בקו מספר 13 וכך זה נשאר לרוב הכרטיסים החדשים יותר. מספר זה מוגדר בקובץ הגדרות של הכרטיס ולא של הסביבה, כך שהוא יכול להיות שונה לכל כרטיס. למשל לכרטיס Arduino MKRZero שמבוסס על מיקרובקר SAMD21, ה-LED_BUILTIN מוגדר כ-32. ל-MKR1000 זה 6.

תחשבו מה היה קורה אם היה צריך לפרסם מדריך שונה לכל כרטיס… זה היה מתסכל מאוד מתחילים בתחום שלרוב מעתיקים את הקוד מהמדריכים בלי להבין מה כתוב בו. במקרה שיש הגדרה כזו לכל כרטיס, הקוד עצמו שמשתמש בלד נשאר זהה לכל הכרטיסים.

הנה אנקדוטה: כש-SparkFun ייצרו את הכרטיסים שמבוססים על מיקרובקר SAMD21, הם זרמו עם קו 13 לטובת הלד כמו שזה קורה ברוב הכרטיסים, והגיעו למצב שלקו אליו מחובר הלד אין יכולות PWM, כך שאי אפשר לייצר אפקט של "לד נושם" בצורה פשוטה. זו היתה הסיבה שבכרטיס Plug-n-Play בחרתי בקו A4 לטובת הלד הפנימי.

הנה עוד משהו מעניין: מפתחי ארדואינו בכבודם ובעצמם חטאו עם מספרי קסם בתחילת דרכם. עד אמצע 2016 הקוד של דוגמת ה-blink (קישור לארכיון) כלל את ה-13 כמספר קסם ללד הפנימי. כנראה באותה התקופה הם הבינו שיהיו כרטיסים שהלד יהיה מחובר לקו אחר של מיקרובקר ושינו את הקוד. עדיף מאוחר מאשר אף פעם.

זו דוגמה קלאסית לכך שנתקלים בדברים שמפריעים להתקדמות של הפרוייקט ומצריכים שינויים. עדיף להתחיל מראש בכתיבה תקינה ונקיה כדי שלא תצטרכו להיתקל בבעיות כאלה.

מקרים מיוחדים

גם מספר 0 שמשתמשים בו לאתחול משתנים והשוואות הוא סוג של מספר קסם, אבל מספר שדי מקובל על כולם.

ראינו שערך הגדרת ה-LOW בסביבת ארדואינו הוא 0 וזו דוגמה טובה בה נתנו ל-0 משמעות לוגית ולא השתמשו בו כמספר קסם.

יש פרוייקטים או סביבות פיתוח בהן מגדירים גם את ה-TRUE וה-FALSE כ-1 ו-0 בהתאמה. אפשר להשתמש ב-true ו-false (באותיות קטנות) רק מגרסת C23 של שפת C, שפורסמה רשמית בסוף אוקטובר 2024. עד אז לא היתה ברירה אלא להוסיף הגדרות של TRUE ו-FALSE לפרוייקט.

רגילים להשתמש ב-NULL באותיות גדולות? גם זו הגדרה. זו השורה של ההגדרה בקבצים של הקומפיילר GCC במחשב שלי, שלמעשה שווה ערך ל-0:
קוד: בחר הכל
#define NULL __null


טיפ חשוב:
צריך להיזהר עם השוואה ל-TRUE. מכיוון שהערך שלו הוא 1, כל ערך אחר שמשווים אליו יתן תוצאה שלילית.

למשל:
קוד: בחר הכל
unsigned char flag;

flag = 4;
if (flag == TRUE) {
    // Will never happen!
}


במקום, תוכלו להשתמש בלוגיקה הפוכה כדי לבדוק שהערך הוא לא FALSE (אני פחות אוהב את הגישה הזו):
קוד: בחר הכל
unsigned char flag;

flag = 4;
if (flag != FALSE) {
    // Do something
}


או להוריד לגמרי את הערך אליו משווים את המשתנה, כך שהתנאי יתקיים כל עוד ערך ה-flag הוא לא אפס (יותר טוב מהגישה של לוגיקה הפוכה, אבל עדיין פחות מועדפת עלי):
קוד: בחר הכל
unsigned char flag;

flag = 4;
if (flag) {
    // Do something
}


איך בכלל מגיעים למצב שדגל שווה לערך 4?
נניח שיש לכם מפסק DIP של 8 מצבים, שאת כל 8 המפסקים אתם מקבלים בקריאה אחת ממיקרובקר, כך שכל מפסק מציין ביט אחד במשתנה בשם flag.
נניח שאתם צריכים לבצע משהו בקוד כשהמפסק השלישי במצב ON. הגישה המקובלת היא לעשות פעולה לוגית AND עם הביט שאתם רוצים לבדוק, למשל כך:
קוד: בחר הכל
flag = dipSwitch & 4;


או כך:
קוד: בחר הכל
flag = dipSwitch & 1<<2;


או בצורה הנכונה יותר בלי מספרי קסם! את כל ההגדרות בדרך כלל מרכזים בקובץ h. נפרד:
קוד: בחר הכל
#define IMPORTANT_BIT_MASK 1<<2

flag = dipSwitch & IMPORTANT_BIT_MASK;


בבינארי זה 0b00000100, כלומר ספרה 1 מוזזת שמאלה פעמיים (2>>1).
וכך משתנה flag יכול לקבל ערך 0 כשהמפסק השלישי כבוי או ערך 4 כשהמפסק דלוק.

הגישה שאני מעדיף היא להשלים את הפעולה בהזזה ימינה של משתנה ה-flag כדי שבמשתנה תמיד יהיה ערך בינארי, 0 (ה-FALSE) או 1 (ה-TRUE).
ועושים זאת כך:
קוד: בחר הכל
#define IMPORTANT_BIT_NUM 2
#define IMPORTANT_BIT_MASK (1<<IMPORTANT_BIT_NUM)

flag = (dipSwitch & IMPORTANT_BIT_MASK) >> IMPORTANT_BIT_NUM;


במצב כזה לא צריך לחשוב למה מותר או אסור להשוות את ערך משתנה flag. אם הקוד נראה הגיוני יותר כשמשווים את המשתנה ל-TRUE, אין שום בעיה לעשות את זה בגישה זו.

מדריכים נוספים בסדרת כותבים קוד יותר טוב:


ש לכם הערות או שאלות?
מוזמנים לשאול ולהגיב כתגובה לפוסט זה בפייסבוק.

תוויות:


מדריכים נוספים:

  • מדריך כותבים קוד יותר טוב - גיבוי וניהול גרסאות

    כותבים קוד יותר טוב - גיבוי וניהול גרסאות

    2025-08-03 22:23:38

    זוהי סדרה קצרה של מדריכים שיסבירו לכם איך לכתוב קוד שיהיה קצת יותר טוב ויעיל.
    הדגש הוא לא על הביצועים, אלא שיהיה קריא יותר, מובן לאחרים וגם לכם, לאחר פרק זמן כלשהו.
    פרק זה של המדריך פחות קשור לתכנות עצמו, אלא לניהול גרסאות ולגיבויים.

  • מדריך כותבים קוד יותר טוב - תיעוד

    כותבים קוד יותר טוב - תיעוד

    2025-08-02 13:49:37

    זוהי סדרה קצרה של מדריכים שיסבירו לכם איך לכתוב קוד שיהיה קצת יותר טוב ויעיל.
    הדגש הוא לא על הביצועים, אלא שיהיה קריא יותר, מובן לאחרים וגם לכם, לאחר פרק זמן כלשהו.
    מדריך זה הוא על תיעוד הקוד שאתם כותבים.

  • מדריך עבודה עם VSCode+PlatformIO

    עבודה עם VSCode+PlatformIO

    2025-05-05 14:42:01

    במדריך זה נראה איך מתקינים PlatformIO בתוך VSCode. נוסיף כמה פרוייקטים לכרטיס Arduino, נוסיף ספריות ונראה לכם איך עובדים עם זה.

  • מדריך מצחיק שאתה צריך לשאול - מה זה SDA ו-SCL?

    מצחיק שאתה צריך לשאול - מה זה SDA ו-SCL?

    2025-01-28 09:14:27

    ידידינו מ-SparkFun מפרסמים סדרה של סרטונים קצרים בנושאי האלקטרוניקה השונים, שהם קוראים לה "מצחיק שאתה צריך לשאול"...
    השאלה בסרטון זה היא: מה זה SDA ו-SCL?

  • מדריך מצחיק שאתה צריך לשאול - מה זה TX ו-RX?

    מצחיק שאתה צריך לשאול - מה זה TX ו-RX?

    2025-01-27 09:12:28

    ידידינו מ-SparkFun מפרסמים סדרה של סרטונים קצרים בנושאי האלקטרוניקה השונים, שהם קוראים לה "מצחיק שאתה צריך לשאול"...
    השאלה בסרטון זה היא: מה זה TX ו-RX?