Συνήθως, όταν γράφουμε κώδικα, γνωρίζουμε ποια κλάση χρησιμοποιούμε, ποιες ιδιότητες διαθέτει και ποιες μεθόδους μπορούμε να καλέσουμε.

Για παράδειγμα

Console.WriteLine(product.Name); 

Εδώ γνωρίζουμε ότι το product διαθέτει μια ιδιότητα που ονομάζεται Name.

Τι γίνεται, όμως, όταν θέλουμε να γράψουμε ένα εργαλείο που λειτουργεί με διαφορετικές κλάσεις, χωρίς ξεχωριστό κώδικα για καθεμία;

Εκεί μπορεί να βοηθήσει το reflection.

Τι είναι το reflection;

Το reflection επιτρέπει σε ένα πρόγραμμα να εξετάζει τη δομή των τύπων του όσο εκτελείται.

Με απλά λόγια, ο κώδικάς μας μπορεί να ανακαλύψει:

  • Ποιος είναι ο τύπος ενός αντικειμένου.
  • Ποιες ιδιότητες και μεθόδους διαθέτει.
  • Τι τύπο έχει κάθε ιδιότητα.
  • Ποιες παραμέτρους δέχεται μια μέθοδος.
  • Ποια attributes έχουν δηλωθεί σε έναν τύπο ή στα μέλη του.

Μπορεί επίσης να διαβάσει ή να αλλάξει τιμές ιδιοτήτων, να καλέσει μεθόδους και να δημιουργήσει αντικείμενα δυναμικά.

Στο .NET, αυτές οι δυνατότητες παρέχονται κυρίως από το System.Type και το namespace System.Reflection. Τεκμηρίωση Microsoft

Σκέψου το σαν ένα αντικείμενο που συνοδεύεται από μια περιγραφή:

Ανήκω στην κλάση Product. Έχω μια ιδιότητα Name τύπου string και μια ιδιότητα Stock τύπου int.

Αυτή η περιγραφή αποτελεί μέρος των metadata, δηλαδή πληροφοριών για τον κώδικα.

Το reflection αξιοποιεί αυτές τις πληροφορίες. Δεν χρειάζεται να ανοίξει και να διαβάσει το αρχείο .cs.

Compile time και runtime: η βασική διαφορά

Για να καταλάβουμε το reflection, βοηθά να ξεχωρίσουμε δύο στιγμές:

  • Compile time: όταν ο compiler ελέγχει και μεταγλωττίζει τον κώδικα.
  • Runtime: όταν η εφαρμογή εκτελείται.

Στην άμεση πρόσβαση:

product.Name 

ο compiler ελέγχει ότι η ιδιότητα Name υπάρχει στον τύπο του product.

Με reflection μπορούμε να ζητήσουμε:

Βρες τώρα, όσο τρέχει η εφαρμογή, μια ιδιότητα που ονομάζεται Name.

Αυτή η μεταφορά της αναζήτησης στο runtime μάς δίνει ευελιξία, αλλά μεταφέρει και ορισμένους ελέγχους από τον compiler στον δικό μας κώδικα.

Παράδειγμα 1: η ίδια τιμή με δύο τρόπους

Το παρακάτω είναι ένα πλήρες παράδειγμα για το Program.cs μιας σύγχρονης εφαρμογής Console σε C#:

using System; using System.Reflection; var product = new Product { Name = "Keyboard", Stock = 12 }; // Άμεση πρόσβαση. Console.WriteLine(product.Name); // Πρόσβαση μέσω reflection. Type type = product.GetType(); PropertyInfo? property = type.GetProperty("Name"); if (property is null) { throw new InvalidOperationException( "Η ιδιότητα δεν βρέθηκε."); } Console.WriteLine(property.GetValue(product)); public class Product { public string Name { get; set; } = ""; public int Stock { get; set; } } 

Το αποτέλεσμα:

Keyboard Keyboard 

Η τιμή είναι ίδια. Ο τρόπος πρόσβασης διαφέρει.

