Menu

C# Reflection ו-Attributes: typeof, GetType ותכונות מותאמות

איך עובד reflection ב-C#: אובייקטי Type מ-typeof ומ-GetType, קריאה והצבה של מאפיינים, קריאה למתודות לפי שם, יצירת מופעים, ותכונות (attributes): המובנות כמו Obsolete, הצהרה על תכונות משלכם וקריאתן בזמן ריצה. ובנוסף, כמה reflection עולה.

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

Reflection מאפשר לתוכנית לבחון טיפוסים בזמן שהיא רצה: אילו מאפיינים יש למחלקה, מה הערכים שלהם, אילו מתודות קיימות, אילו תכונות מוצמדות. Attributes (תכונות) הם החצי השני: תגיות הצהרתיות בסוגריים מרובעים, כמו [Obsolete] או [JsonPropertyName("id")], שאין להן משמעות בפני עצמן עד שהקומפיילר או קוד כלשהו קוראים אותן עם reflection. סריאלייזרים, ORMs, ספריות אימות, מסגרות בדיקה והניתוב של ASP.NET עובדים כולם כך.

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

אובייקטי Type: typeof ו-GetType

הכול מתחיל מ-System.Type. יש שתי דרכים לקבל אחד:

פלט:

Employee
Manager
Employee
False
True
True
System.Int32
False
Name

ההבדל שצריך לזכור: typeof(X) נותן שם לטיפוס שאתם מכירים כשאתם כותבים את הקוד, ו-obj.GetType() שואל אובייקט מה הוא באמת. ההשוואה GetType() == typeof(Employee) היא התאמה מדויקת שנכשלת עבור מחלקות נגזרות, ובדרך כלל זה לא מה שרוצים; is ו-IsAssignableFrom מתחשבים בירושה. nameof נראה דומה אבל הוא בכלל לא reflection: הקומפיילר מחליף אותו בקבוע מחרוזת.

GetType() על הפניה שהיא null זורק NullReferenceException, כי אין אובייקט לשאול. על טיפוס ערך nullable ארוז הוא מחזיר את הטיפוס שבבסיס: ((int?)5).GetType() הוא System.Int32.

קריאה והצבה של מאפיינים

GetProperties() מפרט את המאפיינים הציבוריים של טיפוס כאובייקטי PropertyInfo, שכל אחד מהם יכול לקרוא ולכתוב את הערך במופע נתון:

פלט:

Name   String   = Mug
Price  Decimal  = 8.50
Stock  Int32    = 12
7.90
True
SUP-77

שלושה דברים שזה מראה:

  • GetValue מחזיר object, כך שטיפוסי ערך חוזרים ארוזים (boxed) וצריך להמיר אותם כדי להשתמש בהם.
  • GetProperty עם שם שלא קיים מחזיר null, והקריאה הבאה עליו זורקת NullReferenceException. בדקו לפני השימוש.
  • BindingFlags.NonPublic | BindingFlags.Instance מגיע לאיברים פרטיים. זה לגיטימי בכלים ובבדיקות, אבל זה עוקף את הכימוס ונשבר בשקט כשמבצעים refactoring למחלקה.

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

קריאה למתודות ויצירת אובייקטים לפי שם

GetMethod מוצא מתודה, ו-Invoke קורא לה עם מערך של ארגומנטים. Activator.CreateInstance יוצר אובייקט מ-Type, וכך מערכות תוספים ומכלי DI בונים טיפוסים שנבחרים בזמן ריצה:

פלט:

60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True

DeclaredOnly מגביל את הרשימה לאיברים שהוצהרו במחלקה עצמה; בלעדיו, GetMethods מחזירה גם את ToString, Equals, GetHashCode ו-GetType מ-object. Type.GetType("Name") צריך את השם המלא כולל ה-namespace, ובטיפוסים מ-assemblies אחרים גם את שם ה-assembly ("MyApp.Plugins.Csv, MyApp.Plugins").

