109 votes

Comment puis-je déterminer avec fiabilité le type d’une variable qui est déclarée à l’aide de var au moment du design ?

Je suis en train de travailler sur un achèvement (intellisense) pour C# dans emacs.

L'idée est que, si un utilisateur tape un fragment, puis demande à l'achèvement par l'intermédiaire d'un particulier, d'une combinaison de touches, l'achèvement de la facilité d'utilisation .NET de la réflexion pour déterminer les complétions possibles.

Cette opération nécessite le type de la chose d'être complétée, à être connue. Si c'est une chaîne, il y a un ensemble de méthodes et de propriétés; si c'est un Int32, il possède un ensemble distinct, et ainsi de suite.

À l'aide de la sémantique, un code lexer/analyseur de paquet disponible dans emacs, je peux localiser les déclarations de variables et leurs types. Compte tenu de cela, il est simple à utiliser la réflexion pour obtenir les méthodes et les propriétés du type, puis présenter la liste des options à l'utilisateur. (Ok, pas tout à fait simple à faire dans emacs, mais à l'aide de la capacité à exécuter un processus powershell à l'intérieur d'emacs, il devient beaucoup plus facile. J'écris une coutume .NET de l'assemblée de la réflexion, de la charger dans le powershell, puis elisp cours d'exécution dans emacs peut envoyer des commandes powershell et lire les réponses, via comint. Comme un résultat emacs peut obtenir les résultats de la réflexion rapidement.)

Le problème arrive lorsque le code utilise var dans la déclaration de la chose en cours d'achèvement. Cela signifie que le type n'est pas spécifié, et l'achèvement de ne pas travailler.

Comment puis-je déterminer de manière fiable le type réel utilisé lorsque la variable est déclarée avec l' var mot-clé? Juste pour être clair, je n'ai pas besoin de le déterminer au moment de l'exécution. Je veux déterminer au "moment de la Conception".

Jusqu'à présent j'ai de ces idées:

  1. de compiler et d'invoquer:
    • extrait de la déclaration de la fonction, par exemple, `var foo = "chaîne de valeur";`
    • concaténer une déclaration " foo.GetType();`
    • compiler dynamiquement le C# résultant fragment dans la nouvelle assemblée
    • charger l'assembly dans un nouveau domaine d'application, exécutez la framgment et d'obtenir le type de retour.
    • décharger et de jeter l'assemblée

    Je ne sais comment faire tout cela. Mais ça sonne terriblement lourd, pour chaque réalisation requête dans l'éditeur.

    Je suppose que je n'ai pas besoin d'un nouveau domaine d'application à chaque fois. J'ai pu re-utiliser un seul domaine d'application pour plusieurs temporaire assemblées, et d'amortir le coût de sa mise en et de le déchirer vers le bas, à travers de multiples achèvement des demandes. C'est plus qu'un réglage de l'idée de base.

  2. de compiler et d'inspecter IL

    Simplement compiler la déclaration dans un module, puis vérifiez l'IL, afin de déterminer le type réel qui a été déduite par le compilateur. Comment serait-ce possible? Que serais-je utiliser pour examiner l'-t-IL?

Toutes les meilleures idées? Des commentaires? des suggestions?


EDIT - penser à cette autre, de compiler et d'invoquer n'est pas acceptable, parce que l'appel peut avoir des effets secondaires. Si la première option doit être exclu.

Aussi, je pense que je ne peut pas conclure à la présence d' .NET 4.0.


Mise à JOUR - La bonne réponse, non mentionnés ci-dessus, mais gentiment remarquer par Eric Lippert, est de mettre en œuvre une pleine fidélité type de système d'inférence. Il;s le seul moyen fiable de déterminer le type d'une variable au moment de la conception. Mais, c'est pas aussi facile à faire. Parce que je ne souffrent pas d'illusions que je veux essayer de construire une telle chose, j'ai pris le raccourci de l'option 2 - extrait de la déclaration code et le compiler, puis inspectez la résultante de l'IL.

