Как я могу надежно определить тип переменной, которая объявляется с помощью var во время проектирования?

Я работаю над завершением (intellisense) средство для C# в emacs.

Идея, если пользователь вводит фрагмент, то просит завершение через конкретное сочетание клавиш, средство завершения будет использовать отражение.NET для определения возможных завершений.

Выполнение этого требует, чтобы тип завершаемой вещи, был известен. Если это - строка, существует известный набор возможных методов и свойств; если это - Int32, это имеет отдельный набор и так далее.

Используя семантический, пакет лексического анализатора/синтаксического анализатора кода, доступный в emacs, я могу определить местоположение объявлений переменной и их типов. Учитывая, что, это просто для использования отражения, чтобы получить методы и свойства на типе, и затем представить список опций пользователю. (Хорошо, не совсем простой, чтобы сделать в emacs, но использовании способности выполнить процесс powershell внутри emacs, это становится намного легче. Я пишу пользовательский блок.NET, чтобы сделать отражение, для загрузки его в powershell, и затем elisp работающий в emacs может отправить команды в powershell и считать ответы через comint. В результате emacs может получить результаты отражения быстро.)

Проблема прибывает, когда код использует var в объявлении завершаемой вещи. Это означает, что тип явно не указан, и завершение не будет работать.

Как я могу надежно определить фактический используемый тип, когда переменная объявляется с var ключевое слово? Только, чтобы быть ясным, я не должен определять его во времени выполнения. Я хочу определить его во "Время проектирования".

До сих пор у меня есть эти идеи:

  1. скомпилируйте и вызовите:
    • извлеките оператор объявления, например, 'нечто var = "строковое значение"';
    • свяжите оператор 'нечто. GetType ()';
    • динамично скомпилируйте получающийся фрагмент C# это в новый блок
    • загрузите блок в новый AppDomain, выполните framgment и получите тип возврата.
    • разгрузите и отбросьте блок

    Я знаю, как сделать все это. Но это звучит ужасно как тяжеловес для каждого запроса завершения в редакторе.

    Я предполагаю, что мне не нужен свежий новый AppDomain каждый раз. Я мог снова использовать единственный AppDomain для нескольких временных блоков и амортизировать стоимость установки его и разъединения его через несколько запросов завершения. Это - больше тонкая настройка основной идеи.

  2. скомпилируйте и осмотрите IL

    Просто скомпилируйте объявление в модуль и затем осмотрите IL, для определения фактического типа, который был выведен компилятором. Как это было бы возможно? Что я использовал бы для исследования IL?

Какие-либо лучшие идеи там? Комментарии? предложения?


РЕДАКТИРОВАНИЕ - думающий об этом далее, компилировать-и-вызывать не приемлемо, потому что вызывание может иметь побочные эффекты. Таким образом, первая опция должна быть исключена.

Кроме того, я думаю, что не могу принять присутствие.NET 4.0.


ОБНОВЛЕНИЕ - корректный ответ, неупомянутый выше, но мягко указанный Eric Lippert, должен реализовать полную систему вывода типа точности. Это; s единственный способ надежно определить тип var во время проектирования. Но, также не легко сделать. Поскольку я не переношу иллюзий, что я хочу попытаться создать такую вещь, я взял ярлык опции 2 - извлекают соответствующий код объявления, и компилируют его, затем осматривают получающийся IL.

Это на самом деле работает для справедливого подмножества сценариев завершения.

Например, предположите в следующих фрагментах кода? положение, в котором пользователь просит завершение. Это работает:

var x = "hello there"; 
x.?

Завершение понимает, что x является Строкой и предоставляет подходящие возможности. Это делает это путем генерации и затем компиляции следующего исходного кода:

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

... и затем осматривая IL с простым отражением.

Это также работает:

var x = new XmlDocument();
x.? 

Механизм добавляет соответствующие пункты использования к сгенерированному исходному коду, так, чтобы он скомпилировал правильно, и затем контроль IL является тем же.

Это работает, также:

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

Это просто означает, что контроль IL должен найти тип третьей локальной переменной вместо первого.

И это:

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

... который является всего одним уровнем глубже что предшествующий пример.

Но, что не работает, завершение на любой локальной переменной, инициализация которой зависит в любой точке от члена экземпляра или локальном аргументе метода. Как:

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

Ни синтаксис LINQ.

Я должен буду думать о том, насколько ценный те вещи состоят в том, прежде чем я рассмотрю обращение к ним через то, что является определенно "ограниченным дизайном" (вежливое слово для взлома) для завершения.

