20. Atrybuty i mechanizm refleksji

20. Atrybuty i mechanizm refleksji

Autor: Paweł Kruczkowski

Opublikowano: 2/5/2007, 12:00 AM

Liczba odsłon: 102268

Tematem dzisiejszego - ostatniego już w tej części nauki programowania w języku C# 2.0 na łamach portalu CentrumXP.pl – artykułu, będą atrybuty oraz mechanizm refleksji. Pokrótce zdefiniujemy sobie pojęcie atrybutu oraz przybliżymy sobie sposób jego użycia w oparciu o przykłady. Opowiemy sobie również o refleksji: co to jest, jakie ma zalety i wady oraz gdzie ją stosować i w jaki sposób.

Na początku wprowadźmy sobie prawidłowe pojęcie atrybutu. Najprościej mówiąc atrybut to mechanizm, który służy do dodawania do naszego programu metadanych za pomocą instrukcji bądź innych danych. Metadane to informacje jakie są przechowywane w plikach wykonywalnych (pliki z rozszerzeniem .exe czy .dll), i które opisują typy czy metody umieszczone w tych plikach. Z atrybutami ściśle powiązany jest mechanizm refleksji, gdyż program używa go do odczytu własnych metadanych bądź metadanych innych programów. Innymi słowy, nasz program „rzuca refleksję” sam na siebie bądź na program, z którego chcemy sczytać właśnie metadane, a następnie te metadane można wyświetlić na ekranie komputera lub dzięki nim zmodyfikować dalsze działanie naszej aplikacji.

Atrybuty są dostarczane nie tylko przez system (przez środowisko CLR). Możemy bowiem tworzyć własne atrybuty i używać ich do własnych celów (najczęściej robi się tak przy używaniu mechanizmu refleksji). Jednak większość programistów używa tych wbudowanych atrybutów.

Powróćmy jeszcze na chwilkę do definicji atrybutów. Atrybuty to obiekty, które reprezentują dane wiązane z pewnym elementem w programie. Elementy te nazywamy adresatami (ang. target) atrybutu.

Poniższa tabela prezentuje wszystkie możliwe adresaty atrybutów:

Nazwa adresata Zastosowanie
All Można stosować do dowolnego elementu: pliku wykonywalnego, konstruktora, metody, klasy, zdarzenia, pola, właściwości czy struktury
Assembly Można stosować do samego podzespołu (pliku wykonywalnego)
Class Można stosować do klasy
Constructor Można stosować do konstruktora
Delegate Można stosować do delegata
Enum Można stosować do wyliczenia
Event Można stosować do zdarzenia
Field Można stosować do pola
Interface Można stosować do interfejsu

Method Można stosować do metody
Parametr Można stosować do parametru metody
Property Można stosować do właściwości (get i set)
ReturnValue Można stosować do zwracanej wartości
Struct Można stosować do struktury

Aby przypisać atrybut do adresata musimy umieścić go w nawiasach kwadratowych przed elementem docelowym (klasą, metodą czy właściwością etc.). Na przykład:

[Serializable]
      class MojaKlasa

      { … }

W powyższym fragmencie kodu znacznik atrybutu znajduje się w nawiasach kwadratowych bezpośrednio przed adresatem (czyli klasą MojaKlasa). Tak na marginesie, atrybut [Serializable] to jeden z najczęściej używanych atrybutów przez programistę. Umożliwia on serializację klasy na np. dysk lub poprzez sieć komputerową.

Jak już wyżej zostało napisane, programiści używają nie tylko atrybutów, jakie dostarcza nam system, ale również piszą swoje własne.

Wyobraźmy sobie sytuację, że jesteśmy twórcami klasy, która wykonuje operacje matematyczne (np. dodawanie i odejmowanie). Informacje o autorze tej klasy (imię, nazwisko, data oraz krótki komentarz) trzymamy w bazie danych, a w naszym programie w postaci komentarza.  Z czasem jednak nasza klasa zostanie poszerzona przez kogoś innego o dodatkowe funkcjonalności (operacje mnożenia i dzielenia). Owszem, programista, który poszerzy naszą klasę może opisać swoje zmiany w kolejnym komentarzu, ale..lepszym rozwiązaniem byłoby stworzenie mechanizmu, który automatycznie aktualizowałby nasz wpis w bazie o autorze tejże klasy na podstawie nowego komentarza. W takiej sytuacji idealnym rozwiązaniem jest stworzenie własnego atrybutu, który będzie działał w programie jak komentarz. Drugą zaletą takiego podejścia jest to, że atrybut ten będzie pozwalał nam na programowe pobranie treści wspomnianego komentarza i na jej podstawie aktualizację bazy danych.

