web-dev-qa-db-fra.com

Fonctions locales en C # - pour capturer ou ne pas capturer lors de la transmission des paramètres?

Lorsque vous utilisez Fonctions locales en C # 7, vous avez deux options lorsque vous souhaitez passer des paramètres (ou d'autres variables locales) de la méthode principale à la fonction locale: Vous pouvez soit déclarer explicitement les paramètres comme vous le feriez n'importe quelle autre fonction ou vous pouvez simplement "capturer" les paramètres/variables de la méthode de conteneur et les utiliser directement.

Un exemple illustre peut-être mieux cela:

Déclaration explicite

public int MultiplyFoo(int id)
{
    return LocalBar(id);

    int LocalBar(int number)
    {
        return number * 2;
    }
}

Capture

public int MultiplyFoo(int id)
{
    return LocalBar();

    int LocalBar()
    {
        return id * 2;
    }
}

Les deux méthodes fonctionnent de la même façon, mais la façon dont elles invoquent la fonction locale est différente.

Ma question est donc:

Y a-t-il une différence entre les deux que je devrais connaître? Je pense en termes de performances, d'allocation de mémoire, de récupération de place, de maintenabilité, etc.

30
Dan Diplo

Les fonctions locales en C # sont intelligentes en termes de capture - au moins dans l'implémentation de Roslyn. Lorsque le compilateur est en mesure de garantir que vous ne créez pas de délégué à partir de la fonction locale (ou que vous ne faites rien d'autre qui prolongera la durée de vie de la variable), il peut utiliser un paramètre ref avec toutes les variables capturées dans une structure générée pour communiquer avec la fonction locale. Par exemple, votre deuxième méthode se terminerait par quelque chose comme:

public int MultiplyFoo(int id)
{
    __MultiplyFoo__Variables variables = new __MultiplyFoo__Variables();
    variables.id = id;
    return __Generated__LocalBar(ref variables);
}

private struct __MultiplyFoo__Variables
{
    public int id;
}

private int __Generated__LocalBar(ref __MultiplyFoo__Variables variables)
{
    return variables.id * 2;
}

Il n'y a donc pas d'allocation de tas requise comme il y en aurait pour (disons) une expression lambda convertie en délégué. D'un autre côté, il y a la construction de la structure puis la copie des valeurs dans celle-ci. Que passer un int par valeur soit plus ou moins efficace que passer le struct par référence est peu probable être significatif ... bien que je suppose que dans les cas où vous aviez un énorme struct comme variable locale, cela signifierait que l'utilisation de la capture implicite serait plus efficace que l'utilisation d'un paramètre de valeur simple. (De même, si votre fonction locale a utilisé de nombreuses variables locales capturées.)

La situation devient déjà plus compliquée lorsque plusieurs variables locales sont capturées par différentes fonctions locales - et encore plus lorsque certaines d'entre elles sont des fonctions locales dans des boucles, etc. Explorer avec ildasm ou Reflector etc. peut être très amusant.

Dès que vous commencez à faire quelque chose de compliqué, comme écrire des méthodes asynchrones, des blocs d'itérateur, des expressions lambda dans les fonctions locales, utiliser des conversions de groupes de méthodes pour créer un délégué à partir de la fonction locale, etc., à ce stade, j'hésiterais à continuer de deviner. Vous pouvez soit essayer de comparer le code dans chaque sens, soit regarder l'IL, ou simplement écrire le code le plus simple et s'appuyer sur vos tests de validation des performances plus importants (que vous avez déjà, non? :) pour vous faire savoir si c'est un problème .

14
Jon Skeet

C'était une question intéressante. J'ai d'abord décompilé la sortie du build.

public int MultiplyFoo(int id)
{
  return LocalFunctionTests.\u003CMultiplyFoo\u003Eg__LocalBar\u007C0_0(id);
}

public int MultiplyBar(int id)
{
  LocalFunctionTests.\u003C\u003Ec__DisplayClass1_0 cDisplayClass10;
  cDisplayClass10.id = id;
  return LocalFunctionTests.\u003CMultiplyBar\u003Eg__LocalBar\u007C1_0(ref cDisplayClass10);
}

Lorsque vous passez id en paramètre, la fonction locale est appelée avec le paramètre id passé. Rien de spécial et le paramètre est stocké sur le cadre de pile de la méthode. Cependant, si vous ne passez pas le paramètre, une structure (pensée nommée 'classe' comme l'a souligné Daisy) est créée avec un champ (cDisplayClass10.id = id) et l'id lui est assigné. Ensuite, la structure est passée comme référence dans la fonction locale. Le compilateur C # semble le faire pour prendre en charge la fermeture.

En termes de performances, j'ai utilisé Stopwatch.ElapsedTicks, passer id comme paramètre était toujours plus rapide. Je pense que c'est à cause du coût de création d'une structure avec un champ. L'écart de performances s'est élargi lorsque j'ai ajouté un autre paramètre à la fonction locale.

  • Passing Id: 2247
  • Ne passe pas Id: 2566

Ceci est mon code de test, si quelqu'un est intéressé

public int MultiplyFoo(int id, int id2)
{
    return LocalBar(id, id2);

    int LocalBar(int number, int number2)
    {
        return number * number2 * 2;
    }
}

public int MultiplyBar(int id, int id2)
{
    return LocalBar();

    int LocalBar()
    {
        return id * id2 * 2;
    }
}


[Fact]
public void By_Passing_Id()
{
    var sut = new LocalFunctions();

    var watch = Stopwatch.StartNew();
    for (int i = 0; i < 10000; i++)
    {
        sut.MultiplyFoo(i, i);
    }

    _output.WriteLine($"Elapsed: {watch.ElapsedTicks}");
}

[Fact]
public void By_Not_Passing_Id()
{
    var sut = new LocalFunctions();

    var watch = Stopwatch.StartNew();
    for (int i = 0; i < 10000; i++)
    {
        sut.MultiplyBar(i, i);
    }

    _output.WriteLine($"Elapsed: {watch.ElapsedTicks}");
}
5
Andy