Як я можу надійно визначити тип змінної, яка оголошується за допомогою var під час проектування?


109

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

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

Для цього потрібно знати тип речі, що завершується, бути відомим. Якщо це рядок, існує відомий набір можливих методів та властивостей; якщо це Int32, він має окремий набір тощо.

Використовуючи семантичний пакет коду лексеру / парсера, доступний в emacs, я можу знайти декларації змінних та їх типи. Враховуючи це, просто використовувати відображення, щоб отримати методи та властивості за типом, а потім представити користувачеві список параметрів. (Добре, не зовсім просто зробити в Emacs, але використовуючи можливість запуску процесу Powershell всередині Emacs , стає набагато простіше. Я пишу для користувача збірки .NET , щоб зробити відображення, завантажте його в PowerShell, а потім Elisp працює в межах emacs може відправляти команди в паттерн і читати відповіді за допомогою comint. В результаті emacs може швидко отримати результати відображення.)

Проблема виникає, коли код використовує varв декларації про завершену річ. Це означає, що тип не вказаний явно, і завершення не працюватиме.

Як я можу надійно визначити фактично використаний тип, коли змінна оголошується за допомогою varключового слова? Щоб було зрозуміло, мені не потрібно визначати це під час виконання. Я хочу визначити це в "Час проектування".

Поки що у мене є такі ідеї:

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

    Я знаю, як все це зробити. Але це звучить надзвичайно важко для кожного запиту на завершення в редакторі.

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

  2. складати та перевіряти ІЛ

    Просто складіть декларацію в модуль, а потім огляньте ІЛ, щоб визначити фактичний тип, який був зроблений висновком компілятора. Як це було б можливо? Що б я використав для дослідження ІЛ?

Якісь кращі ідеї там? Коментарі? пропозиції?


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

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


ОНОВЛЕННЯ - Правильна відповідь, не згадана вище, але її обережно зазначив Ерік Ліпперт, полягає в застосуванні повної системи виводу типу вірності. Це єдиний спосіб надійно визначити тип вару на час проектування. Але це також зробити непросто. Оскільки я не відчуваю жодних ілюзій, що хочу спробувати створити таке, я взяв ярлик варіанту 2 - витягніть відповідний код декларації та скомпілюйте його, а потім огляньте отриманий ІР.

Це фактично працює для справедливого набору сценаріїв завершення.

Наприклад, припустимо, у наступних фрагментах коду:? - це позиція, в якій користувач просить завершити. Це працює:

var x = "hello there"; 
x.?

Завершення розуміє, що x - це String, і забезпечує відповідні параметри. Це робиться, генеруючи та потім компілюючи такий вихідний код:

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

... а потім огляд ІЛ з простим відображенням.

Це також працює:

var x = new XmlDocument();
x.? 

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

Це також працює:

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

Це просто означає, що інспекція ІЛ повинна знайти тип третьої локальної змінної замість першої.

І це:

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.

Мені доведеться подумати про те, наскільки цінні ці речі, перш ніж розглянути їх вирішення через те, що, безумовно, є "обмеженим дизайном" (ввічливим словом для злому) для завершення.

Підхід до вирішення проблеми із залежностями від аргументів методу чи методів екземплярів полягав би у заміні у фрагменті коду, який генерується, компілюється та потім аналізується ІЛ, посиланнями на ці речі на "синтетичні" локальні параметри одного типу.


Ще одне оновлення - завершення на 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 є прямим за допомогою 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. Тож на цьому етапі ми запитуємо визначник типу "що таке тип у"? Потім він запускає оцінювач виразів, який аналізує x.ToCharArray () і запитує "який тип x"? У нас є визначник типу для того, що говорить "Мені потрібно шукати" Рядок "у поточному контексті". У поточному типі немає рядка String, тому ми шукаємо в просторі імен. Його також немає, тому ми заглянемо у використання директив і виявимо, що існує "використовуюча система" і що система має тип String. Гаразд, так це тип x.

Потім ми запитуємо метадані System.String для типу ToCharArray, і він говорить, що це System.Char []. Супер. Отже, у нас є тип для y.

Тепер ми запитуємо "чи System.Char [] має метод Де?" Ні. Отже, ми розглянемо використання директив; ми вже попередньо обчислили базу даних, що містить усі метадані для методів розширення, які, можливо, можуть бути використані.

Тепер ми говоримо "Добре, існує вісімнадцять десятків методів розширення з назвою" Де в області ", чи є в будь-якого з них перший формальний параметр, тип якого сумісний із System.Char []?" Тож ми розпочинаємо тест на конвертованість. Однак методи розширення де є загальними , це означає, що ми маємо робити умовивід.

Я написав спеціальний тип перетворюючого двигуна, який може обробляти неповні умовиводи від першого аргументу до методу розширення. Ми запускаємо типовий підхід і виявляємо, що існує метод 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, є і залишатиметься лише на вимогу; немає попередніх пропозицій. Для іншого - я буду реалізовувати базову підтримку місцевих жителів, але я із задоволенням заїдаю, коли все стане волохатим, або коли графік залежності перевищить певну межу. Не впевнений, яка ще є межа. Знову дякую.
Чесо

13
Мені чесно кидається на думку, що все це може працювати так швидко і надійно, особливо з лямбда-висловами та загальним висновком типу. Я насправді був дуже здивований, коли вперше написав лямбда-вираз, і Intellisense знав тип мого параметра, коли я натискав. Дякую за цей маленький зазирнути в чари.
Дан Брайант

