Jugando con CallerMemberName | C#
Avanzado
CallerMemberName
es un atributo utilizado en C# desde .Net Framework 4.5 y también esta soportado en WinRT.
Cómo funciona
Este atributo debe ser aplicado a un parámetro de tipo string dentro de un método, de tal forma que a este parámetro se asigne el nombre del método que ha hecho la llamada.
Aunque muchos en internet mencionan CallerMemberName
como una característica del lenguaje, lo cierto es que es una característica del compilador. Es decir el parámetro que se decora con dicho atributo recibe el nombre del método que lo invoca, pero esta asignación se resuelve al momento de compilar el código fuente, no en ejecución.
Esto se hace mucho más evidente cuando agregamos el namespace necesario para usarlo: System.Runtime.CompilerServices
.
Veamos ahora un ejemplo sencillo de uso en una aplicación de consola:
Ejemplo
public class DemoCallerMemberName
{
public void UsingCallerMemberName([CallerMemberName]string callerName = "No asignado")
{
Console.WriteLine("El método que invocó fué: {0}", callerName);
}
}
class Program
{
static void Main(string[] args)
{
var dcmn = new DemoCallerMemberName();
dcmn.UsingCallerMemberName();
Console.ReadLine();
}
}
Output
El método que invocó fué: Main
Qué pasa si se llama desde...?
Dependiendo desde que parte del código se invoque el método el resultado puede ser diferente, así que analizaremos su funcionamiento en más detalle.
Este es el código con el que empezaremos a trabajar:
//DemoCallerMemberName.cs
using System;
using System.Runtime.CompilerServices;
namespace DemoCallerMemberName
{
public class DemoCallerMemberName
{
public string UsingCallerMemberName([CallerMemberName]string callerName = "No asignado")
{
var msg = string.Format("El método que invocó fué: {0}", callerName);
Console.WriteLine(msg);
return msg;
}
public static string stUsingCallerMemberName([CallerMemberName]string callerName = "No asignado")
{
var msg = string.Format("El elemento que invocó fué: {0}", callerName);
Console.WriteLine(msg);
return msg;
}
}
}
//Program.cs
using System;
namespace DemoCallerMemberName
{
class Program
{
static DemoCallerMemberName dcmn = new DemoCallerMemberName();
static void Main(string[] args)
{
var dcmn = new DemoCallerMemberName();
dcmn.UsingCallerMemberName();
Console.ReadLine();
}
}
}
Métodos y propiedades
Cuando lo llamamos desde cualquiera de estos el nombre devuelto es predeciblemente el nombre del métodos o propiedad.
Agregamos la siguiente propiedad en CallerMemberNameDemo
private bool _callingMethodProperty;
public bool CallingMethodProperty
{
get
{
UsingCallerMemberName();
return _callingMethodProperty;
}
set
{
_callingMethodProperty = value;
UsingCallerMemberName();
}
}
Así mismo esté método
public void IndirectCalling()
{
UsingCallerMemberName();
}
Ahora desde nuestro Main los invocamos.
class Program
{
static DemoCallerMemberName dcmn = new DemoCallerMemberName();
static void Main(string[] args)
{
dcmn.UsingCallerMemberName();
dcmn.IndirectCalling();
dcmn.CallingMethodProperty = true;
Console.ReadLine();
}
}
output
El elemento que invocó fué: Main
El elemento que invocó fué: IndirectCalling
El elemento que invocó fué: CallingMethodProperty
Pero pongamonos un poco más rudos, que pasa con los eventos y campos de clase?
Eventos
Dentro de la clase CallerMemberNameDemo
definimos el siguiente delegado y el evento correspondiente:
public delegate void DemoEventHandler(object sender, EventArgs args);
public event DemoEventHandler DemoEvent;
Adicionalmente definimos el primer handler del evento así:
private void OnDemoEvent(object sender, EventArgs args)
{
Console.Write("Default handler->");
UsingCallerMemberName();
}
Y lo inicializamos desde el constructor:
public DemoCallerMemberName()
{
DemoEvent += OnDemoEvent;
}
casí listo, adicionemos un método que nos permita dispara el evento desde afuera de la clase:
public void ActivateEvent()
{
DemoEvent(this, new EventArgs());
}
Terminado, ahora en Program
adicionamos un nuevo handler para el evento e disparamos el evento 'manualmente'
static void dcmn_DemoEvent(object sender, EventArgs args)
{
Console.Write("Program handler->");
dcmn.UsingCallerMemberName();
}
static void Main(string[] args)
{
dcmn.UsingCallerMemberName();
dcmn.IndirectCalling();
dcmn.CallingMethodProperty = true;
dcmn.DemoEvent += dcmn_DemoEvent;
dcmn.ActivateEvent();
Console.ReadLine();
}
output
El elemento que invocó fué: Main
El elemento que invocó fué: IndirectCalling
El elemento que invocó fué: CallingMethodProperty
Default handler->El elemento que invocó fué: OnDemoEvent
Program handler->El elemento que invocó fué: dcmn_DemoEvent
Pese a lo sofisticado del ejemplo, el resultado sigue siendo igual de predecible, pero las cosas están a punto de cambiar...
Campos de clase
Declaremos en nuestra clase Program
un campo estático y asignémosle el valor devuelto por el método UsingCallerMemberName
static string _myTestField = dcmn.UsingCallerMemberName();
output
El elemento que invocó fué: _myTestField
El elemento que invocó fué: Main
El elemento que invocó fué: IndirectCalling
El elemento que invocó fué: CallingMethodProperty
Default handler->El elemento que invocó fué: OnDemoEvent
Program handler->El elemento que invocó fué: dcmn_DemoEvent
Cómo observan, nuestra lógica nos dice que el que está invocando el método es directamente la clase Program
, sin embargo para efectos de CallerMemberName
el invocador es el campo _myTestField
al que se le asigno el resultado del método.
Sigamos con los casos especiales.
Constructores
Los constructores y destructores a la final no son más que métodos al menos desde el punto de vista del IL, pero con que nombre los interpretará CallerMemberName
?
Además hay dos tipos de constructores, los normales y los estáticos. Adicionemos a la clase CallerMemberNameDemo
el llamado al método desde sus dos constructores :
public DemoCallerMemberName()
{
Console.Write("Constructor->");
UsingCallerMemberName();
//esto ya estaba para la parte de eventos
DemoEvent += OnDemoEvent;
}
static DemoCallerMemberName()
{
Console.Write("Constructor Estático->");
stUsingCallerMemberName();
}
No debemos hacer modificaciones en Program, ya que los constructores se llaman automaticamente al instanciar la clase.
output
Constructor Estático->El método que invocó fué: .cctor
Constructor->El elemento que invocó fué: .ctor
_
El elemento que invocó fué: _myTestField
El elemento que invocó fué: Main
El elemento que invocó fué: IndirectCalling
El elemento que invocó fué: CallingMethodProperty
El elemento que invocó fué: ActivateEvent
Default handler->El elemento que invocó fué: OnDemoEvent
Program handler->El elemento que invocó fué: dcmn_DemoEvent
Ahora ya encontramos diferencias, los nombres de los constructores son reportados como
- .cctor : constructor estático
- .ctor : constructor normal
De dónde salen estos nombres?
Como lo mencione anteriormente los constructores son a la final métodos pero no para C# sino para IL, el lenguaje intermedio en el cual se compilan los ensamblados, .ctor
y .cctor
son los nombres de los métodos constructores en IL.
Destructores
Adicionamos el destructor a DemoCallerMemberName
~DemoCallerMemberName()
{
Console.Write("Destructor->");
stUsingCallerMemberName();
}
Y preparamos el Main
para crear un objeto y forzar su destrucción:
static void Main(string[] args)
{
dcmn.UsingCallerMemberName();
dcmn.IndirectCalling();
dcmn.CallingMethodProperty = true;
dcmn.DemoEvent += dcmn_DemoEvent;
dcmn.ActivateEvent();
var localDestructed = new DemoCallerMemberName();
GC.Collect();
Console.ReadLine();
}
output
Constructor Estático->El método que invocó fué: .cctor
Constructor->El elemento que invocó fué: .ctor
El elemento que invocó fué: _myTestField
El elemento que invocó fué: Main
El elemento que invocó fué: IndirectCalling
El elemento que invocó fué: CallingMethodProperty
El elemento que invocó fué: ActivateEvent
Default handler->El elemento que invocó fué: OnDemoEvent
Program handler->El elemento que invocó fué: dcmn_DemoEvent
_
Constructor->El elemento que invocó fué: .ctor
Destructor->El método que invocó fué: Finalize
La llamada al constructor .ctor
es consecuencia del nuevo objeto que hemos creado para destruir, el que verdaderamente nos importa es Finalize
. El método Finalize es otra forma de escribir el destructor y al compilar para IL el método destructor lleva este nombre.
Sobrecargas de Operadores
Que sucede con las sobrecargas de operadores?
Sobrecargaremos el operador de suma [+] en la clase DemoCallerMemberName
public static DemoCallerMemberName operator +(DemoCallerMemberName a1, DemoCallerMemberName a2)
{
Console.Write("Operador sobrecargado->");
stUsingCallerMemberName();
return new DemoCallerMemberName();
}
Ahora en Program
hacemos la suma de instancias
static void Main(string[] args)
{
dcmn.UsingCallerMemberName();
dcmn.IndirectCalling();
dcmn.CallingMethodProperty = true;
dcmn.DemoEvent += dcmn_DemoEvent;
dcmn.ActivateEvent();
var localDestructed = new DemoCallerMemberName();
GC.Collect();
var dummy = dcmn + new DemoCallerMemberName();
Console.ReadLine();
}
output
Constructor Estático->El método que invocó fué: .cctor
Constructor->El elemento que invocó fué: .ctor
El elemento que invocó fué: _myTestField
El elemento que invocó fué: Main
El elemento que invocó fué: IndirectCalling
El elemento que invocó fué: CallingMethodProperty
El elemento que invocó fué: ActivateEvent
Default handler->El elemento que invocó fué: OnDemoEvent
Program handler->El elemento que invocó fué: dcmn_DemoEvent
Constructor->El elemento que invocó fué: .ctor
_
Constructor->El elemento que invocó fué: .ctor
Operador sobrecargado->El método que invocó fué: op_Addition
Constructor->El elemento que invocó fué: .ctor
Los 2 llamados a constructores .ctor
son consecuencia del nuevo objeto que hemos creado para la suma y del objeto retornado para la suma, el que verdaderamente nos importa es op_Addition
.
op_Addition
es el nombre de la sobrecarga del operador + en IL.
Métodos anónimos
Creamos el siguiente método en la clase DemoCallerMemberName
, allí creamos un método anómimo y lanzamos su ejecución
public void AnonymousLocalMethod()
{
Action anonyMethod = () =>
{
Console.Write("anonymous LocalMethod->");
UsingCallerMemberName();
};
anonyMethod.Invoke();
}
Lo invocamos desde Program
static void Main(string[] args)
{
dcmn.UsingCallerMemberName();
dcmn.IndirectCalling();
dcmn.CallingMethodProperty = true;
dcmn.DemoEvent += dcmn_DemoEvent;
dcmn.ActivateEvent();
var localDestructed = new DemoCallerMemberName();
GC.Collect();
var dummy = dcmn + new DemoCallerMemberName();
dcmn.AnonymousLocalMethod();
Console.ReadLine();
}
output
Constructor Estático->El método que invocó fué: .cctor
Constructor->El elemento que invocó fué: .ctor
El elemento que invocó fué: _myTestField
El elemento que invocó fué: Main
El elemento que invocó fué: IndirectCalling
El elemento que invocó fué: CallingMethodProperty
El elemento que invocó fué: ActivateEvent
Default handler->El elemento que invocó fué: OnDemoEvent
Program handler->El elemento que invocó fué: dcmn_DemoEvent
Constructor->El elemento que invocó fué: .ctor
Constructor->El elemento que invocó fué: .ctor
Operador sobrecargado->El método que invocó fué: op_Addition
Constructor->El elemento que invocó fué: .ctor
_
anonymous LocalMethod->El elemento que invocó fué: AnonymousLocalMethod
Es decir, cuando usamos un método anonimo en un contexto local, el nombre reportado por CallingMemberName
es el del método nombrado que lo contiene.
Ahora hacemos algo similar, solo que creamos el método anónimo a nivel de clase y creamos un método para disparar su invocación:
Action _anoymousClassMethod = () =>
{
Console.Write("anonymous ClassMethod->");
stUsingCallerMemberName();
};
public void AnonymousClassMethod()
{
_anoymousClassMethod.Invoke();
}
Desde luego lo invocamos desde el Main
static void Main(string[] args)
{
dcmn.UsingCallerMemberName();
dcmn.IndirectCalling();
dcmn.CallingMethodProperty = true;
dcmn.DemoEvent += dcmn_DemoEvent;
dcmn.ActivateEvent();
var localDestructed = new DemoCallerMemberName();
GC.Collect();
var dummy = dcmn + new DemoCallerMemberName();
dcmn.AnonymousLocalMethod();
dcmn.AnonymousClassMethod();
Console.ReadLine();
}
output
Constructor Estático->El método que invocó fué: .cctor
Constructor->El elemento que invocó fué: .ctor
El elemento que invocó fué: _myTestField
El elemento que invocó fué: Main
El elemento que invocó fué: IndirectCalling
El elemento que invocó fué: CallingMethodProperty
El elemento que invocó fué: ActivateEvent
Default handler->El elemento que invocó fué: OnDemoEvent
Program handler->El elemento que invocó fué: dcmn_DemoEvent
Constructor->El elemento que invocó fué: .ctor
Constructor->El elemento que invocó fué: .ctor
Operador sobrecargado->El método que invocó fué: op_Addition
Constructor->El elemento que invocó fué: .ctor
anonymous LocalMethod->El elemento que invocó fué: AnonymousLocalMethod
_
anonymous ClassMethod->El método que invocó fué: _anoymousClassMethod
En este caso, el resultado es de nuevo el nombre del campo definido a nivel de clase, que es el que en este caso nos permite acceder al método anónimo.
Métodos invocados dinámicamente
Adicionamos este método a CallerMemberNameDemo
para invocar dinámicamente a stUsingCallerMemberName
.
public void DymamicInvocationMethod()
{
var t = this.GetType();
var m = t.GetMethod("stUsingCallerMemberName");
m.Invoke(null, new object[] { "" });
}
output
Constructor Estático->El método que invocó fué: .cctor
Constructor->El elemento que invocó fué: .ctor
El elemento que invocó fué: _myTestField
El elemento que invocó fué: Main
El elemento que invocó fué: IndirectCalling
El elemento que invocó fué: CallingMethodProperty
El elemento que invocó fué: ActivateEvent
Default handler->El elemento que invocó fué: OnDemoEvent
Program handler->El elemento que invocó fué: dcmn_DemoEvent
Constructor->El elemento que invocó fué: .ctor
Constructor->El elemento que invocó fué: .ctor
Operador sobrecargado->El método que invocó fué: op_Addition
Constructor->El elemento que invocó fué: .ctor
anonymous LocalMethod->El elemento que invocó fué: AnonymousLocalMethod
anonymous ClassMethod->El método que invocó fué: _anoymousClassMethod
_
Invocación dinámica->El método que invocó fué:
Para CallerMemberName
es imposible definir el nombre del método invocador si el llamado es hecho dinámicamente.
Y tiene mucho sentido, recuerden que CallerMemberName
resuelve el nombre del invocador en tiempo de compilación, por ende es imposible determinar el origen de un llamado dinámico que ocurre en tiempo de ejecución.
Otros atributos similares
Los invito a revisar otroa atributos 'hermanos' de CallerMemberName
aunque mucho menos complejos.
- CallerFilePath
- CallerLineNumber
Espero que este tema haya sido de su agrado!
byte!