Cela fonctionne en fait, pour juste un sous-ensemble de l'achèvement des scénarios.

Par exemple, supposons que dans le code suivant fragments, l' ? est la position à laquelle l'utilisateur en fait la demande pour l'achèvement. Ceci fonctionne:

var x = "hello there"; 
x.?

La réalisation se rend compte que x est une Chaîne, et offre les options appropriées. Pour cela, il génère et puis compiler le code source suivant:

namespace N1 {
  static class dmriiann5he { // randomly-generated class name
    static void M1 () {
       var x = "hello there"; 
    }
  }
}

...et ensuite d'inspecter le IL par simple réflexion.

Cela fonctionne aussi:

var x = new XmlDocument();
x.? 

Le moteur ajoute le à l'aide de clauses pour le code source généré, de sorte qu'il compile correctement, et puis la IL de l'inspection est la même.

Cela fonctionne aussi:

var x = "hello"; 
var y = x.ToCharArray();    
var z = y.?

Cela signifie simplement IL d'inspection doit trouver le type de la troisième variable locale, au lieu de la première.

Et ceci:

var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
    {
        foo,
        foo.Length.ToString()
    };
var z = fred.Count;
var x = z.?

...ce qui est d'un niveau plus profond que l'état de la exemple.

Mais, ce n'est pas le travail est effectué sur toute variable locale dont l'initialisation dépend à un point sur un membre de l'instance, ou d'un argument de méthode. Comme:

var foo = this.InstanceMethod();
foo.?

Ni la syntaxe LINQ.

Je vais devoir réfléchir à la façon de précieuses ces choses sont avant je envisager de lutter contre eux par le biais de ce qui est certainement un "processus de conception" (mot poli pour hack) pour l'achèvement.

Une approche pour résoudre le problème avec des dépendances sur des arguments de méthode ou les méthodes d'instance serait de remplacer, dans le fragment de code qui est généré, compilé et puis IL a analysé, les références à ces choses "synthétique" local vars du même type.


Une autre mise à Jour - l'achèvement des travaux sur vars qui dépendent des membres de l'instance, fonctionne maintenant.

Ce que j'ai fait a été d'interroger le type (via sémantique), puis de générer synthétique doublure membres de tous les membres existants. Pour un C# tampon comme ceci:

public class CsharpCompletion
{
    private static int PrivateStaticField1 = 17;

    string InstanceMethod1(int index)
    {
        ...lots of code here...
        return result;
    }

    public void Run(int count)
    {
        var foo = "this is a string";
        var fred = new System.Collections.Generic.List<String>
        {
            foo,
            foo.Length.ToString()
        };
        var z = fred.Count;
        var mmm = count + z + CsharpCompletion.PrivateStaticField1;
        var nnn = this.InstanceMethod1(mmm);
        var fff = nnn.?

        ...more code here...

...le code généré qui sera compilé, afin que je puisse apprendre à partir de la sortie IL le type de local var nnn, ressemble à ceci:

namespace Nsbwhi0rdami {
  class CsharpCompletion {
    private static int PrivateStaticField1 = default(int);
    string InstanceMethod1(int index) { return default(string); }

    void M0zpstti30f4 (int count) {
       var foo = "this is a string";
       var fred = new System.Collections.Generic.List<String> { foo, foo.Length.ToString() };
       var z = fred.Count;
       var mmm = count + z + CsharpCompletion.PrivateStaticField1;
       var nnn = this.InstanceMethod1(mmm);
      }
  }
}

L'ensemble de l'instance et de la statique des membres du type sont disponibles dans le squelette de code. Il compile correctement. À ce stade, de déterminer le type de local var est simple via la Réflexion.

Ce qui rend cela possible est:

  • la capacité à exécuter powershell dans emacs
  • le compilateur C# est vraiment rapide. Sur ma machine, il faut environ 0,5 s pour compiler une mémoire de l'assemblée. Pas assez rapide entre les frappes de l'analyse, mais suffisante pour prendre en charge la génération de la demande de l'achèvement des listes.

Je n'ai pas regardé en LINQ encore.
Qui sera le plus gros problème parce que la sémantique lexer/parser emacs a pour C#, ne pas "faire" de LINQ.

202voto

Eric Lippert Points 300275

Je peux décrire pour vous la façon dont nous le faire de manière efficace dans la "vraie" C# IDE.

La première chose à faire est de lancer un pass qui analyse seulement le "haut niveau" des trucs dans le code source. Nous sautons tous les corps de méthode. Qui nous permet de construire rapidement une base de données des informations sur ce que l'espace de noms, types et les méthodes (et les constructeurs, etc) sont dans le code source du programme. L'analyse de chaque ligne de code dans tous les corps de la méthode prendrait beaucoup trop de temps si vous essayez de le faire entre les frappes de touches.

Lorsque l'IDE doit travailler le type d'une expression particulière à l'intérieur d'un corps de méthode, disons que vous avez tapé "foo." et nous avons besoin de comprendre quels sont les membres de foo -- nous faisons la même chose; nous sauter autant de travail que nous pourrons.

Nous commençons avec un pass qui analyse seulement la variable locale déclarations à l'intérieur de cette méthode. Quand nous courons qui passent nous faire un mappage à partir d'une paire de "portée" et "nom" type "déterminant". Le type "déterminant" est un objet qui représente la notion de "je peux travailler sur le type de ce local si j'en ai besoin". De travail, le type de local peut être coûteux, de sorte que nous voulons de différer ce travail, si nous en avons besoin.

Nous avons maintenant un paresseusement construit la base de données que peut nous dire le type de chaque local. Donc, pour en revenir à ce "foo." -- nous savoir laquelle de déclaration de l'expression pertinente est puis exécutez l'analyseur sémantique contre cette déclaration. Par exemple, supposons que vous avez le corps de la méthode:

String x = "hello";
var y = x.ToCharArray();
var z = from foo in y where foo.

et maintenant, nous avons besoin de travailler que foo est du type char. Nous construisons une base de données qui a toutes les métadonnées, les méthodes d'extension, le code source des types, et ainsi de suite. Nous construisons une base de données qui a pour type les déterminants de x, y et z. Nous analysons la déclaration contenant les intéressant d'expression. Nous commençons par la transformation de ce point de vue syntaxique à

var z = y.Where(foo=>foo.

Afin de trouver le type de foo, nous devons d'abord savoir le type de y. Donc, à ce moment nous demander le type de déterminant "qu'est-ce que le type de y"? Il commence alors un évaluateur d'expression qui analyse x.ToCharArray() et lui demande "quel est le type de x"? Nous avons un type de facteur déterminant pour que qui dit "j'ai besoin de consulter la rubrique "String" dans le contexte actuel". Il n'y a pas de type Chaîne de caractères dans le type de courant, de sorte que nous attendons dans l'espace de noms. Ce n'est pas là, soit nous cherchons à l'aide de directives et de découvrir qu'il y a un "à l'aide du Système" et que le Système a un type Chaîne de caractères. OK, donc c'est le type de x.

Ensuite, nous avons un Système de requête.La chaîne de métadonnées pour le type de ToCharArray et il dit que c'est un Système.Char[]. Super. Nous avons donc un type y.

Demandons-nous maintenant "n'Système.Char[] est une méthode Où?" Pas de. Nous avons donc regarder dans l'aide de directives; nous avons déjà précalculées une base de données contenant l'ensemble des métadonnées pour les méthodes d'extension qui pourrait éventuellement être utilisé.

Maintenant nous dire "OK, il y a dix-huit douzaine de méthodes d'extension nommée Où dans la portée, certains d'entre eux ont un premier paramètre formel dont le type est compatible avec le Système.Char[]?" Donc, nous commençons un tour de la convertibilité des tests. Cependant, la Où les méthodes d'extension sont génériques, ce qui signifie que nous avons à faire de l'inférence de type.

J'ai écrit un type spécial infererencing moteur qui peut gérer ce qui rend incomplète des inférences à partir du premier argument à une méthode d'extension. Nous courons le type inferrer et de découvrir qu'il y a une méthode qui prend un IEnumerable<T>, et que l'on peut faire une inférence à partir du Système.Char[] IEnumerable<System.Char>, donc T est Système.Char.

La signature de cette méthode, c'est - Where<T>(this IEnumerable<T> items, Func<T, bool> predicate), et nous savons que T est le Système.Char. Nous savons aussi que le premier argument à l'intérieur des parenthèses de la méthode d'extension est un lambda. Donc, nous commençons une expression lambda de type inferrer qui dit que "le paramètre formel foo est supposé être du Système.Char", utiliser ce fait lors de l'analyse du reste de la lambda.

Nous avons maintenant toutes les informations dont nous avons besoin pour analyser le corps de la lambda, qui est "foo.". Nous regardons le type de toto, nous découvrons que, selon le lambda binder, c'est le Système.Char, et nous avons fait; nous le type d'affichage de l'information pour le Système.Char.

Et nous faisons tout sauf le "haut niveau" analyse entre les frappes de touches. C'est le véritable problème. En fait l'écriture de l'analyse n'est pas difficile; c'est faire ça assez vite que vous pouvez le faire à la vitesse de frappe, qui est le véritable problème.

Bonne chance!

15voto

Barry Kelly Points 30330

Je peux vous dire en gros comment le Delphi IDE fonctionne avec le compilateur Delphi faire intellisense (code insight est ce que Delphi appelle). Ce n'est pas de 100% applicable à C#, mais c'est une approche intéressante qui mérite de retenir l'attention.

La plupart d'analyse sémantique en Delphi est fait dans l'analyseur lui-même. Les Expressions sont typées comme ils sont analysés, à l'exception des situations où ce n'est pas facile dans ce cas de " look-ahead d'analyse est utilisé pour déterminer ce qui est prévu, et alors que la décision est utilisée dans l'analyse.

L'analyse est en grande partie LL(2) descente récursive, sauf pour les expressions, qui sont analysées à l'aide de la priorité de l'opérateur. L'une des choses distinctes à propos de Delphi, c'est que c'est un seul passage de la langue, les constructions doivent être déclarées avant d'être utilisées, donc pas de haut-niveau de la passe est nécessaire d'apporter cette information.

Cette combinaison de caractéristiques qui signifie que l'analyseur a peu près toutes les informations nécessaires pour code un aperçu de n'importe quel point où c'est nécessaire. La façon dont cela fonctionne est la suivante: l'IDE informe le compilateur de l'analyseur lexical de la position du curseur (le point où l'aperçu du code désiré) et l'analyseur lexical, transforme ce dans un jeton spécial (ça s'appelle le kibitz jeton). Chaque fois que l'analyseur rencontre ce jeton (qui pourrait être n'importe où), il sait que c'est le signal pour envoyer toutes les informations dont il a le dos à l'éditeur. Il le fait à l'aide d'un longjmp parce que c'est écrit en C; ce qu'il fait est-il en informe le summum de l'appelant, du type de construction syntaxique (c'est à dire de grammaire context) la kibitz point a été trouvée, ainsi que toute la symbolique des tables nécessaires à ce point. Ainsi, par exemple, si le contexte est une expression qui est un argument à une méthode, nous pouvons vérifier la méthode surcharges, regardez les types d'argument, et de filtrer les symboles valables uniquement à ceux qui peuvent résoudre à ce type d'argument (ce qui réduit de beaucoup de pertinence les fichiers inutiles dans le menu déroulant). Si c'est dans une étude de la portée de contexte (par exemple, après un "."), l'analyseur ont été remis une référence à la portée, et l'IDE peut énumérer tous les symboles trouvés dans cette portée.

D'autres choses sont également fait; par exemple, le corps de méthode sont ignorées si le kibitz jeton ne réside pas dans leur gamme - ce qui est fait avec optimisme, et a roulé en arrière si elle sauté sur le jeton. L'équivalent de l'extension des méthodes de la classe des aides dans Delphi ont une sorte de versionnées cache, de sorte que leur recherche est assez rapide. Mais Delphi générique de l'inférence de type est beaucoup plus faible que celui de C#.

Maintenant, à la question: inférer les types de variables déclarées avec var est équivalent à la façon dont Pascal déduit le type de constantes. Il s'agit du type de l'expression d'initialisation. Ces types sont construits de bas en haut. Si x est de type Integer, et y est de type Double, alors x + y sera de type Double, parce que ce sont les règles de la langue; etc. Vous suivez ces règles jusqu'à ce que vous avez un type pour la pleine expression sur le côté droit, et c'est le type que vous utilisez pour le symbole sur la gauche.

7voto

Daniel Plaisted Points 11183

Si vous ne voulez pas avoir à écrire votre propre analyseur pour générer l’arborescence de syntaxe abstraite, vous pouvez regarder à l’aide des analyseurs de SharpDevelop ou MonoDevelop, qui sont libres.

4voto

Dan Bryant Points 19021