21
@Dan: Я бачив (або написав) вихідний код, і він зважає на те, що він теж працює. :-) Там є деякі волохаті речі.
Ерік Ліпперт

11
Хлопці Eclipse, ймовірно, роблять це краще, тому що вони приголомшливіші за компілятор C # та команду IDE.
Ерік Ліпперт

23
Я взагалі не пам'ятаю робити цей дурний коментар. Це навіть не має сенсу. Я, мабуть, був п’яний. Вибачте.
Томаш Андрле

15

Я можу вам приблизно сказати, як Delphi IDE працює з компілятором Delphi, щоб робити інтелігенцію (розуміння коду - це, як Delphi називає це). Це не 100% застосовно до C #, але це цікавий підхід, який заслуговує на розгляд.

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

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

Ця комбінація функцій означає, що аналізатор має приблизно всю інформацію, необхідну для ознайомлення з кодом для будь-якої точки, де це потрібно. Це працює так: IDE інформує лексему компілятора про позицію курсору (точку, де бажане введення коду), і лексер перетворює це в спеціальний маркер (його називають токеном kibitz). Щоразу, коли аналізатор зустріне цей маркер (який може бути де завгодно), він знає, що це сигнал для повернення всієї інформації, яку він має, в редактор. Це робиться за допомогою 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>return і TOut можна зробити висновок з результату лямбда-виразу, тому отриманий тип елементів повинен бути IEnumerable<Bar>.

Правильно, воно може заглибитися досить глибоко. Мені комфортно вирішувати всі залежності. Поміркувавши над цим, перший варіант, який я описав - компілювати та викликати - абсолютно неприйнятний, тому що виклик коду може мати побічні ефекти, як-от оновлення бази даних, і це редактор не повинен робити. Компіляція нормальна, виклик - ні. Що стосується побудови AST, я не думаю, що я хочу цього робити. Дійсно, я хочу відкласти цю роботу компілятору, який вже знає, як це зробити. Я хочу мати можливість попросити компілятора сказати мені те, що я хочу знати. Я просто хочу просту відповідь.
Чесо

Завдання перевірити його від компіляції полягає в тому, що залежності можуть бути довільно глибокими, а значить, вам може знадобитися побудувати все для того, щоб компілятор генерував код. Якщо ви це зробите, я думаю, ви можете використовувати налагоджувальні символи з генерованим ІЛ і співставити тип кожного локального з ним символу.
Ден Брайант

1
@Cheeso: компілятор не пропонує такий тип аналізу як послугу. Я сподіваюся, що в майбутньому це буде, але не обіцяє.
Ерік Ліпперт

так, я думаю, що це може бути шлях - вирішити всі залежності, а потім скласти та перевірити ІЛ. @Eric, добре знати. Поки що, якщо я не прагну зробити повний аналіз AST, тож я мушу вдатися до брудного зламу для створення цієї послуги за допомогою існуючих інструментів. Наприклад, складіть інтелектуально сконструйований фрагмент коду, а потім використовуйте програмно ILDASM (або подібний) для отримання відповіді, яку я шукаю.
Чешо

4

Оскільки ви орієнтовані на Emacs, можливо, найкраще почати з набору CEDET. Усі деталі, які Ерік Ліпперт вже висвітлюються в аналізаторі коду в інструменті CEDET / Semantic для C ++. Існує також аналізатор C # (який, мабуть, потребує невеликого TLC), тому єдині деталі, які відсутні, пов'язані з налаштуванням необхідних частин для C #.

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

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


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

Побічний коментар - CEDET надзвичайно амбітний, але мені було важко використовувати та розширювати. В даний час аналізатор розглядає "простір імен" як показник класу в C #. Я навіть не міг зрозуміти, як додати "простір імен" як виразний синтаксичний елемент. Це завадило всьому іншому синтаксичному аналізу, і я не міг зрозуміти, чому. Раніше я пояснював труднощі, що виникли в рамках завершення. Крім цих проблем, є шви і перекриття між шматками. Як один із прикладів, навігація є частиною як семантичного, так і сенаторського характеру. CEDET здається привабливим, але врешті-решт ... це занадто непросто, щоб взяти на себе зобов'язання.
Чесо

Cheeso, якщо ви хочете отримати максимальну користь із менш задокументованих частин CEDET, найкраще скористатися списком розсилки. Питання легко заглибитись у сфери, які ще не були добре розвинені, тому для розробки хороших рішень або пояснення існуючих потрібні кілька ітерацій. Зокрема, для C #, оскільки я нічого не знаю про це, не буде простих однозначних відповідей.
Ерік

2

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

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

Коротше кажучи, я думаю, що "низькобюджетна" версія дуже складна, а "справжня" версія - дуже, дуже складна. (Тут "важко" вимірює як "зусилля", так і "технічні труднощі".)


Так, версія з низьким бюджетом має деякі чіткі обмеження. Я намагаюся вирішити, що таке "досить добре", і чи можу я зустрітись з цим баром. На моєму власному досвіді, що я зараз маю на увазі, це робить текст C # в Emacs набагато приємнішим.
Чешо


0

Для рішення "1" у вас є новий інструмент в .NET 4, щоб зробити це швидко і легко. Тож якщо ви зможете перетворити програму на .NET 4, це буде найкращим вибором.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.