Menu

מאפיינים ב-C#: get, set, מאפיינים אוטומטיים, init ו-required

איך עובדים מאפיינים (properties) ב-C#: מאחזרי get ו-set מעל שדה גיבוי, מאפיינים אוטומטיים, private set ומאפייני קריאה בלבד, מאפיינים מחושבים, אימות ב-setter, ומילות המפתח init ו-required מ-C# 9 ו-11.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

מאפיין (property) נראה כמו שדה לקוד שמשתמש בו (order.Total, order.Total = 5), אבל הוא למעשה זוג מתודות: מאחזר get שמחזיר ערך ומאחזר set שמקבל ערך. העקיפות הזו מאפשרת למחלקה לבדוק ערכים, לחשב אותם או לסרב לכתיבה, בלי לשנות את האופן שבו הקוראים משתמשים בה.

שדות מול מאפיינים

שדה ציבורי נותן גישה ישירה לאחסון של האובייקט. כל אחד יכול לשים בו כל דבר:

public class Account
{
    public decimal Balance;   // any code can write -500 here
}

מאפיין שם קוד בין הקורא לבין האחסון. הנה הצורה המלאה שנכתבת ביד: שדה גיבוי (backing field) פרטי ועוד מאפיין עם שני המאחזרים. בתוך set, מילת המפתח value היא הערך שמוצב.

פלט:

250
Rejected a negative balance
250

מעבר לאימות, מאפיינים חשובים כי רוב האקוסיסטם של .NET עובד איתם ומתעלם משדות: ממשקים יכולים להצהיר על מאפיינים אבל לא על שדות מופע, data binding ב-WPF וב-MAUI נקשר למאפיינים, ו-System.Text.Json מבצע סריאליזציה של מאפיינים ציבוריים כברירת מחדל ומדלג על שדות.

מאפיינים אוטומטיים

כשהמאחזרים רק היו קוראים וכותבים את השדה, תנו לקומפיילר לכתוב אותם. { get; set; } הוא מאפיין אוטומטי (auto-implemented property): הקומפיילר יוצר בשבילכם את שדה הגיבוי הנסתר.

פלט:

Notebook: 9.99, 10 left

זה נראה כמו שדה, אז למה לטרוח? כי הפיכה של שדה ציבורי למאפיין בהמשך היא שינוי שובר לקוד מקומפל שמשתמש בו (ולארגומנטים של ref ו-out), ואילו שינוי של מאפיין אוטומטי למאפיין מלא עם אימות שקוף לקוראים. התחלה עם { get; set; } שומרת את האפשרות הזו פתוחה בחינם.

private set ומאפייני קריאה בלבד

את רוב המצב כדאי שכולם יוכלו לקרוא ורק האובייקט עצמו יוכל לשנות. תנו ל-setter מגדיר גישה מצומצם יותר:

פלט:

Ines: 2 items, total 15.75