Intellisense systèmes représentent généralement le code à l'aide d'un Arbre de Syntaxe Abstraite, ce qui leur permet de régler le type de retour de la fonction assignée à l' 'var' variable plus ou moins de la même manière que le compilateur. Si vous utilisez le VS Intellisense, vous remarquerez peut-être qu'il ne vous donnera pas le type de var jusqu'à ce que vous avez fini d'entrer valide (résolu) expression d'affectation. Si l'expression est encore ambiguë (par exemple, il ne peut pas pleinement en déduire les arguments génériques pour l'expression), le var type ne va pas résoudre. Cela peut être un processus assez complexe, comme vous pourriez avoir besoin de marcher assez profondément dans l'arbre afin de résoudre ce type. Par exemple:

var items = myList.OfType<Foo>().Select(foo => foo.Bar);

Le type de retour est - IEnumerable<Bar>, mais la résolution de ce nécessaire de savoir:

  1. myList est de type qui implémente IEnumerable.
  2. Il est une extension de la méthode de OfType<T> qui s'applique à IEnumerable.
  3. La valeur obtenue est - IEnumerable<Foo> et il est une extension de la méthode de Select qui s'applique à la présente.
  4. L'expression lambda foo => foo.Bar a le paramètre de foo de type Foo. C'est déduite par l'utilisation de Select, qui prend un Func<TIn,TOut> et depuis l'Étain est connu (Foo), le type de foo peut être déduite.
  5. Le type Foo a une Barre de propriétés, qui est de type Bar. Nous savons que de Sélectionner les retours IEnumerable<TOut> et TOut peut être déduit du résultat de l'expression lambda, de sorte que le type résultant des articles doivent être IEnumerable<Bar>.

4voto

Eric Points 3491

Puisque vous êtes ciblage Emacs, il peut être préférable de commencer avec les CEDET suite. Tous les détails que Eric Lippert sont abordés dans le code de l'analyseur dans le CEDET/outil Sémantique pour le C++ déjà. Il y a aussi un C# analyseur (qui a probablement besoin d'un peu de TLC) si les seules parties manquantes sont liées au fait d'accorder les pièces nécessaires pour C#.

Le comportement de base sont définis dans les algorithmes de base qui dépendent de overloadable fonctions qui sont définies par la langue. Le succès de l'achèvement du moteur dépend de la façon dont beaucoup de réglage a été fait. Avec le c++ comme un guide, l'obtention d'une prise en charge similaire à C++ ne devrait pas être trop mauvais.

Daniel en réponse suggère d'utiliser MonoDevelop pour faire de l'analyse syntaxique et l'analyse. Cela pourrait être un mécanisme alternatif à la place de l'existant en C# de l'analyseur, ou il pourrait être utilisé pour augmenter l'existant de l'analyseur.

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