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


109

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

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

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

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

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

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

Пока у меня есть эти идеи:

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

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

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

  2. компилировать и проверять IL

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

Есть идеи получше? Комментарии? предложения?


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

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


ОБНОВЛЕНИЕ . Правильный ответ, не упомянутый выше, но мягко указанный Эриком Липпертом, - реализовать систему вывода типов с полной точностью. Это единственный способ надежно определить тип переменной во время разработки. Но сделать это тоже непросто. Поскольку у меня нет иллюзий, что я хочу попытаться создать такую ​​вещь, я воспользовался сокращением варианта 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.? 

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

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

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

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

И это:

var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
    {
        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<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...

... сгенерированный код, который компилируется, чтобы я мог узнать из выходного IL тип локальной переменной 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<String> { foo, foo.Length.ToString() };
       var z = fred.Count;
       var mmm = count + z + CsharpCompletion.PrivateStaticField1;
       var nnn = this.InstanceMethod1(mmm);
      }
  }
}

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

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

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

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


4
Тип foo определяется и заполняется компилятором посредством вывода типа. Подозреваю, что механизмы совсем другие. Возможно, у механизма вывода типов есть зацепка? По крайней мере, я бы использовал «вывод типа» в качестве тега.
Джордж Мауэр

3
Ваша методика создания «поддельной» объектной модели, которая имеет все типы, но не имеет семантики реальных объектов, является хорошей. Вот как я когда-то делал IntelliSense для JScript в Visual InterDev; мы делаем «фальшивую» версию объектной модели IE, которая имеет все методы и типы, но не имеет побочных эффектов, а затем запускаем небольшой интерпретатор над проанализированным кодом во время компиляции и смотрим, какой тип вернется.
Эрик Липперт

Ответы:


202

Я могу описать вам, как мы делаем это эффективно в «настоящей» 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<T>, и что мы можем сделать вывод из System.Char [] IEnumerable<System.Char>, поэтому T - это System.Char.

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

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

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

Удачи!


8
Эрик, спасибо за полный ответ. Вы приоткрыли мне глаза. Что касается emacs, я не стремился создать динамический движок между нажатиями клавиш, который бы конкурировал с Visual Studio с точки зрения качества взаимодействия с пользователем. Во-первых, из-за задержки ~ 0,5 с, присущей моему дизайну, средство на основе emacs есть и останется только по запросу; нет предложений с опережающим вводом. Во-вторых, я буду реализовывать базовую поддержку var locals, но я буду рад, когда все станет плохо или когда график зависимостей превысит определенный предел. Еще не уверен, что это за предел. Еще раз спасибо.
Cheeso

13
Честно говоря, у меня не получается, что все это может работать так быстро и надежно, особенно с лямбда-выражениями и выводом общих типов. На самом деле я был весьма удивлен, когда впервые написал лямбда-выражение, а Intellisense знала тип моего параметра, когда я нажимал., Хотя оператор еще не был завершен, и я никогда явно не указывал общие параметры методов расширения. Спасибо за этот небольшой взгляд на волшебство.
Дэн Брайант

21
@Dan: Я видел (или писал) исходный код, и у меня не получается, что он тоже работает. :-) Там есть какие-то волосатые штучки.
Эрик Липперт

11
Ребята из Eclipse, вероятно, справляются с этим лучше, потому что они круче, чем команда компилятора C # и IDE.
Эрик Липперт

23
Я вообще не помню, чтобы делал этот глупый комментарий. Это даже не имеет смысла. Я, должно быть, был пьян. Сожалею.
Tomas Andrle

15

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

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

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

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

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

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


7

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


4

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

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

Тип возврата есть IEnumerable<Bar>, но для его решения необходимо знать:

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

Да, это может оказаться довольно глубоким. Мне комфортно разрешать все зависимости. Если подумать, первый вариант, который я описал - компиляция и вызов - абсолютно неприемлем, потому что вызов кода может иметь побочные эффекты, такие как обновление базы данных, а это не то, что редактор должен делать. Компиляция - это нормально, вызов - нет. Что касается создания AST, я не думаю, что хочу этим заниматься. На самом деле я хочу передать эту работу компилятору, который уже знает, как это делать. Я хочу иметь возможность попросить компилятор сказать мне то, что я хочу знать. Я просто хочу получить простой ответ.
Cheeso

Проблема с проверкой его из компиляции заключается в том, что зависимости могут быть сколь угодно глубокими, а это означает, что вам может потребоваться построить все, чтобы компилятор сгенерировал код. Если вы это сделаете, я думаю, вы можете использовать символы отладчика с сгенерированным IL и сопоставить тип каждого локального с этим символом.
Дэн Брайант

1
@Cheeso: компилятор не предлагает такого рода анализ типов в качестве услуги. Надеюсь, что в будущем так и будет, но никаких обещаний.
Эрик Липперт

да, я думаю, это может быть выход - разрешить все зависимости, а затем скомпилировать и проверить IL. @ Эрик, полезно знать. На данный момент, если я не стремлюсь выполнять полный анализ AST, я должен прибегнуть к грязному взлому, чтобы создать эту услугу с использованием существующих инструментов. Например, скомпилируйте грамотно сконструированный фрагмент кода, а затем программно используйте ILDASM (или аналогичный), чтобы получить ответ, который я ищу.
Cheeso

4

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

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

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


Да, я знаю о CEDET, и я использую поддержку C # в каталоге contrib для семантики. Semantic предоставляет список локальных переменных и их типов. Механизм завершения может сканировать этот список и предлагать пользователю правильный выбор. Проблема в том, когда переменная var. Semantic правильно определяет его как var, но не обеспечивает вывод типа. Мой вопрос был конкретно вокруг того, как решить , что . Я также искал возможность подключения к существующему завершению CEDET, но не мог понять, как это сделать. Документация для CEDET ... а ... не полная.
Cheeso

Дополнительный комментарий - CEDET чрезвычайно амбициозен, но мне было трудно его использовать и расширять. В настоящее время синтаксический анализатор рассматривает «пространство имен» как индикатор класса в C #. Я даже не мог понять, как добавить «пространство имен» как отдельный синтаксический элемент. Это предотвратило весь другой синтаксический анализ, и я не мог понять почему. Ранее я объяснил трудности, с которыми я столкнулся с фреймворком завершения. Помимо этих проблем, между деталями есть швы и перекрытия. Например, навигация является частью семантики и сенатора. CEDET кажется заманчивым, но, в конце концов ... он слишком громоздкий, чтобы брать на себя обязательства.
Cheeso

Чизо, если вы хотите максимально использовать менее документированные части CEDET, лучше всего попробовать список рассылки. Вопросы легко охватывают области, которые еще не были хорошо разработаны, поэтому требуется несколько итераций, чтобы выработать хорошие решения или объяснить существующие. В частности, для C #, поскольку я ничего о нем не знаю, простых одноразовых ответов не будет.
Эрик

2

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

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

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


Да, у "малобюджетной" версии есть явные ограничения. Я пытаюсь решить, что такое «достаточно хорошо» и смогу ли я достичь этой планки. По моему собственному опыту, копирование того, что у меня есть, делает написание C # в emacs намного приятнее.
Cheeso


0

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.