Ας δούμε τα βήματα του reflection:

  1. Το product.GetType() επιστρέφει πληροφορίες για τον πραγματικό τύπο του αντικειμένου.
  2. Το GetProperty("Name") αναζητά μια δημόσια ιδιότητα με αυτό το όνομα.
  3. Το αποτέλεσμα είναι ένα PropertyInfo, δηλαδή ένα αντικείμενο που περιγράφει την ιδιότητα.
  4. Το GetValue(product) διαβάζει την τιμή της από το συγκεκριμένο αντικείμενο.

Αν η ιδιότητα δεν βρεθεί, η αναζήτηση επιστρέφει null. Γι’ αυτό ελέγχουμε το αποτέλεσμα πριν το χρησιμοποιήσουμε. Τεκμηρίωση πρόσβασης σε ιδιότητες

Σε αυτό το παράδειγμα, η άμεση πρόσβαση είναι η καλύτερη επιλογή.

Γνωρίζουμε ήδη την κλάση και την ιδιότητα. Το reflection προσθέτει πολυπλοκότητα χωρίς πρακτικό όφελος.

Το χρησιμοποιήσαμε για να καταλάβουμε τον μηχανισμό.

Παράδειγμα 2: ένα εργαλείο για διαφορετικές κλάσεις

Ας υποθέσουμε ότι θέλουμε μια μέθοδο που εμφανίζει τις δημόσιες ιδιότητες διαφορετικών αντικειμένων.

Θα μπορούσαμε να γράψουμε:

Console.WriteLine(product.Name); Console.WriteLine(product.Stock); Console.WriteLine(category.Title); 

Αυτό λειτουργεί, αλλά για κάθε νέα κλάση ή ιδιότητα χρειάζεται να ενημερώνουμε τον κώδικα εμφάνισης.

Με reflection μπορούμε να γράψουμε μια κοινή μέθοδο:

