Sari la conținutul principal

Reflectie

In foarte multe cazuri nu este suficient sa folosim tipuri doar la compile-time ci trebuie sa extragem informatii la runtime despre tipuri de date, functii si alte informatii despre cod. Limbajele cu runtime cum sunt C# si Java si-au castigat popularitatea prin faptul ca expun aceste informatii la runtime prin mecanisme de reflectie. Reflectie inseamna practic ca programul poate sa faca introspectie si sa cunoasca informatii despre codul care se executa.

Cel mai simplu exemplu pentru care C# si Java au devenit populare pe partea de sisteme Cloud este simplul fapt ca avand informatii de cum este scrisa o clasa obiectele pot fi serializate si deserializate automat in diferite formate cum sunt JSON sau XML fara ca acest lucru sa fie explicit scrisa metoda respectiva pentru fiecare clasa.

In C# exista foarte multe metode de extragere a informatiilor despre un tip in principal prin structura Type care poate fi obtinuta prin metoda GetType a oricarei instante sau cuvantul cheie typeof pe un tip. Prin Type se pot extrage:

  • Informatii despre metode/campuri/proprietati/contructori
  • Ierarhia de mostenire a tipului
  • Daca este clasa, structura, clasa abstracta sau interfata
  • Daca este un tip generic
  • Parametri de genericitate daca este cazul
  • Atribute atasate clasei

Cateva exemple de cum putem folosi reflectie se pot vedea mai jos.

var interfaceType = typeof(TInterface);
var implementationType = typeof(TImplementation);

// Putem verifica daca un tip este o interfata.
if (!interfaceType.IsInterface)
{
throw new ArgumentException($"{interfaceType.Name} is not a interface!");
}

// Sau este o clasa.
if (!implementationType.IsClass)
{
throw new ArgumentException($"{implementationType.Name} is not a concrete class!");
}

public class TestClass
{
public TestClass(int intArg)
{
// ...
}

public TestClass(string stringArg)
{
// ...
}
}

// Pentru fiecare tip pentru implementare extragem contructorii pentru a invoca unul.
var constructors = typeof(TestClass).GetConstructors();

foreach (var constructor in constructors)
{
Console.Write("Printing constructor arguments: ");

// Apoi putem extrage informatii despre paramtri construtorului.
foreach (var parameter in constructor.GetParameters())
{
Console.Write("{0} {1}, ", parameter.ParameterType.Name, parameter.Name);
}

Console.WriteLine();

if (constructor.GetParameters().Length == 1 && constructor.GetParameters()[0].ParameterType == typeof(string))
{
// Putem invoca un anumit constructor si sa obtinem o instanta.
var instance = constructor.Invoke("Test");

Console.WriteLine("Created an instance of {0}: {1}", typeof(TestClass).Name, instance);
}
}

Pe langa informatiile generale pe care le avem pentru o clasa mai putem asigna mai multe informatii acesteia prin atribute daca nu ne este de ajuns ce ne ofera clasa si lantul de mostenire. Un atribut in C# este o clasa care mosteneste Attribute si se termina in acest nume care poate fi atasat intre [] la o clasa, metoda, proprietate, camp sau parametru de metoda.

/* Pentru a folosi atribute trebuie sa mostenim Attribute si trebuie sa
* adaugam un atribut pentru a indica unde poate fi folosit acest atribut,
* In acest caz este folosit pe o clasa.
*/
[AttributeUsage(AttributeTargets.Class)]
public class InjectableAttribute : Attribute
{
public LifetimeEnum Lifetime { get; }
public InjectableAttribute(LifetimeEnum lifetime) => Lifetime = lifetime;
}

// Atributul se ataseaza la clasa si i se pot trimite date in constructor.
[Injectable(LifetimeEnum.Transient)]
public class InjectedService
{
// ...
}

// Aici extragem toate tipurile existente din Assembly-ul care se executa.
foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
{
// Pentru atribute non-standard create de noi putem sa extragem lista de atribute care sunt de tipul dat.
var attribute = type.GetCustomAttributes(typeof(InjectableAttribute)).FirstOrDefault();

if (attribute is InjectableAttribute injectableAttribute)
{
Console.WriteLine("Type {0} has attribute {1} with lifetime {2}.", type.Name, typeof(InjectableAttribute).Name, injectableAttribute.Lifetime);
}
}

Atributele sunt necesare in multe aplicatii pentru a fi folosite ca marchere pentru diferite scopuri continand informatii utile pe pot fi atasate prin constructorul atributului. Mai mult decat atat, atributele, sau echivalentul lor in alte limbaje cum sunt adnotarile in Java, se folosesc in programarea orientata pe atribute (attribute-oriented programming sau @OP). Rolul @OP-ului este sa faciliteze adaugarea de logica peste cod fara a-l modifica, de exemplu, daca vrem sa logam apelurile unor metode in mod automat pentru anumite clase putem face o logica in care doar decorand cu un atribut clasa sa logam fiecare apel fara sa intram in metode si sa le modificam.

atenție

Reflectia impune mult timp de executie pentru runtime si din motive de performante nu ar trebui exagerat cu aceste mecanisme. De asemenea, multe mecanisme uzual folosite in aplicatii mari cum ar fi dependency injection sunt deja implementate si optimizate si ar trebui folosite acestea in loc de solutii proprii.

📄️ Injectarea de dependente

Pentru aplicatii moderne s-au creat diferite modalitati prin care sa se simplifice implementarea de la zero a unei aplicatii. Chiar daca exista multe biblioteci care sa faciliteze scrierea aplicatilor pentru diferite cazuri de utilizare se pune problema constructiei si controlului aplicatiei. In programarea procedurala atunci cand se scrie cod acesta apeleaza alt cod care se poate afla in biblioteci sau este expus de catre un framework dictand atat fluxul de control al programului cat si constructia acestuia. Aceasta abordare este problematica deoarece i se impune programului ca fluxul de control a diferitelor componente sa fie dat de implementare si nu de o abstractizare care sa faca codul mai refolosibil si mai usor de urmarit.