Подход к решению проблемы с зависимостями от аргументов метода или методов экземпляра должен был бы заменить во фрагменте кода, который сгенерирован, скомпилированный и затем проанализированная IL, ссылки на те вещи с "синтетическим" локальным Варом того же типа.


Другое Обновление - завершение на Варе, которые зависят от членов экземпляра, теперь работает.

То, что я сделал, было опросить тип (через семантический) и затем генерировать синтетического заместителя участников для всех существующих участников. Поскольку C# буферизует как это:

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
        {
            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...

... сгенерированный код, который компилируется, так, чтобы я мог узнать из вывода о IL тип локального var nnn, похож на это:

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 { foo, foo.Length.ToString() };
       var z = fred.Count;
       var mmm = count + z + CsharpCompletion.PrivateStaticField1;
       var nnn = this.InstanceMethod1(mmm);
      }
  }
}

Весь экземпляр и статические участники типа доступен в скелетном коде. Это компилирует успешно. В той точке, определяя тип локального var просто через Отражение.

Что делает, это возможное:

  • способность выполнить powershell в emacs
  • компилятор C# действительно быстр. На моей машине требуется приблизительно 0,5 с для компиляции блока в оперативной памяти. Не достаточно быстро для анализа между нажатиями клавиш, но достаточно быстро поддерживать поколение по запросу списков завершения.

Я еще не изучил LINQ.
Это будет намного большей проблемой, потому что семантический лексический анализатор/синтаксический анализатор emacs имеет для C#, не "делает" LINQ.

109
задан 12 revs, 3 users 100% 20 February 2012 в 14:13
поделиться

7 ответов

Я могу описать вам, как мы делаем это эффективно в «настоящей» C # IDE.

Первое, что мы делаем, это запускаем проход, который анализирует только «верхний уровень» в исходном коде. Мы пропускаем все тела методов. Это позволяет нам быстро создать базу данных с информацией о том, какое пространство имен, типы и методы (а также конструкторы и т. Д.) Находятся в исходном коде программы. Анализ каждой строчки кода в каждом теле метода займет слишком много времени, если вы пытаетесь делать это между нажатиями клавиш.

Когда IDE необходимо определить тип конкретного выражения внутри тела метода - скажем, вы набрали «foo». и нам нужно выяснить, что входит в состав foo - мы делаем то же самое; мы пропускаем столько работы, сколько можем.

Мы начинаем с этапа, который анализирует только объявления локальной переменной в этом методе. Когда мы запускаем этот проход, мы сопоставляем пару «область видимости» и «имя» «определителю типа». «Определитель типа» - это объект, который представляет понятие «Я могу определить тип этого локального объекта, если мне нужно». Определение типа местного жителя может быть дорогостоящим, поэтому мы хотим отложить эту работу, если нам нужно.

Теперь у нас есть база данных, созданная лениво, которая может сказать нам тип каждого локального объекта. Итак, возвращаясь к этому "foo." - мы выясняем, в каком операторе находится соответствующее выражение, а затем запускаем семантический анализатор только для этого оператора. Например, предположим, что у вас есть тело метода:

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

, и теперь нам нужно определить, что foo имеет тип char.Мы создаем базу данных, в которой есть все метаданные, методы расширения, типы исходного кода и так далее. Мы создаем базу данных, в которой есть определители типов для x, y и z. Разбираем высказывание, содержащее интересное выражение. Мы начнем с его синтаксического преобразования в

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

. Чтобы определить тип foo, мы должны сначала узнать тип y. Итак, на этом этапе мы спрашиваем определитель типа «что это за тип y»? Затем он запускает оценщик выражений, который анализирует x.ToCharArray () и спрашивает «какой тип x»? У нас есть определитель типа, который говорит: «Мне нужно найти строку« в текущем контексте »». В текущем типе нет типа String, поэтому мы смотрим в пространство имен. Его тоже нет, поэтому мы смотрим в директивы using и обнаруживаем, что есть «using System» и что System имеет тип String. Хорошо, это тип x.

Затем мы запрашиваем у метаданных System.String тип ToCharArray, и он говорит, что это System.Char []. Супер. Итак, у нас есть тип для y.

Теперь мы спрашиваем: "Есть ли у System.Char [] метод Where?" Нет. Итак, мы смотрим в директивы using; мы уже предварительно вычислили базу данных, содержащую все метаданные для методов расширения, которые могли бы быть использованы.