Napiszmy więc taki program, który będzie prezentował powyższy problem biznesowy:

using System;
using System.IO;
 
namespace CentrumXP_20
{
    // deklaracja wlasnego atrybutu
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Field |
                    AttributeTargets.Property, AllowMultiple = true)]
    //deklaracja klasy, ktora dziedziczy po klasie System.Attribute
    public class MojPierwszyAtrybut : System.Attribute
    {
        // wlasciwosci odpowiadajace wpisowi do bazy danych na temat tworcy klasy
        private int autorID;
        /// ID autora klasy
        public int AutorID
        {
            get { return autorID; }
            set { autorID = value; }
        }
        private string imie;
        /// imie autora klasy
        public string Imie
        {
            get { return imie; }
            set { imie = value; }
        }
        private string nazwisko;
        /// nazwisko autora klasy
        public string Nazwisko
        {
            get { return nazwisko; }
            set { nazwisko = value; }
        }
        private string data;
        // data stworzenia klasy
        public string Data
        {
            get { return data; }
            set { data = value; }
        }
        private string komentarz;
        // krotki komentarz na temat klasy
        public string Komentarz
        {
            get { return komentarz; }
            set { komentarz = value; }
        }
 
        // konstruktor klasy MojPierwszyAtrybut
        public MojPierwszyAtrybut(int autorID, string imie, string nazwisko, string data, string komentarz)
        {
            this.autorID = autorID;
            this.imie = imie;
            this.nazwisko = nazwisko;
            this.data = data;
            this.komentarz = komentarz;
        }
 
        //przypisanie atrybutu do klasy
        [MojPierwszyAtrybut(1, "Paweł", "Kruczkowski", "22-10-2006", "dodawanie i odejmowanie 2 liczb całkowitych")]
        [MojPierwszyAtrybut(2, "Gal", "Anonim", "24-10-2006", "uzupełnienie klasy o metody mnożenia i dzielenia")]
        public class Operacje
        {
            public int Dodawanie(int a, int b)
            {
                return a + b;
            }
 
            public int Odejmowanie(int a, int b)
            {
                return a - b;
            }
 
            public int Mnozenie(int a, int b)
            {
                return a * b;
            }
 
            public double Dzielenie(int a, int b)
            {
                return a / b;
            }
        }
 
        class Glowna
        {
            public static void Main()
            {
                Operacje o = new Operacje();
                Console.WriteLine("Podaj pierwszą liczbę całkowitą:");
                int a = Int32.Parse(Console.ReadLine());
                Console.WriteLine("Podaj drugą liczbę całkowitą:");
                int b = Int32.Parse(Console.ReadLine());
                Console.WriteLine("Wynik dodawania tych liczb to: {0}.", o.Dodawanie(a, b));
                Console.WriteLine("Wynik odejmowania tych liczb to: {0}.", o.Odejmowanie(a, b));
                Console.WriteLine("Wynik mnożenia tych liczb to: {0}.", o.Mnozenie(a, b));
                Console.WriteLine("Wynik dzielenia tych liczb to: {0}.", o.Dzielenie(a, b));
            }
        }
    }
}

Powyższy przykład prezentuje sposób definiowania i używania artybutów. Jak widzimy, atrybuty tworzymy w klasie, która dziedziczy po System.Attribute. W klasie tej umieszczamy wszystkie informacje dla odpowiednich elementów, które będą bezpośrednio powiązane z atrybutem. Elementy te są definiowane w następujący sposób:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Field |
      AttributeTargets.Property, AllowMultiple = true)]

AttributeUsage to po prostu metaatrybut (udostępnia dane, które opisują metadane). Do konstruktora tego atrybutu należy przekazać 2 parametry:

  • Adresaty atrybutu: klasa, metoda, konstruktor, zmienne oraz właściwości
  • Określenie, czy dana klasa może mieć przypisane więcej niż jeden atrybut MojPierwszyAtrybut (warunek spełniony, bo AllowMultiple = true).

