Guest Post: Ottenere informazioni sul chiamante di un metodo in C# 5
Questo post è stato scritto da Antonio Pelleriti (twitter @zetanove)
La principale novità introdotta dalle specifiche di C# 5.0, e quindi quella conosciuta ai più, è senza dubbio il supporto alla programmazione asincrona, integrata nel linguaggio mediante le parole chiave async/await.
Una seconda novità che probabilmente passa in secondo piano, magari perché dedicata ad ambiti e aspetti un po’ più specifici, ma che come vedremo può tornare utile anche nella vita quotidiana di uno sviluppatore (vedremo in particolare la sua utilità a supporto del paradigma MVVM) è l’introduzione dei cosiddetti Caller Info Attributes.
Avete presente il servizio dei gestori telefonici che permette di ottenere informazioni sul numero chiamante? Applicatelo a un programma C# e pensate al chiamante come al membro che invoca un altro metodo.
Soprattutto per scopi di debug, di logging e diagnostica, spesso può essere utile conoscere il chiamante di un metodo e ottenerne informazioni di vario genere.
Prima di C# 5, per ottenere tali informazioni era necessario ricorrere alle classi StackFrame e StackTrace del namespace System.Diagnostics, e alle funzionalità di Reflection, che, come è noto, possono anche influire pesantemente sulle prestazioni, ed il loro funzionamento può avvenire esclusivamente a runtime: ha poco senso accedere allo stack delle chiamate se il programma non è in esecuzione.
Ecco un semplice esempio in una applicazione console che ricava le informazioni sul metodo attuale, e le stampa:
using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
MioMetodo();
Console.ReadLine();
}
public static void MioMetodo()
{
Log();
}
private static void Log()
{
var sf = new StackFrame(1, true);
var methodName = sf.GetMethod().Name;
var sourceFile = sf.GetFileName();
var lineNumber = sf.GetFileLineNumber();
Console.WriteLine("[{1}] - {0} ({2}:{3})",
methodName, DateTime.Now, sourceFile, lineNumber);
}
}
}
Inoltre i metodi della classe StackFrame, come GetFileNumber o GetFileName, ottengono le informazioni estraendole dai simboli di debug, se disponibili.
Nell’esempio precedente, il metodo Log ottiene informazioni sul suo chiamante, stampandole sulla console.
Caller Information in C# 5.0
In C# 5.0, è possibile ottenere tali informazioni in maniera molto più immediata, mediante l’utilizzo dei tre nuovi attributi che vedremo a breve, ed a tempo di compilazione: cioè i valori delle informazioni sul chiamante vengono generati come valori letterali in Intermediate Language (IL).
A differenza dei risultati della proprietà StackTrace, i risultati non sono così interessati da offuscamento e non vengono ricavati con Reflection o dai simboli di debug, quindi non risentono di problemi di performance.
I tre attributi suddetti, definiti nel namespace System.Runtime.CompilerServices, sono:
• CallerMemberName : è il nome del membro chiamante;
• CallerfilePath : è il percorso del file contenente il sorgente del chiamante;
• CallerLineNumber: è il numero di riga del chiamante all’interno del file sorgente.
Essi permettono di ottenere informazioni sul chiamante di un metodo, sia esso un altro metodo, una proprietà, un evento.
Il seguente metodo mostra come usare i tre attributi, che verranno utilizzati dal compilatore per assegnare i valori ai corrispondenti parametri opzionali:
public static void Log2(
[CallerMemberName] string memberName = null,
[CallerFilePath] string filePath = null,
[CallerLineNumber] int lineNumber = 0)
{
Console.WriteLine(memberName);
Console.WriteLine(filePath);
Console.WriteLine(lineNumber);
}
Il metodo non fa altro che stampare i valori dei suoi argomenti.
Normalmente il compilatore assegnerebbe ai parametri opzionali i valori indicati direttamente nella firma del metodo ma, con l’uso degli attributi Caller Info, a essi sarà assegnato un valore ricavato direttamente dal contesto.
Per esempio, utilizzando ora il metodo Log, come implementato sopra, all’interno del metodo Main:
class Program
{
static void Main(string[] args)
{
Log2();
}
}
Al parametro memberName sarà assegnato il nome del metodo chiamante, in questo caso Main, al secondo il percorso completo del file in cui è definita la classe Program, al terzo il numero di riga all’interno del file precedente al quale avviene l’invocazione del metodo Log2.
Se il metodo Log2 venisse invocato all’interno di una proprietà, CallerMemberName restituirebbe naturalmente il nome di quest’ultima.
La figura seguente dimostra come le informazioni sono ricavata a tempo di compilazione e inserite come stringhe nel codice IL.
Il metodo Log2 viene infatti invocato direttamente con i parametri che rappresentano le informazioni sul chiamante
La pagina di MSDN dedicata all’argomento fornisce ulteriori dettagli: http://msdn.microsoft.com/en-us/library/hh534540.aspx.
L’interfaccia INotifyPropertyChanged
Oltre all’uso appena visto per ricavare informazioni di debug, o per tracciare l’esecuzione di un programma, uno ancora più pratico è quello di usare gli attributi Caller Info nell’implementazione dell’interfaccia INotifyPropertyChanged.
Se si utilizza il pattern architetturale MVVM, approccio frequente e consigliato ad esempio con applicazioni che definiscono la propria interfaccia grafica in XAML (Windows Phone, Windows RT, Universal Apps e così via), si rende spesso necessario aggiornare l’interfaccia grafica nel momento in cui una proprietà di un’oggetto ha cambiato valore.
Ecco un esempio di classe che implementa l’interfaccia,
using System;
using System.ComponentModel;
class Class1: INotifyPropertyChanged
{
private string _field;
public event PropertyChangedEventHandler PropertyChanged;
public string OldStyleField
{
get
{
return _field;
}
set
{
if (value != _field)
{
_field = value;
OnPropertyChangedOldStyle("OldStyleField");
}
}
}
public string Field
{
get
{
return _field;
}
set
{
if (value != _field)
{
_field = value;
OnPropertyChanged();
}
}
}
private void OnPropertyChangedOldStyle(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private void OnPropertyChanged([CallerMemberName]string name=null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
Senza l’esistenza di CallerMemberName sarebbe necessario, come potete notare all’interno del ramo set della proprietà OldStyleField, indicare il nome della proprietà in maniera letterale, per ognuna delle proprietà della classe, con il rischio di sbagliare per distrazione o per la fretta nel copia e incolla, e introducendo così dei bug non sempre immediatamente visibili e facilmente scovabili.
La proprietà Field invece, quando cambia il valore da memorizzare nel campo _field, invoca il metodo OnPropertyChanged che fa uso dell’attributo CallerMemberName, senza necessità quindi di indicare manualmente il nome della proprietà che ha cambiato valore.
Per ulteriori informazioni e approfondimenti, sull’argomento trattato o in generale sul linguaggio C# 5.0, non posso che consigliarvi la lettura del mio libro “Programmare C# 5.0, guida completa” edito da LSWR, sul quale trovate maggiori informazioni nella pagina http://www.antoniopelleriti.it/page/libro-csharp.