using System; using System.Reflection; var product = new Product { Name = "Keyboard", Stock = 12 }; var category = new Category { Title = "Accessories" }; PrintProperties(product); PrintProperties(category); static void PrintProperties(object item) { var properties = item.GetType().GetProperties( BindingFlags.Public | BindingFlags.Instance); foreach (var property in properties) { // Παραλείπουμε ιδιότητες χωρίς δημόσιο getter // και ιδιότητες που χρειάζονται δείκτη. if (property.GetGetMethod() is null || property.GetIndexParameters().Length > 0) { continue; } Console.WriteLine( $"{property.Name}: {property.GetValue(item)}"); } } public class Product { public string Name { get; set; } = ""; public int Stock { get; set; } } public class Category { public string Title { get; set; } = ""; } 

Η έξοδος για αυτά τα αντικείμενα:

Name: Keyboard Stock: 12 Title: Accessories 

Η PrintProperties δεν αναφέρει πουθενά την Product ή την Category. Δέχεται ένα αντικείμενο και ανακαλύπτει τις ιδιότητές του κατά την εκτέλεση.

Τα:

BindingFlags.Public | BindingFlags.Instance 

περιορίζουν την αναζήτηση σε δημόσιες ιδιότητες που ανήκουν στα αντικείμενα, αποκλείοντας τις static.

Ο χαρακτήρας | συνδυάζει τις δύο επιλογές. Τεκμηρίωση GetProperties

Ο έλεγχος μέσα στον βρόχο παραλείπει:

  • Ιδιότητες χωρίς δημόσιο getter.
  • Indexers, δηλαδή ιδιότητες που χρειάζονται δείκτη, όπως το item[0].

Αν προσθέσουμε άλλη μία απλή δημόσια ιδιότητα στην Product, η μέθοδος μπορεί να την εμφανίσει χωρίς αλλαγή στον βρόχο.

Εδώ βρίσκεται το όφελος: γράφουμε κώδικα που λειτουργεί με τη δομή που ανακαλύπτει.

Το παράδειγμα προορίζεται για απλές κλάσεις δεδομένων. Η GetValue εκτελεί τον getter μιας ιδιότητας. Επομένως, ένας getter με πρόσθετη λογική μπορεί να εκτελέσει εργασία ή να πετάξει exception. Τεκμηρίωση GetValue

Πότε είναι χρήσιμο το reflection;

1. Όταν δημιουργούμε γενικά εργαλεία

Το προηγούμενο παράδειγμα είναι μια τέτοια περίπτωση.

Ένα διαγνωστικό εργαλείο μπορεί να εμφανίζει πληροφορίες για διαφορετικές κλάσεις χωρίς ξεχωριστή υλοποίηση για καθεμία.

Η ανάγκη του είναι να εξετάζει τη δομή των αντικειμένων, επομένως το reflection ταιριάζει στο πρόβλημα.

2. Όταν μια βιβλιοθήκη επεξεργάζεται διαφορετικά μοντέλα

Μια βιβλιοθήκη serialization μπορεί να ανακαλύπτει ιδιότητες και να διαβάζει τις τιμές τους, ώστε να μετατρέψει ένα αντικείμενο σε JSON.

Υπάρχουν και εναλλακτικές, όπως η παραγωγή κώδικα κατά το build μέσω source generators. Τεκμηρίωση για serialization και source generation

3. Όταν αξιοποιούμε attributes

Τα attributes προσθέτουν metadata σε κλάσεις, μεθόδους ή ιδιότητες.

Ένα εργαλείο μπορεί να τα διαβάσει μέσω reflection και να αποφασίσει πώς θα χειριστεί το αντίστοιχο μέλος.

Για παράδειγμα, ένα εργαλείο δοκιμών μπορεί να αναζητά μεθόδους με ένα συγκεκριμένο attribute.

Το attribute από μόνο του δεν εκτελεί τη λειτουργία. Χρειάζεται κώδικας που το διαβάζει και ενεργεί ανάλογα. Τεκμηρίωση attributes

4. Όταν ανακαλύπτουμε plugins

Σε εφαρμογές που υποστηρίζουν δυναμική φόρτωση επεκτάσεων, το reflection μπορεί να βοηθήσει στον εντοπισμό κατάλληλων κλάσεων.

Μετά την ανακάλυψη, η επικοινωνία μπορεί να γίνεται μέσω ενός κοινού interface.

Έτσι, η δυναμική αναζήτηση χρησιμοποιείται για την αρχική ανακάλυψη και η κανονική λειτουργία βασίζεται σε γνωστές μεθόδους.

Τέτοια συστήματα χρειάζονται ιδιαίτερη προσοχή όταν συνδυάζονται με trimming ή Native AOT. Τεκμηρίωση περιορισμών

Πότε να το αποφεύγουμε;

1. Όταν γνωρίζουμε ήδη την κλάση και την ιδιότητα

Αν η απαίτηση είναι «εμφάνισε το όνομα του προϊόντος», αυτό αρκεί:

Console.WriteLine(product.Name); 

Η δυναμική αναζήτηση της ίδιας ιδιότητας δυσκολεύει την ανάγνωση χωρίς να προσθέτει χρήσιμη ευελιξία.

2. Όταν ένα interface ή τα generics καλύπτουν την ανάγκη

Αν διαφορετικές κλάσεις πρέπει να προσφέρουν μια γνωστή λειτουργία, ένα interface επιτρέπει να την καλέσουμε με ελέγχους από τον compiler.

Αν θέλουμε επαναχρησιμοποιήσιμο κώδικα που λειτουργεί με διαφορετικούς τύπους, τα generics μπορεί να επαρκούν.

Το ότι ένας μηχανισμός είναι πιο δυναμικός δεν σημαίνει ότι είναι καταλληλότερος.

3. Όταν χάνουμε χρήσιμους ελέγχους του compiler

Αν γράψουμε:

product.Nmae 

ο compiler θα εντοπίσει ότι η ιδιότητα δεν υπάρχει.

Αν γράψουμε:

type.GetProperty("Nmae") 

ο κώδικας μπορεί να μεταγλωττιστεί κανονικά. Το πρόβλημα θα εμφανιστεί κατά την εκτέλεση, όταν η αναζήτηση επιστρέψει null.

Όταν το μέλος είναι γνωστό στον κώδικά μας, μπορούμε να χρησιμοποιήσουμε:

type.GetProperty(nameof(Product.Name)) 

Το nameof βοηθά να αποφεύγουμε χειροκίνητα ονόματα και λάθη σε μετονομασίες. Δεν καταργεί την ανάγκη ελέγχου του αποτελέσματος της αναζήτησης.

4. Όταν εκτελείται πολύ συχνά και επηρεάζει την απόδοση

Η αναζήτηση μελών και η δυναμική κλήση τους έχουν κόστος.

Μια αναζήτηση κατά την εκκίνηση έχει διαφορετική σημασία από χιλιάδες επαναλαμβανόμενες αναζητήσεις μέσα σε έναν βρόχο.

Το κόστος εξαρτάται από την πράξη, την έκδοση του .NET και τη συχνότητα χρήσης. Δεν υπάρχει ένας πολλαπλασιαστής που ισχύει για κάθε χρήση reflection.

Αν χρειάζεται σε ένα συχνά εκτελούμενο σημείο:

  • Μέτρησε την πραγματική επίδραση στην εφαρμογή.
  • Αποθήκευσε τα αποτελέσματα αναζήτησης, όπως τα PropertyInfo, όταν μπορούν να επαναχρησιμοποιηθούν.
  • Για επαναλαμβανόμενες κλήσεις μεθόδων με γνωστή υπογραφή, εξέτασε τη δημιουργία delegate.

Η αποθήκευση των metadata μειώνει τις αναζητήσεις, αλλά δεν εξαφανίζει κάθε κόστος της δυναμικής πρόσβασης. Τεχνική ανάλυση της Microsoft

5. Όταν εξαρτόμαστε από εσωτερικές λεπτομέρειες βιβλιοθηκών

Η πρόσβαση σε ένα private πεδίο μπορεί να φαίνεται βολική για μια συγκεκριμένη ανάγκη.

Δημιουργεί όμως εξάρτηση από μια εσωτερική λεπτομέρεια που ο δημιουργός της βιβλιοθήκης μπορεί να αλλάξει.

Μια αναβάθμιση μπορεί να μετονομάσει ή να αφαιρέσει αυτό το πεδίο, με το πρόβλημα να εμφανίζεται κατά την εκτέλεση.

Προτίμησε το δημόσιο API όταν καλύπτει την απαίτηση.

Τι αλλάζει με trimming και Native AOT;

Το trimming αφαιρεί κώδικα που η ανάλυση του build θεωρεί ότι δεν χρησιμοποιείται.

Το Native AOT μεταγλωττίζει την εφαρμογή σε native κώδικα πριν από την εκτέλεση.

Η δυναμική αναζήτηση τύπων και μελών μπορεί να δυσκολέψει αυτή την ανάλυση: το build πρέπει να γνωρίζει ποια μέλη θα χρειαστούν αργότερα.

Πολλές χρήσεις reflection μπορούν να γίνουν συμβατές, αρκεί να διατηρούνται τα απαραίτητα μέλη και να αντιμετωπίζονται σωστά οι σχετικές προειδοποιήσεις.

Σε ορισμένα σενάρια, τα source generators προσφέρουν μια κατάλληλη εναλλακτική, παράγοντας τον απαιτούμενο κώδικα κατά το build. Οδηγός Microsoft για trim warnings

Πώς αποφασίζουμε στην πράξη;

Ανάγκη Συνήθης πρώτη επιλογή
Γνωρίζουμε την κλάση και την ιδιότητα Άμεση πρόσβαση
Διαφορετικές κλάσεις μοιράζονται γνωστή συμπεριφορά Interface
Θέλουμε επαναχρησιμοποίηση με ελέγχους τύπων Generics
Χρειάζεται να ανακαλύψουμε τη δομή κατά την εκτέλεση Reflection
Μπορούμε να παράγουμε τον απαιτούμενο κώδικα κατά το build Source generator

Πριν χρησιμοποιήσεις reflection, διατύπωσε συγκεκριμένα την ανάγκη σου:

Ποια πληροφορία χρειάζεται να ανακαλύψει το πρόγραμμα κατά την εκτέλεση;

Αν υπάρχει σαφής απάντηση, το reflection μπορεί να είναι το κατάλληλο εργαλείο.

Αν ήδη γνωρίζεις τον τύπο, την ιδιότητα και τη λειτουργία που χρειάζεσαι, ξεκίνα με τον απλούστερο κώδικα.

nikosstit@gmail.com