אם המתודה שהופעלה זורקת חריגה, Invoke עוטף אותה ב-TargetInvocationException; החריגה המקורית נמצאת ב-InnerException שלה.

תכונות: תגיות שהקומפיילר ומסגרות העבודה קוראים

תכונה נכתבת בסוגריים מרובעים לפני הדבר שהיא מתארת. מסגרת העבודה מגדירה הרבה תכונות; כמה מהן משנות את מה שהקומפיילר עושה:

public class OrderService
{
    [Obsolete("Use PlaceOrderAsync instead.")]
    public void PlaceOrder(Order order) { }
    // Every call site: warning CS0618: 'OrderService.PlaceOrder(Order)' is obsolete: 'Use PlaceOrderAsync instead.'
    // [Obsolete("...", true)] makes it error CS0619 instead.

    [Conditional("DEBUG")]
    public void Trace(string message) => Console.WriteLine(message);
    // Calls to Trace are removed entirely from builds without the DEBUG symbol.
}

[Flags] enum Channels { None = 0, Email = 1, Sms = 2 }   // changes how ToString formats combinations
[Serializable] class Snapshot { }                     // marks a type for legacy binary serialization

אחרות נקראות על ידי ספריות בזמן ריצה: [JsonPropertyName] ו-[JsonIgnore] על ידי System.Text.Json, [Required] ו-[MaxLength] על ידי אימות המודלים של ASP.NET Core ועל ידי Entity Framework, [HttpGet("orders/{id}")] על ידי הניתוב של ASP.NET, [Fact] ו-[Test] על ידי מריצי בדיקות. התכונה עצמה לא עושה כלום; הקוד שמחפש אותה עושה.

השם ObsoleteAttribute מתקצר ל-[Obsolete] כשמחילים אותו: לפי המוסכמה כל שם של מחלקת תכונה מסתיים ב-Attribute, ו-C# מאפשרת להשמיט את הסיומת.

הצהרה על תכונה מותאמת וקריאתה

תכונה מותאמת היא מחלקה שנגזרת מ-Attribute. [AttributeUsage] אומר על מה מותר להחיל אותה. פרמטרים של הבנאי הופכים לארגומנטים לפי מיקום, ומאפיינים ציבוריים שאפשר להציב בהם הופכים לארגומנטים עם שם:

פלט:

Username must be at most 20 characters
Email is required
Keep the city code short
0

זו גרסה מוקטנת של מה שאימות המודלים של ASP.NET Core עושה עם System.ComponentModel.DataAnnotations. ארגומנטים של תכונות חייבים להיות קבועים בזמן קומפילציה (מספרים, מחרוזות, typeof(...), ערכי enum, או מערכים שלהם), כי הם נשמרים במטא-נתונים של ה-assembly. GetCustomAttribute<T>() היא מתודת הרחבה ב-System.Reflection; יש גם IsDefined(typeof(T)) כשצריך רק לדעת אם תכונה קיימת.

המחיר של reflection

reflection מחליף מהירות ובטיחות בגמישות:

  • מהירות. חיפוש איבר לפי שם וקריאה לו דרך Invoke או GetValue איטיים בהרבה מקריאה ישירה, ומבצעים boxing לטיפוסי ערך. לשימוש חוזר, חפשו את ה-PropertyInfo או ה-MethodInfo פעם אחת ושמרו אותו, או הפכו אותו ל-delegate עם Delegate.CreateDelegate או MethodInfo.CreateDelegate וקראו לו.
  • בטיחות. שם עם שגיאת כתיב או חתימה שהשתנתה מתקמפלים בלי בעיה ונכשלים בזמן ריצה. העדיפו את nameof(Product.Price) על פני המחרוזת "Price" בכל מקום אפשרי, כך ששינויי שמות ייתפסו.
  • Trimming ו-AOT. אפליקציות עם trimming ו-Native AOT מסירות איברים שנראה שאף אחד לא משתמש בהם, ו-reflection מסתיר שימוש מהניתוח הזה. ספריות מודרניות (System.Text.Json, GeneratedRegex, לוגים) עוברות ל-source generators, שעושים את אותה עבודה בזמן קומפילציה.

