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.