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


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

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

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

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

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

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

пока у меня такие идеи:

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

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

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

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

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

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


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

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

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

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

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, ссылки на эти вещи с "синтетическими" локальными vars того же типа.


Еще Одно Обновление - завершение на vars, которые зависят от членов экземпляра, теперь работает.

то, что я сделал, это опрос типа (через семантику), а затем создать синтетические резервные члены для всех существующих членов. Для буфера 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 является простым с помощью отражения.

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

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

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

8 109

8 ответов:

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

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

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

мы начинаем с прохода, который анализирует только локальная переменная объявления в этом методе. Когда мы запускаем этот проход, мы делаем сопоставление из пары "область" и " имя " в " тип определитель." В "определитель типа" это объект, который представляет понятие "я могу определить тип этого если мне нужно". Разработка типа локального может быть дорогостоящей, поэтому мы хотим отложить эту работу, если нам нужно.

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

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"? У нас есть определитель типа для того, что говорит: "мне нужно посмотреть строку "в текущем контексте". В текущем типе нет строки типа, поэтому мы ищем в пространстве имен. Его там тоже нет, поэтому мы смотрим в директивах using и обнаруживаем, что есть "using System", и эта система имеет строку типа. Хорошо, так вот в чем дело тип x.

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

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

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

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

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

теперь у нас есть вся информация нам нужно проанализировать тело лямбды, которое есть "фу.". Мы смотрим на тип foo, мы обнаруживаем, что в соответствии с лямбда-связующим это система.Char, и мы закончили; мы показываем информацию о типе для системы.Пеструшка.

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

удачи!

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

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

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

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

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

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

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

системы Intellisense обычно представляют код с помощью абстрактного синтаксического дерева, что позволяет им разрешать возвращаемый тип функции, назначаемой переменной 'var', более или менее так же, как и компилятор. Если вы используете VS Intellisense, вы можете заметить, что он не даст вам тип var, пока вы не закончите вводить допустимое (разрешимое) выражение назначения. Если выражение все еще неоднозначно (например, оно не может полностью вывести общие аргументы для выражения) тип 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> и так как олово известно (Foo), тип foo может быть выведен.
  5. тип Foo имеет панель свойств, которая имеет тип Bar. Мы знаем, что Select возвращает IEnumerable<TOut> и TOut можно вывести из результата лямбда-выражения, поэтому результирующий тип элементов должен быть IEnumerable<Bar>.

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

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

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

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

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

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

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

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