Jugando con CallerMemberName | C#

Avanzado

CallerMemberName es un atributo utilizado en C# desde .Net Framework 4.5 y también esta soportado en WinRT.

screenshot CallerMemberName

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!