יש כאן שלושה סוגים של קריאה בלבד:

  • { get; private set; }: המחלקה יכולה לשנות אותו בכל זמן, קוד חיצוני לא.
  • { get; } (מאפיין אוטומטי לקריאה בלבד, C# 6): אפשר להציב בו ערך רק בבנאי או באתחול. אחרי הבנייה אף אחד לא יכול לשנות אותו, אפילו לא המחלקה. כך יוצרים מאפיין בלתי ניתן לשינוי.
  • => expression (מאפיין מחושב): אין אחסון בכלל. הביטוי רץ בכל קריאה, כך ש-Total תמיד מעודכן לפי הרשימה.

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

מאחזרים עם גוף ביטוי

מאפיינים מלאים עם מאחזרים של שורה אחת יכולים להשתמש ב-=> לכל מאחזר:

private string title;

public string Title
{
    get => title;
    set => title = value?.Trim() ?? "";
}

public decimal Total => SumPrices(); הוא קיצור למאפיין עם getter בלבד; get => ... הוא אותו רעיון בתוך מאפיין שיש לו גם setter.

לוגיקה ב-getter וב-setter: התראות וערכים עצלים

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

פלט:

(building summary)
Theme: light
Theme: light
Theme changed
(building summary)
Theme: dark

Changed הוא event, ותבנית ה-setter הזו היא בדיוק איך שנראים מימושים של INotifyPropertyChanged ב-WPF וב-MAUI. nameof(Theme) שומר על המחרוזת מסונכרנת אם שם המאפיין משתנה.

טעות הרקורסיה האינסופית

הבאג הנפוץ ביותר במאפיינים הוא setter שמציב ערך במאפיין במקום בשדה הגיבוי:

public string Name
{
    get { return Name; }        // calls get again, forever
    set { Name = value; }       // calls set again, forever
}

כל מאחזר קורא לעצמו, והתוכנית קורסת עם StackOverflowException, שאי אפשר לתפוס. התיקון הוא לקרוא ולכתוב שדה נפרד (name, באותיות קטנות), או להשתמש במאפיין אוטומטי. C# 14 מוסיפה את מילת המפתח field בדיוק למקרה הזה: בתוך מאחזר, field מתייחס לשדה הגיבוי שהקומפיילר יוצר, כך ש-set => field = value.Trim(); עובד בלי להצהיר על שדה.

מאחזרי init (C# 9)

מאפיין לקריאה בלבד מכריח אתכם להעביר כל ערך דרך בנאי. C# 9 הוסיפה את init, setter שמותר רק בזמן יצירת האובייקט: בבנאי או באתחול אובייקט.

public class Product
{
    public string Sku { get; init; }
    public decimal Price { get; init; }
}

var p = new Product { Sku = "MUG-01", Price = 8.50m };   // fine: during creation
p.Price = 4m;   // error CS8852: Init-only property or indexer 'Product.Price' can only be assigned
                // in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor

התוצאה היא אובייקט בלתי ניתן לשינוי עם תחביר של אתחול אובייקט, וזה בדיוק מה שמחלקות record משתמשות בו למאפייני המיקום שלהן. כדי "לשנות" אובייקט כזה, יוצרים אחד חדש; records הופכים את זה לשורה אחת עם with.

ב-C# 7, המקבילה הקרובה ביותר היא מאפיין לקריאה בלבד ועוד פרמטר בבנאי:

public class Product
{
    public string Sku { get; }
    public decimal Price { get; }
    public Product(string sku, decimal price) { Sku = sku; Price = price; }
}

איברי required (C# 11)

אתחול אובייקט לא יכול להכריח קוראים להציב שום ערך: new Product { } מתקמפל גם כש-Sku אסור שיהיה ריק לעולם. C# 11 הוסיפה את המגדיר required:

public class User
{
    public required string Email { get; init; }
    public string DisplayName { get; init; } = "";
}

var ok = new User { Email = "ana@example.com" };
var bad = new User { DisplayName = "Ana" };   // error CS9035: Required member 'User.Email'
                                              // must be set in the object initializer or attribute constructor.

required עובד עם set ועם init. בנאי שמציב בעצמו את האיברים הנדרשים יכול להצהיר על כך עם התכונה [SetsRequiredMembers], כך שקוראים שמשתמשים בבנאי הזה לא יתבקשו להציב אותם שוב.

טעויות נפוצות

  • רקורסיה במאחזר. set { Name = value; } קורא לעצמו. השתמשו בשדה גיבוי או במאפיין אוטומטי.
  • getters יקרים או עם תופעות לוואי. קוראים מצפים שקריאה של מאפיין תהיה מהירה ובטוחה לחזרה. לעבודה, השתמשו במתודה.
  • שדות ציבוריים "בינתיים". המרה של שדה למאפיין בהמשך שוברת תאימות בינארית, וסריאלייזרים מתעלמים משדות כברירת מחדל. התחילו עם מאפיין אוטומטי.
  • setters ציבוריים לכל דבר. { get; set; } על יתרה או על סטטוס מזמין מצבים לא תקינים. השתמשו ב-private set, ב-init או בקריאה בלבד, ושנו מצב דרך מתודות.
  • החזרה של אוסף שניתן לשינוי ממאפיין לקריאה בלבד. public List<string> Tags { get; } מונע מקוראים להחליף את הרשימה, לא להוסיף לה. החזירו IReadOnlyList<string> כשהאוסף לא אמור להשתנות מבחוץ.

שאלות נפוצות

מה המשמעות של { get; set; } ב-C#?

זו הצהרה על מאפיין אוטומטי: הקומפיילר יוצר שדה פרטי נסתר, מאחזר get שמחזיר אותו ומאחזר set שמציב בו ערך. public string Name { get; set; } מתנהג כלפי הקוראים כמו שדה ציבורי, אבל הוא זוג מתודות, כך שאפשר בהמשך להוסיף אימות או לשנות אותו ל-private set בלי לשנות את הקוד שמשתמש בו.

מה ההבדל בין שדה למאפיין ב-C#?

שדה הוא משתנה שמאוחסן באובייקט. מאפיין הוא זוג מתודות (get ו-set) שנראות מבחוץ כמו שדה. מאפיינים מאפשרים למחלקה לאמת ערכים, לחשב תוצאות או להגביל כתיבה, והם מה ש-data binding, סריאלייזרים וממשקים עובדים איתו. נתונים ציבוריים כדאי לחשוף כמאפיינים; שדות בדרך כלל נשארים פרטיים.

מה זה private set ב-C#?

public int Stock { get; private set; } מאפשר לכל קוד לקרוא את המאפיין, אבל רק לקוד בתוך המחלקה לשנות אותו. כך מחלקה חושפת מצב שבבעלותה, כמו יתרה או ספירה, ושומרת את כל השינויים מאחורי מתודות שאוכפות את הכללים.

מה עושה init ב-C#?

init (C# 9) הוא setter שעובד רק בזמן יצירת האובייקט: בבנאי או באתחול אובייקט. public string Sku { get; init; } מתיר new Product { Sku = "A-1" } אבל דוחה product.Sku = "B-2" אחר כך עם שגיאה CS8852. הוא נותן לאובייקטים בלתי ניתנים לשינוי תחביר של אתחול אובייקט.

מה זה מאפיין required ב-C#?

מאפיין שמסומן required (C# 11) חייב לקבל ערך בכל אתחול אובייקט שיוצר את הטיפוס; השמטה שלו היא שגיאת קומפילציה (CS9035). הוא משתלב היטב עם init: את public required string Email { get; init; } חייבים לספק ביצירה, ואי אפשר לשנות אותו אחר כך.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל