כבר הרבה שנים חלפו מאז הומצא הרעיון של "תכנות מונחה עצמים", ושפות תכנות רבות החליטו לאמץ את הגישה, לדחות אותה או לאמץ חלקים מסויימים ממנה ולהשאיר אחרים בחוץ. ועדיין, נדמה שהמוסכמה המדברת על "שלושת עקרונות ה-OOP" לא השתנתה כבר כמה עשורים, והיא שרירה וקיימת בקורסים השונים, בספרים ואף בראיונות עבודה. השלושה הם, כמובן, encapsulation (או בעברית "הכמסה" — מי ששמע אי פעם את המילה הזו בעבודת פיתוח אמיתית ולא בקורס של האו"פ, שירים את היד!), inheritance ("הורשה") ו-polymorphism ("רב-צורתיות"). בדרך כלל גם עוברים עליהם על פי הסדר הזה, על-אף שההגיון שעמד מאחוריו בעבר כבר שנים רבות אינו מחייב.
ובכל זאת, Object Oriented Programming הביא איתו בשורה שהיא בעיני חשובה אפילו יותר: ה-constructor, הידוע גם בשם החיבה שלו "ctor" ובשמו העברי (המתחרה עם "הכמסה") — "פונקצית הבנאי".
יותר חשובה? באמת?
כן, לחלוטין; לפחות מבחינת שפת התכנות.
את הסיבה למיקום הזה במדרג אסביר עוד מעט. אבל לפני כן, קצת על מה היא בכלל נותנת לנו.
פונקציית Constructor
כמו שמן הסתם כל מי שקורא את זה יודע, פונקציית ctor מייצרת אובייקט מטיפוס מסויים. ה-ctor יכול לייצר אובייקט "בסיסי" שמכיל את ערכי ברירת המחדל עבור כל השדות של האובייקט. למשל, יהיה סביר לראות קוד כזה:
RgbColor color = new RgbColor();
המייצר אובייקט בשם color אשר שלושת הרכיבים/שדות שלו — red, green, blue — מאותחלים לאפס, מה שנותן את צבע ברירת המחדל, שחור. ניתן לחשוב על ctor נוסף לאותו טיפוס, למשל, קוד בסגנון הזה:
RgbColor color = new RgbColor(50, 100, 150);
אשר ייצור, בהתאמה, אובייקט שבו הרכיב האדום יקבל את הערך 50, הרכיב הירוק את הערך 100 והרכיב הכחול את הערך 150. וניתן גם להגדיר constructors יותר מתוחכמים, למשל, כזה שנקרא עם ערך בודד:
RgbColor color = new RgbColor(128);
ויוצר אובייקט שבו שלושת הרכיבים מקבלים את אותו הערך, כך שמתקבל גוון אפור, או כאלו שמקבלים טיפוסים אחרים, נניח מחרוזת, כך שהקריאה:
RgbColor color = new RgbColor("baby-blue");
מייצרת אובייקט עם ערכים קבועים מראש בהתאם לשם הצבע. ישנם טיפוסים שעבורם ברירת המחדל מספקת לגמרי; אחרים דורשים הכנסה של ערכים כאלו ואחרים; וישנם אלו שבאים עם אוסף מרשים של פונקציות ctor המאפשרות בניית אובייקטים בדרכים רבות ושונות.
ומה זה נותן לנו?
מפתחים רבים מתייחסים, לפיכך, ל-constructors כאל סוג של "Syntactic Sugar": רכיב בשפה שחוסך לנו הקלדה של כמה אותיות ויכול להפוך את הקוד שלנו ליותר אלגנטי. כך, למשל, את
RgbColor(128)
ניתן להחליף ב-
RgbColor(128, 128, 128)
ואת
RgbColor("baby-blue")
אפשר להחליף בערכים המייצגים את הצבע #89cff0 או להשתמש בפונקציית עזר שמחזירה את הרכיבים בהתאם לשם הצבע. למעשה, גם את השורה
RgbColor color = new RgbColor(50, 100, 150);
ניתן בעצם להחליף במשהו כמו:
RgbColor color = new RgbColor(); color.red = 50; color.green = 100; color.blue = 150;
או, אם הקפדנו שלא לאפשר גישה ישירה לשדות עצמם ("הכמסה" או לא?!), במשהו כזה:
RgbColor color = new RgbColor(); color.setRed(50); color.setGreen(100); color.setBlue(150);
כמו שאמרנו: syntactic sugar.
ביג דיל.
בשביל זה לבזבז פוסט שלם?
או, שאולי, זה יותר מסתם syntactic sugar?
אובייקט vs אוסף נתונים
למעשה, פונקציית ה-ctor היא אחד הדברים החשובים ביותר המבדילים בין אוסף של נתונים לבין יישות עצמאית שניתן לכנות "אובייקט". אחד הדברים החשובים בהגדרת טיפוס הוא קביעת הכללים אשר להם כל האובייקטים מטיפוס זה מצייתים — ה-invariant של הטיפוס או המחלקה. עבור מחלקה כמו RgbColor, ייתכן שאין יותר מדי כללים: כל אחד משלושת הרכיבים צריך להיות בין הערכים 0–255. אם הטיפוס שמגדיר את הערך עבור כל אחד מהרכיבים הוא של בית אחד, אז אפילו אם מאוד נשתדל — לא ניתן לייצר אובייקט בעל ערכים המנוגדים לכללים.
אבל מה יקרה כשננסה ליצור אובייקטים ממחלקה שיש לה כללים קצת יותר נוקשים?
למשל, נניח שיש לנו מחלקה בשם Triangle המייצגת משולש. גם כאן, בסופו של דבר, כל אובייקט מטיפוס Triangle מכיל שלושה ערכים, אחד עבור כל צלע. לכן גם כאן, ניתן לחשוב על בניית אובייקטים עם ctor המקבל שלושה ערכים, למשל —
Triangle triangle = new Triangle(30, 40, 50);
או אולי להשתמש ב-constructors נוספים כקיצורי דרך, למשל, לייצר משולש שווה צלעות באמצעות ערך אחד:
Triangle equilateral = new Triangle(100);
בניגוד לצבעים, סביר להניח שטווח הערכים לצלעות משולש לא יהיה מטיפוס uint8 או unsigned char, אלא מטיפוס float או double ודומיהם. מצד שני, לערכי הצלעות של משולש ישנה הגבלה קריטית: לא כל שלושה ערכים יכולים לייצר משולש. כפי שקובע חוק "אי-שוויון המשולש", סכום כל שתי צלעות חייב להיות גדול מן הצלע השלישית על מנת שניתן יהיה לבנות משולש מן הצלעות הללו. הערכים 100, 30, 20, למשל, אינם יכולים להיות צלעותיו של משולש אויקלידי. אנחנו יכולים, כמובן, לנסות ולוותר על שימוש ב-ctor, כמו שהצענו לגבי RgbColor:
Triangle triangle = new Triangle(); triangle.setA(30); triangle.setB(40); triangle.setC(50);
ממש כמו קודם, נקבל משולש בעל הצלעות 30, 40, 50. אלא שבדרך זו אנחנו עלולים גם לייצר משולש כזה:
Triangle triangle = new Triangle(); triangle.setA(20); triangle.setB(30); triangle.setC(100);
אובייקט כזה הוא אובייקט לא חוקי, או invalid.
כיצד לנהוג בערכים לא-חוקיים?
כמו שראינו, השלשה 20, 30, 100 אינה יכולה להיות משולש. אבל לא מדובר רק בעניין לוגי. כל פונקציה או מתודה שננסה להפעיל על אובייקט מסוג משולש, צריכה להחליט כיצד לנהוג במקרה כזה. נניח, למשל, שאנחנו רוצים לצייר את המשולש או לחשב את שטחו:
x = triangle.getArea(); draw(triangle);
לפונקציות הללו ישנם "תנאים מקדימים" – preconditions, אשר הארגומנט שלהם חייב לקיים על מנת שניתן יהיה לבצע אותן.
אפשרות אחת היא שהפונקציות יוודאו קודם כל שאי-שוויון המשולש מתקיים. אם זה אינו המצב, הפונקציה כלל לא תבוצע, אלא תחזיר ערך שגיאה / תזרוק exception / תעצור את התכנית עם assertion. רק אם הבדיקה עברה בהצלחה, הפונקציה תמשיך לחלק בו היא מבצעת את מטרתה. אפשרות שניה היא להניח שהאחריות על תקינות הקלט היא של הקוד הקורא לפונקציות ולא של הפונקציות עצמן. במקרה כזה, לא תבוצע כל בדיקה בתוך הפונקציה.
דרך א': וידוא נתוני Pre-conditions
הדרך הראשונה היא בעייתית מאוד, מכמה סיבות:
- היא מבזבזת זמן יקר על כל קריאה: אם נרצה לצייר את המשולש מאה פעמים, נצטרך לבדוק מאה פעמים את תקינותו.
- היא מחייבת להפעיל בדיקות מסוג זה על חלק גדול מאוד מה-API שמחצין האובייקט — מלבד ()getArea יכולות להיות עוד מתודות רבות שדורשות את הבדיקה, מה שידרוש הרבה code duplication.
- סיכוי גבוה שבחלק מהמתודות המפתחים ישכחו לבצע את הבדיקה.
- המתודה פוגעת בעקרון האחריות היחידה, וגם הקריאות (readability) שלה נפגעת בשל כך.
- ומה לגבי מתודות הקוראות למתודות אחרות? במקרה כזה יתבצעו מספר בדיקות תקינות ללא כל צורך.
- עד כאן דיברנו על מתודות – אבל איך בכלל ניתן לבצע את זה בפונקציות חיצוניות? הפונקציה draw, למשל, עשוייה לקבל לאו דווקא משולש אלא כל צורה שהיא, באופן פולימורפי. לפיכך, גם את בדיקת התקינות עליה לבצע באופן פולימורפי.
דרך ב': ללא וידוא Pre-conditions
הדרך השניה בעייתית לא פחות: היא מטילה את כל האחריות על מי שקורא לפונקציה/מתודה, וזה, כמו שאנחנו יודעים, אף פעם לא רעיון טוב. אפשר להניח די בוודאות שיהיה מי שיקרא לפונקציה הזו על סט ערכים לא מתאים מבלי לבדוק. ומה יקרה אז? אי אפשר באמת לדעת. בשפות קיצוניות כמו C++ אפשר לצפות לכל Undefined Behavior, בהתאם לקוד שמריצה הפונקציה. בשפות קצת יותר מוגנות אולי אפשר לקוות שזה יסתיים ב-exception, אבל גם זה לגמרי לא מובטח. אם לא מדובר בציור משולשים אלא בשינויים ב-DB, למשל, התוצאות של פעולה על אוסף ערכים שאינם מהווים אובייקט תקין יכולות להיות הרות אסון.
הפתרון: לעולם אין לייצר בעייה
אומרים ש"חכם אינו נכנס לבעיות שפיקח יודע לצאת מהן". הפתרון, אם כן, הוא להיות חכם. אם נכתוב מערכת, שבה כל משולש הוא באמת משולש, אז כל פונקציה שפועלת על משולשים יכולה לעשות את מלאכתה ללא חשש: אין צורך בבדיקת ה-preconditions, וגם אין חשש ממצבים לא צפויים. הדרך לכך דורשת שני תנאים פשוטים:
- הדרך היחידה לייצר אובייקט מסוג זה היא באמצעות ctor (או פונקציה אחרת שמשמשת בתפקיד זה), אשר לעולם לא יחזיר אובייקט לא תקין
- חייב להתקיים אחד מאלו:
- או שלא ניתן לשנות את האובייקט כלל לאחר שנוצר
- או שכל שינוי מתאפשר אך ורק באמצעות פונקציות מתאימות, המוודאות את שמירת ה-invariant של האובייקט
אם האובייקט שלנו שומר על שני הכללים הללו, אנחנו יכולים לכתוב בבטחה פונקציות כמו ()draw או ()getArea, בידיעה שהן תמיד תעבודנה ללא חשש כיוון שהאובייקט שהן מקבלות כארגומנט הוא בוודאות אובייקט תקין השומר על הכללים של הטיפוס Triangle.
רגע, זה בסך הכל Encapsulation! מה ה"ביג דיל"?
אם אתם שקועים עמוק בתוך עולמות הדיזיין, אולי אתם מתקשים להבין על מה המהומה. הרי זה בסך הכל שימוש נכון בעקרון ה-encapsulation. למה צריך בכלל לייחד לזה פוסט? התשובה היא, שזה לא ברור מאליו לכל אחד. למשל, רבים בקהילת ה-C++, בעיקר "old school guys" שהגיעו לשפה הזו לאחר שנים רבות של עבודה בשפת C, אוהבים לייצר אובייקטים שבהם מתודה (או member function, בטרמינולוגיה של השפה) בשם ()init. הנטיה של הגישה הזו היא לייצר הפרדה בין ה-constructor, שתפקידו "לייצר" את האובייקט, לבין פונקצית האתחול שלו. התפיסה שעומדת מאחורי הגישה הזו היא ש"לייצר" אובייקט זו פעולה שצריכה להיות פשוטה — כמו שמייצרים אוסף של נתונים. אתחול, לעומת זאת, זו פעולה מורכבת עם השלכות רבות, ומי שמגיע משפות פרוצדורליות עלול להתחלחל מהרעיון שזה "יקרה מעצמו", ללא קריאה מפורשת ומסודרת.
שימוש בפונקציית ()init
נניח, למשל, שיש לנו אובייקטים מסוג Configuration, שאותם אנו מייצרים מתוך קובץ כלשהו ולאחר מכן משתמשים במידע שהם מחזיקים כדי לקנפג את המערכת. בתפיסה הנאיבית, ועל פי עקרון האחריות היחידה, ניתן לחשוב על קוד שייראה בערך כך:
Configuration conf; if (filename) conf.init(filename); else conf.init(); // Use defaults /* ... anything here ...*/ setMaxConns(conf);
בעצם, אתחול בעזרת שם קובץ לא בוודאות יצליח: יכול להיות שהקובץ לא קיים. יכול להיות שאין לנו את ההרשאות המתאימות. יכול להיות שהקובץ לא בפורמט הנכון. יכול להיות שיש בו נתונים לא חוקיים. זו בדיוק הסיבה שמנחה את ה"הגיון" בשיטה הזו לבצע זאת בפונקציה/מתודה עצמאית. למעשה, כיוון שהאתחול עלול להכשל, יש צורך להוסיף כאן קוד של טיפול בשגיאות – או שהפונקציה תחזיר ערך שגיאה, או שהיא תזרוק exception (קוראי הבלוג מכירים את עמדתי בנושא). ולכן, תיאורטית, קוד כזה אמור להיראות בערך כך:
Configuration conf; try { if (filename) conf.init(filename); // Init conf's vals from the file else conf.init(); // Init conf's vals to the hard-coded defaults } catch (/* relevant exception types come here */) { /* relevant error handling comes here */ } /* ... anything here ...*/ setMaxConns(conf);
אז הנה לנו קוד, ששומר, לכאורה, על –
- עקרון ה-encapsulation: המבנה הפנימי של אובייקט Configuration אינו ידוע לנו, והדרך היחידה לגשת אליו הוא באמצעות API שהטיפוס מחצין.
- עקרון האחריות היחידה: הפרדנו "יצירת" אובייקט קונפיגורציה מ"אתחול" אובייקט כזה.
לרוע המזל, קוד מהסוג הזה, שמופיע בהרבה מאוד מערכות, הוא קוד מסוכן מאוד. הסיבה היא פשוטה: לאחר סיום ביצוע הקוד המוצג כאן, קיים אובייקט בשם conf מטיפוס Configuration, שבו אנחנו משתמשים בפונקציה אחרת – ()setMaxConns — שמניחה שהערך שהיא מקבלת הוא תקין. אבל, תחת סיטואציות מסויימות, ייתכן שהערכים של conf יהיו לא תקינים! נכון, אנחנו מצפים שהקוד המטפל בשגיאות ידאג לעשות את מה שצריך, והסיטואציה לא תתרחש. אבל אין לנו שום שליטה על הקוד הזה! זהו קוד שלא אנחנו כתבנו: אנחנו אחראים אך ורק על הקוד של המחלקה Configuration. למען האמת, מפתחים אפילו פחות זהירים עלולים פשוט לכתוב את שתי השורות הללו, כפי שהן, ללא כל הבנה של השלבים הנדרשים בדרך ליצירת אובייקט תקין:
Configuration conf; setMaxConns(conf);
התוצאה היא שניתן להשתמש באובייקט מסוג Configuration שאינו שומר על ה-Invariant שמצופה ממנו, וכך לגרום לבעיות קשות במערכת.
שימוש בפונקציית Ctor
הבעייה שראינו בסעיף הקודם תיעלם מעצמה אם נשמור על הכללים אותם הזכרנו קודם: נאפשר יצירת אובייקט Configuration אך ורק באמצעות פונקצית ctor שמוודאת את תקינותו, ולא נאפשר שינוי (או לפחות שינוי לא חוקי) שלו בשום דרך בהמשך. אם שמרנו על כך, אין לנו שום חשש מקריאה לפונקציות כמו (setMaxConns(conf, כיוון שאין שום אפשרות ש-conf יכיל ערכים שאינם חוקיים. שורה כמו
Configuration conf;
או שלא תתקמפל (אם החלטנו שלא לאפשר default ctor) או שבוודאות תייצר אובייקט תקין עם ערכי ברירת המחדל. לעומתה, שורה כמו
Configuration conf(filename);
גם היא לא תייצר לעולם אובייקט לא תקין, אם נוודא שבמקרה של תקלה בקריאת הקובץ, לא נייצר אובייקט כלל (למשל, נזרוק exception) או נייצר אובייקט העומד בכללים (זוהי האפשרות הפחות מוצלחת מבין השתיים, כיוון שהיא מקיימת רק את ה-basic guarantee, אך ייתכנו מקרים בהם היא תתאים). כעת, אם סיפקנו ספרייה ובה המחלקה Configuration, אנחנו יכולים להיות בטוחים שכל קריאה לפונקציות שלה תתבצע תמיד אך ורק על אובייקטים תקינים.
ומה השפה נותנת לנו?
כמו שכתבתי בתחילת הפוסט, ישנם מספר עקרונות לתכנות מונחה עצמים, ובכל זאת אני מתייחס לפונקציות Ctor כאל חשובות בפני עצמן, ואף יותר מאשר שלושת העקרונות ה"מקובלים" (על אף שאכן, ניתן להתייחס לעקרון זה כאל תת-עקרון של encapsulation). הסיבה היא, שבנקודה הזו, ישנה חשיבות רבה לתמיכה של שפת התכנות בעקרון הזה.
ניתן, למשל, ליישם את עקרונות ה-OOP גם בשפה פרוצדורלית כמו שפת C. לא אכנס כאן לכל פרטי היישום. בגדול, ניתן לשמור על שלושת העקרונות בצורה פחות או יותר כזו:
- הכמסה: נשמור את כל הנתונים הקשורים ביניהם בטיפוס A המורכב ממבנה נתונים יחיד (נניח struct), ונאפשר שינוי שלו רק באמצעות פונקציות המקבלות מצביע לטיפוס הזה.
- הורשה: ניתן לבנות טיפוס חדש B, שהוא struct אשר אחד משדותיו הוא הטיפוס ה"קודם", A — וכך לייצר יחס דומה מאוד להורשה.
- פולימורפיזם: ניתן לשמור בתוך האובייקטים (מטיפוסים A, B או אחרים) מצביעים לפונקציות, ובמקום לקרוא לפונקציה כלשהי על אובייקט, להשתמש במצביע שהאובייקט שומר כך שהפונקציה שתתבצע בפועל תהיה שונה עבור כל אובייקט. בשפות שבהן פונקציות הן "First-class citizens", ניתן לשמור את הפונקציה עצמה במקום מצביע.
המערכת שתיארנו שומרת על כל שלושת כללי ה-OOP. אבל, יש בה עוקץ:
אם נגדיר אובייקט מטיפוס A, אנחנו בעצם מגדירים struct מטיפוס A. השפה תאפשר גישה פשוטה לכל אחד מהשדות של האובייקט הזה. במילים אחרות: לא הצלחנו באמת לממש את עקרון ה-encapsulation. אם אנחנו רוצים encapsulation אמיתי, אסור לנו לאפשר את זה. שפה כמו C דווקא מאפשרת לנו לבצע את זה: אם נחשוף רק את ההצהרה (declaration) של הטיפוס A, אבל לא את ההגדרה (definition) שלו, לא יהיה אפשרי לייצר משתנה מטיפוס A ללא שימוש בפונקצייה מיוחדת שנספק. הפונקציה הזו היא בעצם ה-constructor של A. אבל, במקרה כזה — ניתן יהיה להגדיר משתנה מטיפוס A אך ורק על ה-heap, ולא על ה-stack. למי שמגיע משפות עיליות יותר אולי זה לא אומר הרבה, אבל יש לזה מספר השלכות, וביניהן לפחות שתיים קריטיות:
- אם טיפוס A מכיל מעט מאוד מידע, ויש לנו הרבה מאוד "אובייקטים" מסוג זה — נקבל בזבוז זכרון שקשה לדמיין.
- כל יצירה או שחרור של "אובייקט" מטיפוס A יכולה לקחת זמן לא ידוע, שתלוי במצב ה-heap.
זהו הטרייד-אוף שלנו: ביצועים מול אמינות.
במערכות זמן אמת (בעיקר hard real-time), למשל, בדרך כלל אסור כמעט לחלוטין להשתמש ב-heap. על מנת שמערכות מסוג זה יפעלו כראוי, יש צורך לוודא כי משך הזמן של כל פעולה (למשל, קריאה לפונקציה) אינו חורג מערך ידוע וקבוע מראש. דרישה כזו אינה מאפשרת לייצר פתרון מהסוג שהראינו כאן.
בשפות בהן ישנה תמיכה מובנית בעקרון של ctor, לעומת זאת, אנו יכולים ליהנות משני העולמות: מצד אחד, יצירה של אובייקט מסוג A בזמן קבוע או אפילו באפס זמן, ומצד שני, ודאות שכל אובייקט מסוג A שניתן לגשת אליו בקוד שומר בוודאות על הכללים ואינו חורג מן ה-Invariant של A.
מה צופן העתיד לקונסטרקטורים?
מעניין לראות כיצד מתייחסות לעניין שתי שפות מודרניות שמצהירות כי תכליתן לתמוך גם בביצועים וגם באמינות — שפת Go ושפת Rust. שתיהן, אגב, בעלות מודל שונה בצורה משמעותית ממודל ה-OOP הקלאסי של C++ (ובנות הדודה שלה, כמו Java או C#) שאותה הן מתיימרות לרשת.
בשפת Rust, פונקציית Ctor נראית אחרת לגמרי מבחינה סינטקטית, מכפי שהיא נראית במודל ה-OOP הקלאסי. ובכל זאת, מבחינה סמנטית, העקרון של יצירת משתנים של טיפוסים שונים אך ורק באמצעות פונקציות בנייה יעודיות מוטמע עמוק לתוך תרבות הכתיבה של Rust וזוכה לתמיכה של השפה והקומפיילר. אמנם אין בשפה Exceptions, אך דרך הטיפול הייחודית שלה בשגיאות מוודאת כי במקרה של שגיאה בתהליך הבנייה, לא יוחזר כל אובייקט, וממילא, אם הפונקציה אכן הצליחה לבנות את האובייקט — תוך שמירה על העקרונות שהעלינו בפוסט — ניתן להשתמש בו בבטחה בכל פונקציה מבלי לחשוש שהוא אינו ולידי. השפה אכן מאפשרת, אם כן, גם את המהירות וגם את האמינות שמספקת הנדסה נכונה של פונקציות ctor.
בשפת Go, לעומת זאת, המצב בדיוק הפוך, משתי הבחינות. ראשית, ניתן לייצר אובייקט מכל טיפוס שהוא ללא צורך בכל פונקציה שהיא, והוא יקבל באופן אוטומטי את "ערכי האפס" עבור הטיפוס. ואם ערכי האפס אינם חוקיים (כמו, נניח, בטיפוס מסוג Triangle) — ובכן, זה כלל לא רלוונטי. עדיין ניתן לייצר בקלות אובייקט אינוולידי. יתרה מזו, גישת הטיפול בשגיאות של Go אינה מבטיחה בשום דרך שכשלון בפעולתה של פונקציה כלשהי — בין אם ביצירה של אובייקט או בשינוי שלו — תמנע מהאובייקט הבעייתי להתקיים ב-scope הרלוונטי. לפיכך, האחריות מוטלת על מי שישתמש בטיפוסים שהגדרנו, ולא נוכל להיעזר בכוח של השפה או הקומפיילר כדי למנוע זאת. (כמו בשפת C, ניתן לייצר מבנים מורכבים יותר — אך ללא תמיכת הקומפיילר, שוב נמצא את עצמנו בטרייד אוף בין ביצועים לבין אמינות).
אלו דוגמאות מועטות, אך מאוד מייצגות, של ההבדלים בין Rust ל-Go — אבל על כך כבר ארחיב בפוסט אחר.
Ctors also enable RAII which is a hugely important concept in real life apps
Indeed, Sagiv. You are totally right.
There's much more to know on ctors than I could summarize in a single post.
But even more important for RAII are ctors' best friends – the dtors.
I believe that they deserve a post of their own. RAII is indeed a key pattern for reliable programming in many languages.