בנאי (constructor) הוא הקוד שרץ כשיוצרים אובייקט עם new. יש לו אותו שם כמו למחלקה, אין לו טיפוס החזרה, והתפקיד שלו הוא להביא את האובייקט החדש למצב תקין לפני שמישהו משתמש בו.
פלט:
Mug: 8.50
Rejected: price
מכיוון שהבנאי דוחה מחיר שלילי, לא יכול להתקיים Product עם מחיר שלילי. ההבטחה הזו היא הסיבה העיקרית לכתוב בנאים במקום להציב שדות מבחוץ.
בנאי ברירת המחדל, ומתי הוא נעלם
אם מחלקה לא מצהירה על אף בנאי, הקומפיילר מספק בנאי public בלי פרמטרים שלא עושה כלום מעבר להרצת ה-field initializers. זו הסיבה ש-new BankAccount() עובד על מחלקה שיש בה רק שדות.
ברגע שמצהירים על בנאי כלשהו, הבנאי המרומז כבר לא נוצר:
class Product
{
public string Name;
public Product(string name) { Name = name; }
}
var p = new Product(); // error CS7036: There is no argument given that corresponds to the required parameter 'name'
זה מפתיע אנשים כש-serializer, ORM או אילוץ גנרי new() צריכים בנאי בלי פרמטרים. אם אתם רוצים את שניהם, הצהירו בעצמכם על הבנאי בלי הפרמטרים:
public Product() { } // or
public Product() : this("Unnamed") { } // delegate to the other one (next section)
בנאי יכול להיות public, internal, protected או private. רמת הגישה שלו קובעת מי רשאי ליצור איתו אובייקטים.
העמסה ושרשור עם this(...)
למחלקה יכולים להיות כמה בנאים עם רשימות פרמטרים שונות. כדי לא להעתיק את אותן הצבות לכל אחד מהם, שרשרו אותם: : this(...) קורא לבנאי אחר של אותה מחלקה לפני שהגוף הנוכחי רץ.
פלט:
new Pizza():
full constructor: medium, classic, 0
size-only constructor body
parameterless constructor body
Result: medium classic with 0 toppings
השרשרת מריצה קודם את הבנאי השלם ביותר ומתגלגלת חזרה לזה שקראתם לו. שמרו את העבודה האמיתית (אימות, הצבות) בבנאי אחד, ותנו לאחרים רק לספק ערכי ברירת מחדל.
פרמטרים אופציונליים הם חלופה: public Pizza(string size = "medium", string crust = "classic", int toppings = 0) נותן בנאי אחד שמכסה את שלוש הקריאות. שרשור עדיין מתאים יותר כשהבנאים הקצרים צריכים לחשב משהו, או כששינוי של ערך ברירת מחדל לא אמור לחייב קומפילציה מחדש של הקוד הקורא (ערכי ברירת המחדל של פרמטרים אופציונליים מועתקים לקוד הקורא בזמן קומפילציה).
קריאה לבנאי של מחלקת הבסיס עם base(...)
מחלקה יורשת לא יורשת בנאים. כל בנאי של המחלקה היורשת חייב להריץ קודם בנאי של מחלקת הבסיס. אם לא כותבים כלום, הקומפיילר מוסיף קריאה לבנאי בלי הפרמטרים של הבסיס; אם אין לבסיס כזה, חייבים לבחור אחד עם : base(...).
כדאי לראות פעם אחת את הסדר שבו הכול רץ:
פלט:
Truck field initializer
Vehicle field initializer
Vehicle constructor body
Truck constructor body
KL-204, 3 axles
ה-field initializers רצים ראשונים, של המחלקה היורשת לפני של מחלקת הבסיס, ואז גופי הבנאים רצים מהבסיס כלפי מטה. כך שכשהגוף של Truck רץ, Plate כבר מוצב. המלכודת היחידה בסדר הזה: אם בנאי הבסיס קורא למתודה virtual ש-Truck דורסת, הדריסה רצה לפני גוף הבנאי של Truck, ורואה את Axles עדיין ב-0. הימנעו מקריאה למתודות virtual מתוך בנאים.
בנאים סטטיים
בנאי סטטי מאתחל את הטיפוס ולא אובייקט. אין לו פרמטרים ואין לו מגדיר גישה, וסביבת הריצה קוראת לו בדיוק פעם אחת, רגע לפני השימוש הראשון בטיפוס.
פלט:
Program started
Loading tax table (runs once)
123.00
119.00
סביבת הריצה הופכת את הבנאי הסטטי ל-thread-safe: גם אם כמה threads נוגעים בטיפוס בבת אחת, הוא רץ פעם אחת. אם הוא זורק, כל שימוש מאוחר יותר בטיפוס זורק TypeInitializationException לכל חיי התהליך, אז שמרו אותו פשוט ונקי מ-I/O שעלול להיכשל.
בנאי מול object initializer
object initializer, new Pizza("large") { Toppings = 3 }, הוא לא בנאי שני. הקומפיילר הופך אותו ל"הרץ את הבנאי, ואז הצב את האיברים האלה". השתמשו בכל אחד במה שהוא טוב בו:
- פרמטרים של הבנאי לערכים שבלעדיהם האובייקט לא יכול להיות תקין. כל קוד קורא חייב להעביר אותם, והבנאי יכול לבדוק אותם.
- Initializer להגדרות אופציונליות עם ערכי ברירת מחדל הגיוניים.
var order = new Order(customerId: 42) { Note = "Leave at the door", GiftWrap = true };
מאז C# 11, מאפיין שמסומן required חייב להיות מוצב ב-initializer, וזה נותן לתחביר ה-initializer חלק מההבטחות של בנאי. זה מוסבר בדף על מאפיינים.
בנאים פרטיים
בנאי private פירושו שרק המחלקה עצמה יכולה ליצור מופעים. יש שתי תבניות שמשתמשות בזה. מתודת factory סטטית, שבה המחלקה שולטת ביצירה ויכולה להחזיר אובייקט שמור במטמון או null, ו-singleton:
פלט:
21
100
שגיאת הקומפילציה עבור new Temperature(5m) מחוץ למחלקה היא CS0122, אותה שגיאה ש-מגדירי גישה מייצרים עבור כל איבר private. מתודות factory עם שמות פותרות מגבלה אמיתית: שני בנאים לא יכולים לקבל שניהם decimal אחד, אבל FromCelsius ו-FromFahrenheit כן. מחלקה שיש בה רק איברים סטטיים (מחלקת עזר) צריכה להיות מוצהרת static במקום להסתיר את הבנאי שלה.
בנאים עם expression body
בנאי שהגוף שלו הוא פקודה אחת יכול להשתמש ב-=>:
public Product(string name) => Name = name;
עם tuples אפשר אפילו להציב כמה שדות בשורה אחת: public Point(int x, int y) => (X, Y) = (x, y);.
Primary constructors (C# 12)
C# 12 מאפשרת ל-class או ל-struct להצהיר על הפרמטרים של הבנאי שלהם על הטיפוס עצמו. הפרמטרים נמצאים ב-scope בכל איבר:
public class Customer(string name, int id)
{
public string Name { get; } = name; // copy into a property
public string Label => $"#{id} {Name}"; // or read a parameter directly
public Customer(string name) : this(name, 0) { } // other constructors must chain to it
}
שני הבדלים מ-records מפתיעים אנשים. הפרמטרים של primary constructor במחלקה לא הופכים למאפיינים public; אתם חושפים אותם בעצמכם. ופרמטר שמשמש בתוך איבר נלכד לשדה נסתר שנשאר ניתן לשינוי, כך ש-name = "x"; בתוך מתודה עובר קומפילציה. primary constructors מתאימים במיוחד ל-dependency injection, שבו הפרמטרים הם שירותים שהמחלקה רק קוראת להם:
public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
public Order Get(int id) => repo.Find(id);
}
טעויות נפוצות
- איבוד הבנאי בלי הפרמטרים. הוספת בנאי עם פרמטרים מסירה את הבנאי המרומז (CS7036 בכל
new X()). - שכחת
: base(...)כשלמחלקת הבסיס אין בנאי בלי פרמטרים. הקומפיילר לא יכול להוסיף את הקריאה המרומזת ומדווח שאין ארגומנט לפרמטר הנדרש של בנאי הבסיס. - קריאה למתודות virtual בבנאי. הדריסה רצה לפני גוף הבנאי של המחלקה היורשת.
- עבודה כבדה בבנאי. קריאות רשת, קריאת קבצים או כל דבר איטי הופכים אובייקטים ליקרים וחריגות לקשות לטיפול. השתמשו במתודת factory סטטית, או במתודת אתחול אסינכרונית, לעבודה כזו.
- כתיבת טיפוס החזרה.
public void Product()היא מתודה רגילה בשםProduct, והקומפיילר דוחה אותה כי לאיבר אסור שיהיה אותו שם כמו לטיפוס שמכיל אותו (CS0542).
שאלות נפוצות
מהו בנאי ב-C#?
בנאי הוא מתודה מיוחדת שרצה כשיוצרים אובייקט עם new. יש לו אותו שם כמו למחלקה ואין לו טיפוס החזרה: public Product(string name) { Name = name; }. התפקיד שלו הוא להשאיר את האובייקט החדש במצב תקין, בדרך כלל על ידי הצבת שדות מהפרמטרים שלו ודחיית קלט לא תקין.
האם C# יוצרת בנאי ברירת מחדל אוטומטית?
רק כשהמחלקה לא מצהירה על אף בנאי. אז הקומפיילר מוסיף בנאי public בלי פרמטרים שמשאיר כל שדה בערך ה-initializer שלו או בערך ברירת המחדל. ברגע שכותבים בנאי כלשהו, למשל כזה שמקבל שם, הבנאי המרומז נעלם ו-new Product() מפסיק לעבור קומפילציה (שגיאה CS7036). הצהירו בעצמכם על public Product() { } אם אתם עדיין צריכים אותו.
איך קוראים מבנאי אחד לבנאי אחר ב-C#?
השתמשו ב-: this(...) אחרי רשימת הפרמטרים של הבנאי: public Product(string name) : this(name, 0m) { }. בנאי היעד רץ קודם, ואז הגוף של הבנאי שקראתם לו. כדי לקרוא לבנאי של מחלקת הבסיס, השתמשו ב-: base(...) באותה דרך.
מהו בנאי סטטי ב-C#?
בנאי שמסומן static, בלי פרמטרים ובלי מגדיר גישה, שרץ פעם אחת לכל טיפוס לפני שנוצר המופע הראשון או לפני השימוש הראשון באיבר סטטי. משתמשים בו כדי לאתחל שדות סטטיים שצריכים יותר מ-initializer של שורה אחת. אי אפשר לקרוא לו בעצמכם, ואם הוא זורק, הטיפוס הופך לבלתי שמיש לכל שאר התוכנית (TypeInitializationException).
כדאי להשתמש בבנאי או ב-object initializer?
השתמשו בפרמטרים של הבנאי לערכים שבלעדיהם האובייקט לא יכול להיות תקין, כי הקומפיילר מכריח כל קוד קורא להעביר אותם והבנאי יכול לבדוק אותם. השתמשו ב-object initializer (new Product("Mug") { Color = "blue" }) להגדרות אופציונליות. ה-initializer רץ אחרי שהבנאי מסתיים.
מה הם primary constructors ב-C# 12?
פרמטרים שנכתבים על הצהרת המחלקה עצמה, class Customer(string name, int id) { ... }, וזמינים לכל איבר במחלקה. בניגוד לפרמטרים הפוזיציוניים של record, הם לא הופכים למאפיינים public: חשפו אותם במפורש עם public string Name => name; או public string Name { get; } = name;.