מחלקה היא טיפוס שאתם מגדירים: היא מקבצת נתונים (שדות) עם הקוד שעובד על הנתונים האלה (מתודות). אובייקט הוא מופע אחד של מחלקה, שנוצר בזמן ריצה עם new. מחלקה אחת, הרבה אובייקטים, לכל אחד הערכים שלו.
הצהרה על מחלקה ויצירת אובייקטים
הצהרה על מחלקה מפרטת את האיברים שלה בתוך סוגריים מסולסלים. שדות מחזיקים את הנתונים, מתודות מחזיקות את ההתנהגות. new ClassName() מקצה אובייקט ונותן לכם הפניה אליו.
פלט:
Maya: 120.50
Omar: 40
Withdraw 200 succeeded: False
לכל אובייקט יש Owner ו-Balance משלו. בתוך מתודה, Balance פירושו "היתרה של האובייקט שעליו נקראה המתודה", ולכן maya.Deposit ו-omar.Deposit משנים מספרים שונים. האובייקט המרומז הזה זמין כ-this כשצריך לנקוב בשמו.
שדות שלא מציבים להם ערך מתחילים בערך ברירת המחדל שלהם: 0 למספרים, false ל-bool, ו-null למחרוזות ולטיפוסי מחלקה אחרים.
האיברים כאן הם public כדי ש-Main תוכל להגיע אליהם. מחלקות אמיתיות בדרך כלל שומרות שדות private וחושפות מאפיינים במקומם, והדף על מגדירי גישה מפרט את הכללים. מחלקה יכולה גם להצהיר על בנאי שמציב את השדות שלה כשהאובייקט נוצר, וזה מוסבר בדף על בנאים.
מחלקה היא טיפוס הפניה
משתנה מטיפוס מחלקה לא מכיל את האובייקט. הוא מכיל הפניה לאובייקט שחי ב-heap. הצבת המשתנה מעתיקה את ההפניה, כך ששני משתנים יכולים להצביע על אותו אובייקט.
פלט:
5
6
True
False
רואים כאן שלוש השלכות:
- שינוי של האובייקט דרך
secondנראה דרךfirst, כי יש רק אובייקט אחד. - העברת אובייקט למתודה מעבירה את ההפניה. המתודה יכולה לשנות את השדות של האובייקט, והקוד הקורא רואה את זה. (הצבת אובייקט חדש לפרמטר בתוך המתודה לא משפיעה על המשתנה של הקוד הקורא; לשם כך צריך
ref.) ==על שני מופעים של מחלקה משווה הפניות כברירת מחדל: "האם זה אותו אובייקט?", ולא "האם יש להם אותם ערכים?". ל-lookalikeיש נתונים זהים והוא עדיין אובייקט אחר.
אם אתם רוצים עותקים בהצבה והשוואה לפי ערך, הסתכלו על structs, שהם טיפוסי ערך, או על records, שמשווים לפי ערך.
null ו-NullReferenceException
משתנה הפניה יכול להחזיק null, כלומר "אין אובייקט". שימוש באיבר דרך null זורק NullReferenceException, חריגת זמן הריצה הנפוצה ביותר בקוד C#.
פלט:
True
no city on file
NullReferenceException: Address was never set
Lisbon
הפתרון כמעט אף פעם אינו לתפוס את החריגה. גלו למה ההפניה היא null, ואז צרו את האובייקט מוקדם יותר, בדקו לפני השימוש, או השתמשו באופרטור ה-null-conditional ?., שעוצר ומחזיר null במקום לזרוק. אחר כך ?? מספק ערך חלופי. שני האופרטורים מוסברים ב-nullable types.
דריסת ToString
כל מחלקה יורשת מ-object, שמספקת את ToString(). הגרסה שעוברת בירושה מחזירה את השם המלא של הטיפוס (כולל ה-namespace שלו, אם יש), וזה לעיתים רחוקות מה שרוצים להדפיס. דרסו אותה:
פלט:
Plain
Mug (8.50)
In cart: Mug (8.50)
Total line: Mug (8.50)
Console.WriteLine, string interpolation ו-+ עם מחרוזת קוראים כולם ל-ToString() בשבילכם. גם ה-debugger מציג אותה, ולכן דריסה שימושית גם אם אתם אף פעם לא מדפיסים את האובייקט בעצמכם.
Object initializers
object initializer מציב שדות ומאפיינים public מיד אחרי שהאובייקט נבנה, באותו ביטוי. הוא קריא יותר מרשימה של הצבות ועובד היטב בתוך ליטרלים של אוספים.
פלט:
A12: 25 (0 extras)
A13: 40 (2 extras)
שני פרטים בכרטיס השני. Price = 40m דורס את ה-25m של ה-field initializer, כי ה-object initializer רץ אחרי שהאובייקט נבנה. ל-Extras = { "Parking", "Poster" } אין new: הוא קורא ל-Add על הרשימה שהשדה כבר מחזיק, במקום להחליף אותה.
object initializer אינו בנאי. הוא יכול להציב רק איברים שנגישים מהקוד הקורא, והוא לא יכול לאכוף שאיבר בכלל יוצב. כשאובייקט לא תקין בלי ערכים מסוימים, דרשו אותם בבנאי.
מחלקות partial
מילת המפתח partial מפצלת מחלקה אחת על פני כמה קבצים. הקומפיילר ממזג את החלקים למחלקה אחת.
// Invoice.cs
public partial class Invoice
{
public decimal Subtotal;
public decimal Total() => Subtotal + Tax();
}
// Invoice.Tax.cs
public partial class Invoice
{
private decimal Tax() => Subtotal * 0.2m;
}
כל חלק חייב לכתוב partial, להיות באותו namespace ובאותו פרויקט. השימוש הנפוץ הוא קוד שנוצר אוטומטית: מעצבי WinForms, source generators וחלק מה-ORMs כותבים חלק אחד ומשאירים לכם את השני, כך שיצירה מחדש אף פעם לא דורסת את הקוד שלכם. פיצול מחלקה שנכתבה ביד על פני קבצים רק כדי לקצר אותה בדרך כלל מסתיר מחלקה שהייתה צריכה להיות שתי מחלקות.
טעויות נפוצות
- שימוש באובייקט לפני שיוצרים אותו. שדה מטיפוס מחלקה הוא
nullעד שמציבים לוnew .... הגישה הראשונה לאיבר זורקתNullReferenceException. - ציפייה שהצבה תעתיק.
var b = a;עבור מחלקה נותן שם שני לאותו אובייקט. צרו אובייקט חדש והעתיקו את הערכים כשאתם צריכים אובייקט עצמאי. - השוואת אובייקטים עם
==בציפייה להשוואה לפי ערך. במחלקות זה משווה הפניות, אלא אם המחלקה מעמיסה את==(כמו ש-stringעושה). השוו את השדות הרלוונטיים, או דרסו אתEquals. - הדפסת אובייקט וקבלת שם הטיפוס שלו. דרסו את
ToString(). - הצהרה על הכול כ-
public. זה עובד בדוגמאות קטנות, אבל זה מאפשר לכל קוד להכניס את האובייקט למצב לא תקין, למשל יתרה שלילית. שמרו שדות private ושנו אותם דרך מתודות שבודקות את הכללים.
שאלות נפוצות
מהי מחלקה ב-C#?
מחלקה היא טיפוס שאתם מגדירים: קבוצה בעלת שם של נתונים (שדות ומאפיינים) והתנהגות (מתודות). class Account { public decimal Balance; public void Deposit(decimal amount) { Balance += amount; } } מצהירה על אחת כזו. מחלקה היא שרטוט; היא לא עושה כלום עד שיוצרים ממנה אובייקטים עם new.
מה ההבדל בין מחלקה לאובייקט ב-C#?
המחלקה היא ההגדרה, שנכתבת פעם אחת בקוד המקור. אובייקט הוא מופע אחד שלה, שנוצר בזמן ריצה עם new, ויש לו עותק משלו של כל שדה מופע. new Account() פעמיים נותן שני אובייקטים מאותה מחלקה עם יתרות בלתי תלויות.
איך יוצרים אובייקט ב-C#?
משתמשים ב-new ואחריו שם המחלקה והארגומנטים של הבנאי: var account = new Account();. אפשר להציב שדות ומאפיינים public באותו ביטוי עם object initializer: new Account { Owner = "Maya" }. המשתנה מחזיק הפניה לאובייקט החדש, לא את האובייקט עצמו.
למה אני מקבל NullReferenceException ב-C#?
השתמשתם באיבר (.Name, .Length, קריאה למתודה) דרך משתנה שמחזיק null, כך שאין אובייקט לפעול עליו. סיבות נפוצות הן שדה שאף פעם לא הוצב (שדות של מחלקה מתחילים כ-null), חיפוש שהחזיר null, או מתודה שהחזירה null בכישלון. בדקו עם if (x != null), השתמשו ב-x?.Name, או ודאו שהאובייקט נוצר לפני השימוש.
איך דורסים את ToString ב-C#?
הצהירו על public override string ToString() במחלקה שלכם והחזירו את הטקסט שאתם רוצים, למשל public override string ToString() => $"{Name} ({Price:F2})";. בלי זה, Console.WriteLine(obj) ו-string interpolation מדפיסים את שם המחלקה, כי זה מה ש-object.ToString() מחזירה.