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.