Stworzyliśmy już własny atrybut, a więc możemy umieścić go przed jakimś adresatem. W naszym przykładzie będzie to klasa Operacje, która definiuje 4 metody matematyczne. W taki właśnie sposób atrybut ten będzie nam pomocny przy pilnowaniu informacji na temat twórcy danych metod.

Po skompilowaniu i uruchomieniu powyższego programu otrzymamy następujące wyniki:

Jak łatwo zauważyć, program bez problemu się skompilował i uruchomił, ale nasuwa się pytanie: gdzie są te nasze atrybuty w programie? Poniżej przedstawimy technikę umożliwiająca dostęp do nich w czasie – co należy podkreślić- wykonywania się programu. Mechanizm refleksji, bo o nim mowa, pozwala na przeglądanie i używanie metadanych, czy też na odkrywanie typów plików wykonywalnych.

Do zapamiętania: mechanizm refleksji w języku C# 2.0 korzysta z klas umieszczonych w przestrzeni nazw System.Reflection.

Na początku zaprezentujemy przykład, w którym będziemy przeglądać metadane. Aby to zrealizować musimy utworzyć obiekt typu MemberInfo (klasa ta znajduje się w przestrzeni nazw System.Reflection):

using System;
using System.IO;
using System.Reflection;
 
namespace CentrumXP_20
{
    // deklaracja wlasnego atrybutu
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method |
     AttributeTargets.Constructor | AttributeTargets.Field |
     AttributeTargets.Property, AllowMultiple = true)]
    //deklaracja klasy, ktora dziedziczy po klasie System.Attribute
    public class MojPierwszyAtrybut : System.Attribute
    {
        // wlasciwosci odpowiadajace wpisowi do bazy danych na temat tworcy klasy
        private int autorID;
        /// ID autora klasy
        public int AutorID
        {
            get { return autorID; }
            set { autorID = value; }
        }
        private string imie;
        /// imie autora klasy
        public string Imie
        {
            get { return imie; }
            set { imie = value; }
        }
        private string nazwisko;
        /// nazwisko autora klasy
        public string Nazwisko
        {
            get { return nazwisko; }
            set { nazwisko = value; }
        }
        private string data;
        // data stworzenia klasy
        public string Data
        {
            get { return data; }
            set { data = value; }
        }
        private string komentarz;
        // krotki komentarz na temat klasy
        public string Komentarz
        {
            get { return komentarz; }
            set { komentarz = value; }
        }
 
        // konstruktor klasy MojPierwszyAtrybut

  public MojPierwszyAtrybut(int autorID, string imie, string nazwisko, string  

  data, string komentarz)
        {
            this.autorID = autorID;
            this.imie = imie;
            this.nazwisko = nazwisko;
            this.data = data;
            this.komentarz = komentarz;
        }
 
        //przypisanie atrybutu do klasy
        [MojPierwszyAtrybut(1, "Paweł", "Kruczkowski", "22-10-2006", "dodawanie i odejmowanie
        2 liczb całkowitych")]
        [MojPierwszyAtrybut(2, "Gall", "Anonim", "24-10-2006", "uzupełnienie klasy o metody
        mnożenia i dzielenia")]
        public class Operacje
        {
            public int Dodawanie(int a, int b)
            {
                return a + b;
            }
 
            public int Odejmowanie(int a, int b)
            {
                return a - b;
            }
 
            public int Mnozenie(int a, int b)
            {
                return a * b;
            }
 
            public double Dzielenie(int a, int b)
            {
                return a / b;
            }
        }
 
        class Glowna
        {
            public static void Main()
            {
                object[] mojeAtrybuty;
                Operacje o = new Operacje();
                Console.WriteLine("Podaj pierwszą liczbę całkowitą:");
                int a = Int32.Parse(Console.ReadLine());
                Console.WriteLine("Podaj drugą liczbę całkowitą:");
                int b = Int32.Parse(Console.ReadLine());
                Console.WriteLine("Wynik dodawania tych liczb to: {0}.", o.Dodawanie(a, b));
                Console.WriteLine("Wynik odejmowania tych liczb to: {0}.", o.Odejmowanie(a,
                b));
                Console.WriteLine("Wynik mnożenia tych liczb to: {0}.", o.Mnozenie(a, b));
                Console.WriteLine("Wynik dzielenia tych liczb to: {0}.", o.Dzielenie(a, b));
 
                //tworzymy obiekt klasy MemberInfo i pobieramy atrybuty klasy
                MemberInfo mi = typeof(Operacje);
                mojeAtrybuty = mi.GetCustomAttributes(typeof(MojPierwszyAtrybut), false);
                //przechodzimy po atrybutach
                foreach (Object obj in mojeAtrybuty)
                {
                    MojPierwszyAtrybut mpa = (MojPierwszyAtrybut) obj;
                    Console.WriteLine("");
                    Console.WriteLine("Identyfikator autora metod: {0}.", mpa.AutorID);
                    Console.WriteLine("Imię i nazwisko autora metod: {1} {0}.", mpa.Imie,
                    mpa.Nazwisko);
                    Console.WriteLine("Data stworzenia metod: {0}", mpa.Data);
                    Console.WriteLine("Krótki komentarz autora: {0}", mpa.Komentarz);
                }
            }
        }
    }
}

Obiekt mi klasy MemberInfo potrafi sprawdzić atrybuty oraz pobrać je z danej klasy:

MemberInfo mi = typeof(Operacje);

W powyższej linijce wywołaliśmy operator typeof na klasie Operacje, co powoduje zwrócenie obiektu pochodnego od klasy MemberInfo. Następnie wywołujemy metodę GetCustomAttributes() na obiekcie mi. Do metody tej przekazujemy typ szukanego atrybutu. Metodę tę również informujemy o tym, że jedynym miejscem do wyszukiwania atrybutów jest klasa: MojPierwszyAtrybut  (dlatego drugi parametr tej metody to fałsz):

mojeAtrybuty = mi.GetCustomAttributes(typeof(MojPierwszyAtrybut), false);

Po uruchomieniu powyższego przykładu, program wyświetli na naszym ekranie wszystkie dostępne metadane:

Na koniec przytoczymy książkowy przykład na odkrywanie typów plików wykonywalnych (plik o rozszerzeniu np. dll). Jak już zostało wyżej napisane, mechanizm refleksji jest rewelacyjnym mechanizmem umożliwiającym sprawdzanie zawartości takich plików. Spójrzmy więc na poniższy przykład:

using System;
using System.IO;
using System.Reflection;
 
namespace CentrumXP_20
{
    public class MojaKlasa
    {
        static void Main()
        {
            Assembly assembly = Assembly.Load("Mscorlib.dll");
            Type[] typ = assembly.GetTypes();
            int i = 0;
            foreach (Type t in typ)
            {
                Console.WriteLine("{0} - {1}", i, t.FullName);
                i++;
            }
        }
    }
}

Na początku za pomocą statycznej metody Load() dynamicznie ładujemy główną bibliotekę Mscorlib.dll (zawiera ona wszystkie główne klasy platformy .NET). Następnie wywołujemy na obiekcie assembly klasy Assembly metodę GetTypes(), która zwraca tablicę obiektów Type. Obiekt typu Type to chyba jeden z najważniejszych rzeczy, jakie dostarcza nam refleksja w C# 2.0, bowiem reprezentuje deklaracje typu (np. klasy, tablice itp.). Na koniec petlą foreach „przechodzimy” po wszystkich typach jakie zawiera biblioteka Mscorlib.dll.

Poniżej przedstawiamy jedynie fragment naszych wyników jakie uzyskamy po uruchomieniu powyższego przykładu:

Temat niniejszego artykułu nie należał do łatwych, ale mamy nadzieję, że dzięki niemu poznaliśmy podstawowe informacje związane z atrybutami oraz mechanizmem refleksji, co w przyszłości powinno zaowocować poszerzeniem zdobytej tutaj wiedzy o kolejne ważne aspekty tych zagadnień.

Dzisiejszy artykuł jest również ostatnim z drugiej serii artykułów, które ukazują się na łamach portalu Centrum.XP. Miejmy nadzieję, że wszystkie omówione na łamach portalu tematy przybliżą Państwa do programowania w  języku C# 2.0 i zaowocują wieloma aplikacjami .NETowymi, których Państwo będziecie autorami.

Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży
Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży

Wydarzenia