A volte capita di studiare un argomento e, successivamente, di "dimenticarlo" per scarso utilizzo, salvo poi tornare alla memoria quando, per un motivo qualunque, ci si sbatte la testa in un progetto reale. Ieri mi è capitato proprio questo quando, implementando una libreria di test da dare in pasto ad NUnit, stavo implementando un test che verificava la serializzabilità di una classe... "Bene", mi sono detto: "Adesso implemento due test, che chiamerò IsSerializable e CanBeSerialized che verificheranno rispettivamente l'avvenuta decorazione con l'attributo SerializableAttribute e l'effettiva possibilità di serializzare una istanza". Pronti-via, ho scritto il normale codice con il quale è possibile recuperare gli attributi utilizzati per decorare una classe, adattandolo alla ricerca dell'attributo richiesto:
Type theType = typeof(ValidationRule);
SerializableAttribute[] attributes = (SerializableAttribute[]) theType.GetCustomAttributes(typeof(SerializableAttribute), true);
Con un po' di sorpresa, ho constatato che l'array non conteneva elementi, anche se la classe ValidationRule era effettivamente decorata con SerializableAttribute. Ho quindi creato un piccolo progettino per testare il "problema", definendo due classi, rispettivamente un attributo ed una "cavia":
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class MyCustomAttribute : System.Attribute
{
public string Message=string.Empty;
}
[MyCustom(Message="Custom attribute usage")]
[Obsolete("Oltre che obsoleta, è addirittura inutile.")]
[PrincipalPermission(SecurityAction.Demand)] //richiede "using System.Security.Permissions"!
[Serializable()]
public class Car
{
}
Una bella compilazione (effettuata sia con il compilatore commerciale, sia con qllo di Rotor), e via a sbirciare il codice con ILDASM:
Vedete la parolina serializable lì in alto? :-) Cosa significa? In poche parole, significa che non tutti gli attributi sono "compilati" allo stesso modo o, se preferite, che il compilatore conosce "a priori" alcuni attributi. Mosso dalla curiosità, mi sono detto: "perchè non controllare nel sorgente del compilatore?". A questo punto, grazie al (paziente) supporto del prode Gianluca Carucci ho implementato il pattern multithreaded geeking, ossia abbiamo cercato in parallelo sia nel sorgente di Mono, sia in qllo di Rotor, ed il risultato è decisamente interessante: spulciando tra i sorgenti di quest'ultimo, infatti, si trova il file custattr.cpp, che contiene due sezioni interessanti, la prima delle quali contiene la lista degli attributi "Custom-per-finta" (poi capiremo il motivo di questa definizione) conosciuti by design:
Più in basso, infine, c'è il blocco switch-case che testa gli attributi conosciuti:
Come riportato dal file di documentazione csharp_incremental.html incluso nella documentazione di Rotor, esistono 3 famiglie di attributi: Custom, Pseudo-Custom (Serializable fa parte di questi) e Compiler-Generated. Risulta ora evidente il motivo per il quale Reflection fallisce su alcuni attributi, essendo in grado di rilevare solo qlli di tipo "Custom".
Feature o bug? Sicuramente è una feature, anche se ne avrei fatto volentieri a meno: se è vero, infatti, che è possibile sopperire al problema nel caso dell'attributo SerializableAttribute semplicemente testando la proprietà IsSerializable della classe System.Type, non è però assicurabile la possibilità di recuperare le informazioni di tutti gli attributi non custom. Per esempio, per esplicita ammissione di un membro del team di sviluppo del CLR, non è attualmente possibile recuperare le informazioni relative agli attributi dedicati alla Code Access Security. Francamente, avrei preferito un approccio più omogeneo...
E con Whidbey? Appena ho un momento libero, ci provo... :-)