Как я могу надежно определить тип переменной, объявленной с использованием var во время разработки? - PullRequest
109 голосов
/ 15 мая 2010

Я работаю над средством завершения (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. Я мог бы повторно использовать один домен приложений для нескольких временных сборок и амортизировать стоимость его установки. и разорвать его, через несколько запросов на завершение. Это более тонкая настройка основной идеи.

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

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

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


EDIT - если подумать об этом, компиляция и вызов недопустимы, потому что вызов может иметь побочные эффекты. Таким образом, первый вариант должен быть исключен.

Кроме того, я думаю, что не могу предположить наличие .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.? 

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

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

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

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

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

Ответы [ 8 ]

202 голосов
/ 15 мая 2010

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

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

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

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

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

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. Таким образом, в этот момент мы спрашиваем определитель типа «какой тип у»? Затем он запускает вычислитель выражений, который анализирует x.ToCharArray () и спрашивает «что это за тип x»? У нас есть определитель типа для того, что говорит: «Мне нужно посмотреть« Строка »в текущем контексте». В текущем типе нет типа String, поэтому мы смотрим в пространство имен. Его тоже нет, поэтому мы смотрим в директивах using и обнаруживаем, что существует «using System» и что System имеет тип String. ОК, это тип х.

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

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

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

Я написал специальный механизм вывода типов, который может обрабатывать неполные выводы из первого аргумента в метод расширения. Мы запустили вывод типа и обнаружили, что есть метод 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.

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

Удачи!

15 голосов
/ 16 мая 2010

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

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

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

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

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

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

7 голосов
/ 15 мая 2010

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

4 голосов
/ 15 мая 2010

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

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

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

4 голосов
/ 15 мая 2010

Системы 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, которое принимает Func<TIn,TOut>, и поскольку TIn известен (Foo), тип foo может быть выведен.
  5. Тип Foo имеет свойство Bar, которое имеет тип Bar. Мы знаем, что Select возвращает IEnumerable<TOut>, и TOut может быть выведен из результата лямбда-выражения, поэтому результирующий тип элементов должен быть IEnumerable<Bar>.
2 голосов
/ 11 января 2013

NRefactory сделает это за вас.

2 голосов
/ 15 мая 2010

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

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

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

0 голосов
/ 15 мая 2010

Для решения "1" у вас есть новая возможность в .NET 4, чтобы сделать это быстро и легко. Так что если вы можете конвертировать вашу программу в .NET 4, это будет вашим лучшим выбором.

...