Теперь мы говорим: «Хорошо, существует восемнадцать дюжин методов расширения с именами Где в области видимости, есть ли у любого из них первый формальный параметр, тип которого совместим с System.Char []?» Итак, мы начинаем раунд тестирования конвертируемости. Однако методы расширения Where являются общими , что означает, что мы должны делать вывод типа.

Я написал специальный механизм вывода типов, который может обрабатывать неполные выводы из первого аргумента метода расширения. Мы запускаем вывод типа и обнаруживаем, что существует метод Where, который принимает IEnumerable , и что мы можем сделать вывод из System.Char [] в IEnumerable , поэтому T - это System.Char.

Сигнатура этого метода - Where (этот элемент IEnumerable , предикат Func ) , и мы знаем, что T - это System.Char. Также мы знаем, что первый аргумент в круглых скобках метода расширения - это лямбда. Итак, мы запускаем модуль вывода типа лямбда-выражения, который говорит, что «формальный параметр foo предполагается как System.Char», используйте этот факт при анализе остальной части лямбда.

Теперь у нас есть вся информация, необходимая для анализа тела лямбда, которым является «foo.». Мы ищем тип foo, обнаруживаем, что согласно связке лямбда это System.Char, и все готово; мы отображаем информацию о типе для System.Char.

И мы делаем все, кроме анализа «верхнего уровня» между нажатиями клавиш . Это настоящая хитрость. На самом деле написать весь анализ несложно; это делает его достаточно быстрым , чтобы вы могли делать это со скоростью набора текста, а это действительно сложная задача.

Удачи!

202
ответ дан 24 November 2019 в 03:21
поделиться

Поскольку вы нацелены на Emacs, возможно, лучше будет начать с пакета CEDET. Все детали, которые Эрик Липперт уже покрыл в анализаторе кода в CEDET / Semantic tool для C ++. Существует также синтаксический анализатор C # (который, вероятно, требует небольшого TLC), поэтому единственные отсутствующие части связаны с настройкой необходимых частей для C #.

Базовое поведение определяется в основных алгоритмах, которые зависят от перегружаемых функций, определенных для каждого языка. Успех завершающего двигателя зависит от того, сколько настроек было выполнено. С С ++ в качестве руководства получить поддержку, подобную С ++, не должно быть так уж плохо.

Ответ Дэниела предлагает использовать MonoDevelop для синтаксического анализа и анализа. Это может быть альтернативный механизм вместо существующего синтаксического анализатора C # или его можно использовать для расширения существующего синтаксического анализатора.

4
ответ дан 24 November 2019 в 03:21
поделиться

Преуспеть - сложная задача. В основном вам нужно смоделировать спецификацию / компилятор языка с помощью большей части лексирования / синтаксического анализа / проверки типов и построить внутреннюю модель исходного кода, которую вы затем можете запросить. Эрик подробно описывает это для C #. Вы всегда можете загрузить исходный код компилятора F # (часть F # CTP) и взглянуть на service.fsi , чтобы увидеть интерфейс, предоставляемый компилятором F #, который служба языка F # использует для обеспечения intellisense, всплывающие подсказки для предполагаемых типов и т. д. Это дает представление о возможном «интерфейсе», если у вас уже есть компилятор, доступный в качестве API для вызова.

Другой путь - повторно использовать компиляторы как есть, как вы описываете, а затем использовать отражение или посмотреть на сгенерированный код. Это проблематично с точки зрения того, что вам нужны «полные программы» для получения вывода компиляции из компилятора, тогда как при редактировании исходного кода в редакторе у вас часто есть только «частичные программы», которые еще не анализируются, не реализованы все методы и т. д.

Короче говоря, я думаю, что «малобюджетную» версию очень трудно сделать хорошо, а «настоящую» версию очень, очень сделать хорошо. (Здесь «жесткий» измеряет как «усилия», так и «техническую сложность».)

2
ответ дан 24 November 2019 в 03:21
поделиться

Если вы не хотите писать собственный синтаксический анализатор для построения абстрактного синтаксического дерева, вы можете использовать синтаксические анализаторы из SharpDevelop или MonoDevelop , оба из которых имеют открытый исходный код.

7
ответ дан 24 November 2019 в 03:21
поделиться

Для решения «1» у вас есть новое средство в .NET 4, позволяющее делать это быстро и легко. Итак, если у вас есть программа, преобразованная в .NET 4, будет вашим лучшим выбором.

