Amélioration des informations de diagnostic sur une méthode (C# 10)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 10.0.

Il existe des attributs permettant d’indiquer des informations sur l’appelant d’une méthode. Avant C# 10, il existait 3 attributs:

C# 10 permet de rajouter l’attribut CallerArgumentExpressionAttribute pour indiquer sous forme d’une chaine de caractères l’expression à l’origine de la valeur d’un paramètre de la fonction courante.

Attributs de diagnostic avant C# 10

Tous ces attributs peuvent être utilisés pour apporter des informations de diagnostic sur la façon dont une méthode est appelée. Ces informations peuvent, par exemple, être logguées.

Par exemple si on considère le code suivant:

public class Example
{
  public void CallingMethod()
  {
    string calleeSecondArgument = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor...";
    int firstInteger = 9;
    int secondInteger = 610;

    this.Callee(firstInteger + secondInteger, calleeSecondArgument, true);
  }

  private void Callee(int firstArgument, string secondArgument, bool thirdArgument)
  {
    Console.WriteLine(firstArgument);
    Console.WriteLine(secondArgument);
    Console.WriteLine(thirdArgument);
  }
}

Sans surprise la valeur des 3 arguments est affichée dans la console:

619
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor...
True

Dans Callee(), on peut facilement afficher la valeur de chaque argument. Dans le but d’avoir des informations supplémentaires au moment de l’exécution, on peut utiliser les attributs indiqués précédemment sous la forme d’arguments supplémentaires de la méthode:

using System.Runtime.CompilerServices;

private void Callee(int firstArgument, string secondArgument, bool thirdArgument,
  [CallerMemberName] string memberName = "",
  [CallerFilePath] string sourceFilePath = "",
  [CallerLineNumber] int sourceLineNumber = 0)
{
  Console.WriteLine(firstArgument);
  Console.WriteLine(secondArgument);
  Console.WriteLine(thirdArgument);

  Console.WriteLine(memberName);
  Console.WriteLine(sourceFilePath);
  Console.WriteLine(sourceLineNumber);
}

Quand on rajoute les arguments avec les attributs CallerMemberName, CallerFilePath et CallerLineNumber:

  • Il est obligatoire d’ajouter une valeur par défaut si une erreur de compilation est générée,
  • L’appel de la méthode Callee() n’est pas modifié:
    this.Callee(firstInteger + secondInteger, calleeSecondArgument, true);
    

Le résultat de l’exécution est:

619
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor...
True
CallingMethod
C:\MyStuff\Dev\Example\ExampleCS10\CallerExpresssionArgumentFeature.cs
19

Comme on peut le voir, les arguments avec les attributs de diagnostic ne comportent plus les valeurs par défaut mais sont interprétés par le compilateur.

Précisions sur la valeur de CallerMemberName

La valeur renvoyée par l’argument CallerMemberName change suivant la nature de la méthode appelante:

  • Dans le cas d’une méthode ou propriété: la valeur sera le nom de la méthode ou de la propriété comme on a pu le voir précédemment.
  • Dans le cas d’un constructeur: la valeur sera ".ctor", par exemple:
    internal class Example
    {
      public Example()
      {
        this.Callee();
      }
    
      private void Callee([CallerMemberName] string memberName = "")
      {
        Console.WriteLine(memberName);
      }
    }
    

    Le résultat est:

    .ctor
    
  • Dans le cas d’un constructeur statique: ".cctor", par exemple:
    internal class Example
    {
      static Example()
      {
        var example = new Example();
        example.Callee();
      }
    
      private void Callee([CallerMemberName] string memberName = "")
      {
        Console.WriteLine(memberName);
      }
    }
    

    Le résultat est:

    .cctor
    
  • Pour une surcharge d’opérateur: la valeur sera du type "op_<nom de l'opérateur>", par exemple:
    internal class Example
    {
      public static Example operator +(Example a) => a.Callee();
    
      private Example Callee([CallerMemberName] string memberName = "")
      {
        Console.WriteLine(memberName);
        return this;
      }
    }
    

    On peut appeler la surcharge de l’opérateur en exécutant:

    var example = new Example();
    Console.WriteLine(+classToExecute);
    

    On obtient:

    op_UnaryPlus
    
  • Dans le corps du finalizer: la valeur sera "Finalize".

    Le plus compliqué est d’avoir un exemple permettant d’exécuter le finalizer:

    internal class CallerMemberNameFeature: IDisposable
    {
      // Finalizer
      ~CallerMemberNameFeature()
      {
        this.Callee();
        Dispose(false);
      }
    
      public void Dispose()
      {
        GC.SuppressFinalize(this);
      }
    
      private void Callee([CallerMemberName] string memberName = "")
      {
        Console.WriteLine(memberName);
      }
    }
    

    Pour exécuter:

    static void Main(string[] args)
    {
      var example = new CallerMemberNameFeature();
      MyMethod(1);
      example.Dispose();
      GC.Collect();
      GC.WaitForPendingFinalizers();
    }
    
    private static void MyMethod(int i)
    {
      new CallerMemberNameFeature();
    }
    

    A l’exécution, on obtient:

    Finalize
    

CallerArgumentExpression

C# 10

L’attribut CallerArgumentExpressionAttribute apparu en C# 10 permet de renvoyer l’expression à l’origine de la valeur du paramètre de fonction pour lequel l’attribut est utilisé.

Si on considère l’exemple suivant:

internal class CallerExpressionArgumentFeature
{
  public void CallingMethod()
  {
    string calleeSecondArgument = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor...";
    int firstInteger = 9;
    int secondInteger = 610;
     
    this.Callee(firstInteger + secondInteger, 
    string.Format($"{0} {1} {2}", calleeSecondArgument, firstInteger, secondInteger));
  }

  private void Callee(int argument1, string argument2, 
    [CallerArgumentExpression("argument1")] string argumentExpression = "")
  {
    Console.WriteLine(argumentExpression);
  }
}

Dans cet exemple, le paramètre argumentExpression de la méthode Callee() comporte un attribut CallerArgumentExpression("argument1") indiquant que le paramètre doit contenir l’expression à l’origine de la valeur de l’argument argument1.

L’argument comportant l’attribut CallerArgumentExpression doit obligatoirement comporter une valeur par défaut.

En appelant la méthode CallingMethod():

var example = new CallerExpressionArgumentFeature();
example.CallingMethod();

On obtient:

firstInteger + secondInteger

Cette expression se trouve dans l’appel de la méthode Callee() dans CallingMethod():

this.Callee(firstInteger + secondInteger, 
  string.Format($"{0} {1} {2}", calleeSecondArgument, firstInteger, secondInteger));

De même si on modifie l’implémentation de Callee() de cette façon:

private void Callee(int argument1, string argument2, 
  [CallerArgumentExpression("argument1")] string argument1Expression = "",
  [CallerArgumentExpression("argument2")] string argument2Expression = "")
{
  Console.WriteLine(argument1Expression);
  Console.WriteLine(argument2Expression);
}

On peut obtenir l’expression à l’origine de la valeur de l’argument argument2 de Callee(). A l’exécution, on obtient:

firstInteger + secondInteger
string.Format($"{0} {1} {2}", calleeSecondArgument, firstInteger, secondInteger)

Obtenir l’expression à l’origine des valeurs des arguments peut être intéressant à logguer, par exemple, dans le cas où la valeur n’est pas celle attendue.

Leave a Reply