69 votes

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

Considérer 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 devions appeler les méthodes de cette façon:

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

Dans un communiqué de construire (à savoir, pas de DEBUG drapeau), nous ne voyons GetB imprimé sur la console, comme l'appel à l' Hello() serait omise par le compilateur. Dans une version debug, les deux copies apparaissent.

Maintenant, nous allons enchaîner les appels de méthode:

a.GetB().Hello();

Le comportement dans une version de débogage est inchangé; cependant, nous obtenons un résultat différent si l'indicateur n'est pas défini: les deux appels sont omis et aucun tirage apparaissent sur la console. Un rapide coup d'oeil à IL montre que l'ensemble de la ligne n'est pas compilé.

Selon la dernière norme ECMA pour C# (ECMA-334, c'est à dire de C# 5.0), le comportement attendu lors de l' Conditional attribut est placé sur la méthode est la suivante (l'emphase est mienne):

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

Cela ne semble pas indiquer que l'ensemble de la chaîne doit être ignoré, d'où ma question. Cela étant dit, le C# 6.0 projet de spec à partir de Microsoft offre un peu plus en détail:

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 sont pas évalués est bien documenté, car c'est une des raisons les gens à utiliser cette fonction plutôt que d' #if directives dans le corps de la fonction. La partie sur "l'évaluation du récepteur", cependant, est que je n'arrive pas à trouver ailleurs, et il ne semble pas expliquer ce comportement.

À la lumière de cela, ma question est: quelle est la logique derrière le compilateur C# ne pas évaluer a.GetB() dans cette situation? Faut-il vraiment se comporter de façon différente selon que le récepteur de l'appel conditionnel est stocké dans une variable temporaire ou pas?

62voto

Marc Gravell Points 482669

Il 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 récepteur" est: a.GetB(). Donc: qu'est omise que par la spécification, et est une astuce utile permettant d' [Conditional] pour éviter la surcharge pour des choses qui ne sont pas utilisés. Quand vous la mettez dans un local:

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

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

Cela peut avoir des conséquences inattendues, donc: utiliser [Conditional] avec le plus grand soin. Mais les raisons sont donc que des choses comme la journalisation de débogage et peut être trivialement ajoutés et supprimés. Notez que les paramètres peuvent également être problématique si elle est traitée naïvement:

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

et:

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

peut être très différente si l' LogStatus est marquée [Conditional] - avec le résultat que votre "choses importantes" ne se sont pas fait.

19voto

Eric Lippert Points 300275

Faut-il vraiment se comporter de façon différente selon que le récepteur de l'appel conditionnel est stocké dans une variable temporaire ou pas?

Oui.

Quelle est la logique derrière le compilateur C# ne pas évaluer a.GetB() dans cette situation?

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

  • La fonctionnalité a été conçue en 1999, et l'intention de la fonction a toujours été de supprimer l'intégralité de la déclaration.
  • La conception de notes à partir de 2003 indiquent que l'équipe de conception réalisé alors que la spec n'était pas claire sur ce point. Jusqu'à ce point, la spécification appelée seulement que les arguments ne seraient pas évalués. Je remarque que la spec fait l'erreur d'appeler les arguments "paramètres", même si bien sûr on pourrait supposer qu'ils signifiaient "paramètres" plutôt que de "paramètres formels".
  • Un élément de travail était censé être créées pour corriger la spécification ECMA sur ce point; apparemment, ce n'est jamais arrivé.
  • La première fois que le texte corrigé apparaît dans toute spécification C# a C# 4.0 cahier des charges, qui je crois était 2010. (Je ne me souviens pas si c'était un de mes corrections, ou si quelqu'un l'a trouvé.)
  • Si l'2017 spécification ECMA ne contient pas de cette correction, alors que c'est une erreur qui devrait être résolu dans la prochaine version. Mieux 15 ans tard que jamais, je suppose.

13voto

Søren D. Ptæus Points 853

J'ai fait quelques recherches et trouvé le C# 5.0 langage de spécification n'a en fait déjà contenir votre deuxième citation à l'article 17.4.2 L'attribut Conditionnel à la page 424.

Marc Gravel réponse montre déjà que ce comportement est prévu et ce que cela signifie dans la pratique. Vous avez également posé des questions sur le raisonnement derrière cela, mais semblent être insatisfait par Marc mention d'éliminer la surcharge.

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

a.GetB().Hello(); n'est pas appelée à tous dans votre scénario avec Hello() être omis peut sembler étrange à la valeur nominale.

Je ne sais pas les raisons de cette décision, mais j'ai trouvé quelques plausible raisonnement de mon propre. Peut-être que cela peut vous aider.

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

Si vous avez une fonction qui seulement fait quelque chose sans avoir à retourner une valeur que vous ne peut pas la chaîne quelque chose derrière elle, mais peut la mettre à la fin de la méthode de la chaîne, comme c'est le cas avec votre conditionnelle de la méthode, car il doit avoir le type de retour void.

Notez également que le résultat de la méthode précédente appels obtient jeté, donc dans votre exemple d' a.GetB().Hello(); votre le résultat de l' GetB() a plus de raison de vivre après cette instruction est exécutée. Fondamentalement, vous implique vous avez besoin de la suite de l' GetB() uniquement pour l'utiliser Hello().

Si Hello() est omis pourquoi avez-vous besoin d' GetB() , alors? Si vous omettez Hello() votre ligne se résume à de la a.GetB(); sans affectation et de nombreux outils permettront de donner un avertissement que vous n'êtes pas à l'aide de la valeur de retour parce que c'est rarement quelque chose que vous voulez faire.

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

Si vous affectez le résultat d' GetB() à une variable, c'est une déclaration sur ses propres et sera exécuté de toute façon. Si ce raisonnement explique pourquoi dans

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

seulement l'appel à Hello() est omis alors que lors de l'utilisation de la méthode de chaînage de l'ensemble de la chaîne est omis.

Vous pouvez aussi chercher un endroit complètement différent à obtenir une meilleure perspective: le null-opérateur conditionnel ou elvis opérateur ? introduite en C# 6.0. Bien qu'il n'est sucre syntaxique pour une expression plus complexe avec null contrôles il permet de construire quelque chose comme une méthode de la chaîne avec la possibilité de court-circuit basé sur la valeur null est à vérifier.

E. g. GetFoos()?.MakeBars()?.AnnounceBars(); sera atteint, c'est la fin si les méthodes précédentes ne sont pas de retour null, sinon les appels sont omis.

Il pourrait être contre-intuitif, mais essayez de penser à votre scénario comme l'inverse de cette tendance: le compilateur omet vos appels avant Hello() votre a.GetB().Hello(); chaîne puisque vous n'êtes pas d'atteindre la fin de la chaîne de toute façon.


Avertissement

Tout cela a été un fauteuil raisonnement merci donc de prendre le présent et l'analogie avec la elvis opérateur avec un grain de sel.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X