0
ответ дан 24 November 2019 в 03:21
поделиться

Я могу рассказать вам, как IDE Delphi работает с компилятором Delphi, чтобы сделать intellisense (понимание кода - это то, как Delphi называет это). Это не на 100% применимо к C#, но это интересный подход, который заслуживает внимания.

Большая часть семантического анализа в Delphi выполняется в самом парсере. Выражения типизируются по мере их разбора, за исключением ситуаций, когда это непросто - в этом случае используется разбор с опережением, чтобы выяснить, что имеется в виду, а затем это решение используется в разборе.

Разбор в основном представляет собой рекурсивный спуск LL(2), за исключением выражений, которые разбираются с использованием предшествования операторов. Одной из особенностей Delphi является то, что это однопроходный язык, поэтому конструкции должны быть объявлены до их использования, поэтому для получения этой информации не требуется проход на верхний уровень.

Эта комбинация особенностей означает, что синтаксический анализатор имеет примерно всю информацию, необходимую для понимания кода, в любой точке, где это необходимо. Это работает следующим образом: IDE сообщает лексеру компилятора позицию курсора (точка, где требуется понимание кода), и лексер превращает это в специальный токен (он называется kibitz token). Всякий раз, когда синтаксический анализатор встречает этот маркер (который может быть где угодно), он знает, что это сигнал для отправки всей имеющейся у него информации обратно в редактор. Он делает это с помощью longjmp, так как написан на языке C; что он делает, так это уведомляет конечного пользователя о том, в какой синтаксической конструкции (т.е. грамматическом контексте) была найдена точка kibitz, а также все символьные таблицы, необходимые для этой точки. Так, например, если контекст находится в выражении, которое является аргументом метода, мы можем проверить перегрузки метода, посмотреть на типы аргументов и отфильтровать допустимые символы только до тех, которые могут разрешиться в этот тип аргумента (это сокращает много нерелевантного мусора в выпадающем списке). Если символ находится во вложенном контексте области видимости (например, после "."), синтаксический анализатор передаст ссылку на область видимости, и IDE сможет перечислить все символы, найденные в этой области видимости.

Делаются и другие вещи; например, тела методов пропускаются, если лексема kibitz не находится в их диапазоне - это делается оптимистично, и если лексема пропущена, она откатывается назад. Эквивалент методов расширения - помощники классов в Delphi - имеют своего рода версионный кэш, поэтому их поиск происходит достаточно быстро. Но вывод общих типов в Delphi намного слабее, чем в C#.

Теперь к конкретному вопросу: вывод типов переменных, объявленных с помощью var, эквивалентен тому, как Паскаль выводит тип констант. Он исходит из типа выражения инициализации. Эти типы строятся снизу вверх. Если x имеет тип Integer, а y имеет тип Double, то x + y будет иметь тип Double, потому что таковы правила языка; и так далее. Вы следуете этим правилам, пока не определите тип для полного выражения в правой части, и именно этот тип вы используете для символа слева.

15
ответ дан 24 November 2019 в 03:21
поделиться

Системы Intellisense обычно представляют код с помощью абстрактного синтаксического дерева, что позволяет им определять тип возврата функции, назначаемой переменной 'var', примерно так же, как это делает компилятор. Если вы используете VS Intellisense, вы можете заметить, что он не выдает тип переменной var, пока вы не введете правильное (разрешимое) выражение присваивания. Если выражение все еще неоднозначно (например, он не может полностью определить общие аргументы выражения), тип var не будет определен. Это может быть довольно сложным процессом, поскольку для разрешения типа может потребоваться довольно глубокое проникновение в дерево. Например:

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

Возвращаемый тип - IEnumerable, но для его разрешения необходимо знать:

  1. myList имеет тип, реализующий IEnumerable.
  2. Существует метод расширения OfType, который применяется к IEnumerable.
  3. Результирующее значение - IEnumerable, и существует метод расширения Select, который применяется к нему.
  4. Лямбда-выражение foo => foo.Bar имеет параметр foo типа Foo. Это вытекает из использования Select, который принимает Func, а поскольку TIn известен (Foo), тип foo может быть выведен.
  5. Тип Foo имеет свойство Bar, которое имеет тип Bar. Мы знаем, что Select возвращает IEnumerable, а TOut можно вывести из результата лямбда-выражения, поэтому результирующий тип элементов должен быть IEnumerable.
4
ответ дан 24 November 2019 в 03:21
поделиться
Другие вопросы по тегам:

Похожие вопросы: