Comment puis-je déterminer de manière fiable le type d'une variable qui est déclarée à l'aide de var au moment de la conception?
Je suis en train de travailler sur un achèvement (intellisense) facilité 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 possibilité d'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 le 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:
- 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 une nouvelle assemblée
- charger l'assembly dans un nouveau domaine d'application, exécutez la framgment et d'obtenir le type de retour.
- de décharger et de jeter l'assemblée
Je 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. - 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 là-bas? Des commentaires? des suggestions?
MODIFIER - 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 réellement, 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. Cela fonctionne:
var x = "hello there";
x.?
L'achèvement 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, trop:
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 ce:
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 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 aborder la question 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 possibilité d'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.
- Le type de foo est compris et rempli par le compilateur par l'inférence de type. Je soupçonne que les mécanismes sont totalement différentes. Peut-être le type de moteur d'inférence a un crochet? À tout le moins je voudrais utiliser 'inférence de type' comme une balise.
- Votre technique de faire un "faux" modèle d'objet qui dispose de tous les types, mais aucun de la sémantique des objets réels en est une bonne. Voilà comment j'ai fait IntelliSense pour JScript dans Visual InterDev retour dans la journée; nous faire un "faux" version de l'IE modèle d'objet qui contient toutes les méthodes et les types, mais aucun des effets secondaires, et puis encore un peu d'interprète sur l'analyse de code au moment de la compilation et de voir ce type de revient.
Vous devez vous connecter pour publier un commentaire.
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 les 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 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:
et maintenant nous avons besoin de travail 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 de
Afin de travailler sur 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 de y.
Demandons-nous maintenant "n'Système.Char[] avoir 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érique, 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 nous pouvons faire une inférence à partir du Système.Char[] pourIEnumerable<System.Char>
, donc T est Système.Char.La signature de cette méthode 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. C'est le véritable problème. En fait l'écriture de l'analyse n'est pas difficile; c'est faire il assez rapide que vous pouvez le faire à la vitesse de frappe, qui est le véritable problème.
Bonne chance!
var
dans une méthode, plus l'IDE est de travailler pour trouver l'expression actuelle du type de, droite? Sonne commevar
(quand il n'est pas nécessaire) serait tout à fait un peu de surcharge dans ce cas...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.
Plus l'analyse sémantique 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, l'on peut 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. Six
est de typeInteger
, ety
est de typeDouble
, puisx + y
sera de typeDouble
, 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.Si vous ne voulez pas avoir à écrire votre propre analyseur de construire l'arbre de syntaxe abstraite, on peut le voir à l'aide de la analyseurs de soit SharpDevelop ou MonoDevelop, les deux sont open source.
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:
Le type de retour est
IEnumerable<Bar>
, mais la résolution de ce nécessaire de savoir:IEnumerable
.OfType<T>
qui s'applique à IEnumerable.IEnumerable<Foo>
et il y a une méthode d'extensionSelect
qui s'applique à ce.foo => foo.Bar
a le paramètre de foo de type Foo. C'est déduite par l'utilisation de Select, qui prend unFunc<TIn,TOut>
et depuis l'Étain est connu (Foo), le type de foo peut être déduite.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 êtreIEnumerable<Bar>
.Depuis que vous ciblez 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 le 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 de base. 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 accroître la parser.
var
. Sémantique identifie correctement comme var, mais ne fournit pas l'inférence de type. Ma question a été spécifiquement autour de la façon d'adresse que. J'ai aussi regardé dans le branchant sur l'existant CEDET d'achèvement, mais je ne pouvais pas comprendre comment. La documentation pour CEDET est...ah...pas complète.C'est un problème difficile à bien faire. Fondamentalement, vous avez besoin de modèle de la langue spec/compilateur par la plupart des lexing/analyse/vérification de type et de construire un modèle interne du code source qui vous permet ensuite d'interroger. Eric décrit en détail pour C#. Vous pouvez toujours télécharger le compilateur F# code source (partie de la F# CTP) et de prendre un coup d'oeil à
service.fsi
pour voir l'interface exposée de le compilateur F# que le langage F# service consomme pour fournir de l'intellisense, les info-bulles pour déduire les types, etc. Il donne un sentiment d'une possible "interface" si vous avez déjà eu le compilateur à disposition une API pour l'appeler.L'autre voie consiste à ré-utiliser les compilateurs-est comme vous le décrivez, et ensuite utiliser la réflexion ou de regarder le code généré. C'est problématique du point de vue que vous avez besoin de plein de programmes "pour obtenir une compilation sortie à partir d'un compilateur, alors que lors de l'édition du code source dans l'éditeur, vous avez souvent seulement" partielle des programmes qui ne sont pas encore analyser, n'ont pas toutes les méthodes de mise en œuvre, etc.
En bref, je pense que le 'petit budget' version est très difficile de bien faire, et le "vrai" version est très, très difficile de bien faire. (Où le 'dur' de mesures à la fois de "l'effort" et "difficulté technique'.)
NRefactory le fera pour vous.
Pour la solution "1" vous avez une nouvelle installation .NET 4 à le faire rapidement et facilement.
Donc, si vous pouvez avoir votre programme à des convertis .NET 4, il serait votre meilleur choix.