השתמשו ב-reflection לחלקים של תוכנית שבאמת לא מכירים מראש את הטיפוסים שלהם: תוספים, כלים גנריים, סריאלייזרים, עזרים לבדיקות. כשהטיפוסים ידועים, קוד רגיל, generics או ממשקים מהירים יותר ונבדקים על ידי הקומפיילר.

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

  • GetType() == typeof(Base) כדי לבדוק מחלקת בסיס. זה נכשל עבור טיפוסים נגזרים. השתמשו ב-is או ב-IsAssignableFrom.
  • אי בדיקה של null. GetProperty, GetMethod ו-Type.GetType מחזירים null כששום דבר לא מתאים.
  • reflection בלולאה חמה בלי שמירה במטמון. שמרו את ה-MemberInfo או קמפלו delegate.
  • תפיסה של החריגה הלא נכונה מ-Invoke. החריגה האמיתית היא ה-InnerException של TargetInvocationException.
  • מחרוזות קסם לשמות איברים. השתמשו ב-nameof.

שאלות נפוצות

מה זה reflection ב-C#?

reflection היא היכולת של תוכנית לבחון טיפוסים בזמן ריצה: לפרט את המאפיינים והמתודות של מחלקה, לקרוא ולהציב ערכים לפי שם, לקרוא למתודות, ליצור מופעים ולקרוא תכונות. היא נמצאת ב-System.Reflection ומתחילה מאובייקט Type. סריאלייזרים, ORMs, מכלי הזרקת תלויות ומסגרות בדיקה בנויים עליה.

מה ההבדל בין typeof ל-GetType ב-C#?

typeof(Customer) נפתר בזמן קומפילציה משם של טיפוס ולא צריך אובייקט. obj.GetType() נקרא על מופע בזמן ריצה ומחזיר את הטיפוס האמיתי של האובייקט, שיכול להיות נגזר יותר מהטיפוס המוצהר של המשתנה: עבור Animal a = new Dog();, a.GetType() הוא Dog. GetType() על הפניה שהיא null זורק NullReferenceException.

איך מקבלים ערך של מאפיין לפי שם ב-C#?

obj.GetType().GetProperty("Price") מחזיר PropertyInfo (או null אם אין מאפיין ציבורי כזה), ו-.GetValue(obj) קורא אותו כ-object. .SetValue(obj, value) כותב אותו. שמרו את ה-PropertyInfo אם אתם עושים את זה בלולאה, כי החיפוש הוא החלק היקר.

איך יוצרים תכונה מותאמת (custom attribute) ב-C#?

הצהירו על מחלקה שנגזרת מ-System.Attribute, תנו לה שם עם הסיומת Attribute, וסמנו היכן מותר להשתמש בה עם [AttributeUsage]: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. החילו אותה כ-[MaxLength(50)] וקראו אותה עם property.GetCustomAttribute<MaxLengthAttribute>().

מה עושה התכונה Obsolete ב-C#?

[Obsolete("Use PlaceOrderAsync instead")] על איבר גורם לקומפיילר להפיק אזהרה CS0618, עם ההודעה שלכם, בכל מקום קריאה. [Obsolete("...", true)] הופך את האזהרה לשגיאה CS0619. כך ספריות מוציאות API משימוש בלי לשבור קוראים בן לילה.

האם reflection איטי ב-C#?

בהשוואה לקריאה ישירה, כן: חיפוש איבר לפי שם והפעלה שלו דרך MethodInfo.Invoke או PropertyInfo.GetValue איטיים בדרך כלל פי עשרות עד מאות, והם מבצעים boxing לטיפוסי ערך. זה בסדר לאתחול, להגדרות ולשימוש מזדמן. בנתיבים חמים, שמרו את ה-MemberInfo, בנו delegate פעם אחת, או השתמשו במקום זאת ב-generics או ב-source generator.

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

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

להתחיל