web-dev-qa-db-fra.com

Pourquoi le compilateur C # supprime-t-il une chaîne d'appels de méthode lorsque le dernier est conditionnel?

Considérez les classes suivantes:

public class A {
    public B GetB() {
        Console.WriteLine("GetB");
        return new B();
    }
}

public class B {
    [System.Diagnostics.Conditional("DEBUG")]
    public void Hello() {
        Console.WriteLine("Hello");
    }
}

Maintenant, si nous appelions les méthodes de cette façon:

var a = new A();
var b = a.GetB();
b.Hello();

Dans une version release (c'est-à-dire no DEBUG flag), nous ne verrions que GetB imprimé sur la console, car l'appel à Hello() serait omis par le compilateur. Dans une version de débogage, les deux impressions apparaissent.

Maintenant enchaînons les appels de méthodes:

a.GetB().Hello();

Le comportement dans une version de débogage est inchangé. Cependant, le résultat est différent si l'indicateur n'est pas défini: les deux appels sont omis et aucune impression n'apparaît sur la console. Un rapide coup d'œil à IL montre que toute la ligne n'a pas été compilée.

Selon le dernière norme ECMA pour C # (ECMA-334, c.-à-d. C # 5.0), le comportement attendu lorsque l'attribut Conditional est placé sur la méthode est le suivant (nous soulignons):

Un appel à une méthode conditionnelle est inclus si un ou plusieurs de ses symboles de compilation conditionnels associés sont définis au moment de l'appel, sinon l'appel est omis . (§22.5.3)

Cela ne semble pas indiquer que toute la chaîne devrait être ignorée, d'où ma question. Cela dit, le brouillon de la spécification C # 6.0 de Microsoft offre un peu plus de détails:

Si le symbole est défini, l'appel est inclus. sinon, l'appel (y compris l'évaluation du récepteur et les paramètres de l'appel) est omis.

Le fait que les paramètres de l'appel ne soient pas évalués est bien documenté car c'est l'une des raisons pour lesquelles les utilisateurs utilisent cette fonctionnalité plutôt que les directives #if Dans le corps de la fonction. La partie concernant "l'évaluation du destinataire", cependant, est nouvelle - je ne semble pas pouvoir la trouver ailleurs et elle semble expliquer le comportement ci-dessus.

À la lumière de ceci, ma question est la suivante: quelle est la raison derrière la non évaluation du compilateur C #a.GetB() dans cette situation? Cela devrait-il vraiment se comporter différemment selon que le destinataire de l'appel conditionnel est stocké dans une variable temporaire ou non?

69
Kyrio

J'ai creusé un peu et trouvé le spécification du langage C # 5. contenait déjà votre deuxième guillemet dans la section 17.4.2 L'attribut Conditionnel à la page 424.

Réponse de Marc Gravell montre déjà que ce comportement est voulu et ce qu’il signifie en pratique. Vous avez également posé des questions sur la justification derrière cela, mais vous semblez insatisfait de la mention de Marc de supprimer les frais généraux.

Peut-être vous demandez-vous pourquoi il est considéré comme une surcharge qui peut être supprimé?

a.GetB().Hello(); ne pas être appelé du tout dans votre scénario avec Hello() être omis peut sembler étrange à la valeur faciale.

Je ne connais pas le motif de la décision, mais j’ai trouvé un raisonnement plausible. Peut-être que cela peut vous aider aussi.

Chaînage de méthodes n'est possible que si chaque méthode précédente a une valeur de retour. Cela a du sens quand vous voulez faire quelque chose avec ces valeurs, c'est-à-dire a.GetFoos().MakeBars().AnnounceBars();

Si vous avez une fonction qui ne fait que fait ​​quelque chose sans retourner de valeur, vous ne pouvez pas enchaîner quelque chose derrière elle, mais vous pouvez la placer à la fin de la chaîne de méthodes, comme c'est le cas avec votre méthode conditionnelle car elle doit avoir le type de retour nul.

Notez également que le résultat des appels de méthode précédents est rejeté , Ainsi, dans votre exemple de a.GetB().Hello(); votre, le résultat de GetB() n'a aucune raison de vivre après l'exécution de cette instruction. Fondamentalement, vous implique vous avez besoin du résultat de GetB() uniquement pour utiliser Hello().

Si Hello() est omis, pourquoi avez-vous besoin de GetB() alors? Si vous omettez Hello() votre ligne revient à a.GetB(); sans aucune affectation et de nombreux outils vous avertiront que vous n'utilisez pas la valeur de retour, car il s'agit rarement d'une chose à faire.

La raison pour laquelle vous semblez ne pas être d'accord avec cela est que votre méthode n'est pas seulement d'essayer de faire ce qui est nécessaire pour retourner une certaine valeur, mais que vous avez également un effet secondaire , à savoir I/O. Si vous aviez plutôt un fonction pure , il n'y aurait vraiment ​​aucune raison de GetB() si vous omettez l'appel suivant, c'est-à-dire si vous n'allez pas faire n'importe quoi avec le résultat.

Si vous affectez le résultat de GetB() à une variable, il s'agit d'une instruction propre et sera exécutée de toute façon. Donc, ce raisonnement explique pourquoi dans

var b = a.GetB();
b.Hello();

seul l'appel à Hello() est omis lors de l'utilisation de la chaîne, toute la chaîne est omise.

Vous pouvez également chercher un autre point de vue pour obtenir une meilleure perspective: le opérateur null-conditionnel ou opérateur elvis? Introduit en C # 6.0. Bien qu'il ne s'agisse que du sucre syntaxique pour une expression plus complexe avec des contrôles nuls, il vous permet de construire quelque chose comme une chaîne de méthodes avec l'option de court-circuiter basée sur le contrôle nul.

Par exemple. GetFoos()?.MakeBars()?.AnnounceBars(); atteindra sa fin si les méthodes précédentes ne renvoient pas null, sinon les appels suivants sont omis.

Cela peut sembler contre-intuitif, mais essayez de penser votre scénario à l’inverse de celui-ci: le compilateur omet vos appels antérieurs à Hello() dans votre chaîne a.GetB().Hello(); puisque vous n’atteignez pas la fin de la chaîne quand même.


Avertissement

Tout cela a été un raisonnement de fauteuil alors prenez s'il vous plaît ceci et l'analogie avec l'opérateur elvis avec un grain de sel.

13
Søren D. Ptæus

Cela revient à la phrase:

(y compris l'évaluation du récepteur et les paramètres de l'appel) est omis.

Dans l'expression:

a.GetB().Hello();

"l'évaluation du destinataire" est: a.GetB(). Donc: cela est omis selon la spécification, et est une astuce utile permettant à [Conditional] D'éviter des frais généraux pour des choses qui ne sont pas utilisées. Lorsque vous le mettez dans un local:

var b = a.GetB();
b.Hello();

alors "l'évaluation du destinataire" est juste le local b, mais le var b = a.GetB(); original est toujours évalué (même si le local b finit par être supprimé).

Ceci peut a des conséquences inattendues, donc: utilisez [Conditional] Avec précaution. Mais les raisons en sont que des choses comme la journalisation et le débogage peuvent être ajoutées et supprimées de manière triviale. Notez que les paramètres peuvent aussi être problématiques s'ils sont traités naïvement:

LogStatus("added: " + engine.DoImportantStuff());

et:

var count = engine.DoImportantStuff();
LogStatus("added: " + count);

peut être très différent si LogStatus est marqué [Conditional] - avec pour résultat que vos "tâches importantes" ne sont pas terminées.

62
Marc Gravell

Devrait-il vraiment se comporter différemment selon que le destinataire de l'appel conditionnel est stocké dans une variable temporaire ou non?

Oui.

Quelle est la raison pour laquelle le compilateur C # n'évalue pas a.GetB() dans cette situation?

Les réponses de Marc et Søren sont fondamentalement correctes. Cette réponse est juste pour documenter clairement la chronologie.

  • La fonctionnalité a été conçue en 1999 et l’intention de la fonctionnalité a toujours été de supprimer toute la déclaration.
  • Les notes de conception de 2003 indiquent que l’équipe de conception a alors compris que la spécification n’était pas claire sur ce point. Jusque-là, la spécification appelait uniquement que arguments ne serait pas évalué. Je remarque que la spécification commet l'erreur commune d'appeler les arguments "paramètres", bien que l'on puisse naturellement supposer qu'ils signifiaient "paramètres effectifs" plutôt que "paramètres formels".
  • Un élément de travail devait être créé pour corriger la spécification ECMA sur ce point. apparemment cela n'est jamais arrivé.
  • La première fois que le texte corrigé apparaissait dans une spécification C # était la spécification C # 4.0, qui, je crois, était en 2010. (Je ne me souviens pas s'il s'agissait de l'une de mes corrections ou si quelqu'un d'autre l'avait trouvée.)
  • Si la spécification ECMA 2017 ne contient pas cette correction, c'est une erreur qui devrait être corrigée dans la prochaine version. Mieux vaut 15 ans de retard que jamais, je suppose.
19
Eric Lippert