Подтвердить что ты не робот

Как компиляция С# обойти вокруг файлов заголовков?

Я провел свою профессиональную жизнь как разработчик С#. Будучи студентом, я иногда использовал C, но не изучил его модель компиляции. Недавно я прыгнул на подножку и начал изучать Objective-C. Мои первые шаги только позволили мне узнать о дырах в моих ранее существовавших знаниях.

Из моих исследований, компиляция C/С++/ObjC требует, чтобы все встреченные символы были предварительно объявлены. Я также понимаю, что строительство - это двухэтапный процесс. Сначала вы компилируете каждый отдельный исходный файл в отдельные файлы объектов. Эти объектные файлы могут иметь undefined "символы" (которые в целом соответствуют идентификаторам, объявленным в файлах заголовков). Во-вторых, вы связываете файлы объектов вместе, чтобы сформировать окончательный вывод. Это довольно подробное объяснение, но оно удовлетворяет моему любопытству. Но я также хотел бы иметь аналогичное понимание на высоком уровне процесса сборки С#.

Q: Как процесс сборки С# обойти необходимость в файлах заголовков? Я бы предположил, что, возможно, этап компиляции делает два прохода?

(Изменить: следующий вопрос здесь Как C/С++/ Objective-C сравнить с С#, когда дело доходит до использования библиотек?)

4b9b3361

Ответ 1

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

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

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

Это позволяет компилятору узнать, что существует и чего нет (в его юниверсе).

Чтобы увидеть двухпроходный компилятор, проверьте следующий код, содержащий 3 проблемы, две проблемы, связанные с объявлениями, и одну проблему с кодом:

using System;

namespace ConsoleApplication11
{
    class Program
    {
        public static Stringg ReturnsTheWrongType()
        {
            return null;
        }

        static void Main(string[] args)
        {
            CallSomeMethodThatDoesntExist();
        }

        public static Stringg AlsoReturnsTheWrongType()
        {
            return null;
        }
    }
}

Обратите внимание, что компилятор будет жаловаться только на два типа Stringg, которые он не может найти. Если вы исправите их, то он жалуется на имя метода, вызванное в методе Main, которое он не может найти.

Ответ 2

UPDATE: этот вопрос был тема моего блога на 4 февраля 2010 года. Спасибо за отличный вопрос!

Позвольте мне рассказать вам. В самом основном смысле компилятор является "двухпроходным компилятором", потому что фазы, которые выполняет компилятор, следующие:

1) Генерация метаданных. 2) Генерация IL.

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

IL - это все, что входит в тело метода - фактический императивный код, а не метаданные о том, как структурирован код.

Первый этап фактически реализуется через большое количество проходов над источниками. Это намного больше двух.

Первое, что мы делаем, это взять текст источников и разбить его на поток токенов. То есть мы делаем лексический анализ, чтобы определить, что

class c : b { }

- это класс, идентификатор, двоеточие, идентификатор, левая фигурная, правая фигурная.

Затем мы выполняем "синтаксис верхнего уровня", где мы проверяем, что потоки токенов определяют грамматически правильную С# -программу. Однако мы пропускаем тела метода синтаксического анализа. Когда мы нажимаем тело метода, мы просто пробираемся через токены, пока не дойдем до соответствия фигурному. Мы вернемся к нему позже; мы только заботимся о получении достаточной информации для генерации метаданных на данный момент.

Затем мы делаем проход "объявление", где делаем заметки о местоположении каждого пространства имен и декларации типа в программе.

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

Затем мы выполняем пропуск, где мы проверяем, что все общие ограничения параметров для общих типов также ацикличны.

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

Затем мы выполняем проход, в котором мы вычисляем значения всех полей "const".

В этот момент у нас достаточно информации, чтобы испускать почти все метаданные для этой сборки. У нас по-прежнему нет информации о метаданных для закрытия итераций/анонимных функций или анонимных типов; мы делаем это поздно.

Теперь мы можем начать генерировать IL. Для каждого тела метода (и свойств, индексаторов, конструкторов и т.д.) Мы перематываем лексер до точки начала тела метода и анализируем тело метода.

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

Сначала мы запускаем проход для преобразования циклов в gotos и метки.

(Следующие несколько проходов ищут плохие вещи.)

Затем мы запускаем проход для поиска устаревших типов для предупреждений.

Затем мы запускаем проход, который ищет использование анонимных типов, для которых мы еще не выбрали метаданные, и испускаем их.

Затем мы запускаем проход, который ищет плохое использование деревьев выражений. Например, используя оператор ++ в дереве выражений.

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

Затем мы запускаем проход, который ищет незаконные шаблоны внутри блоков итератора.

Затем мы запускаем проверку доступности, предупреждаем о недостижимом коде и рассказываем вам, когда вы сделали что-то вроде забытого возврата в конце непустого метода.

Затем мы запускаем проход, который проверяет, что каждый goto нацелен на разумную метку, и что каждая метка нацелена на доступную точку доступа.

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

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

Затем мы запускаем проход, который обнаруживает отсутствующие аргументы ref для вызовов COM-объектов и исправляет их. (Это новая функция в С# 4.)

Затем мы запускаем проход, который ищет материал формы "new MyDelegate (Foo)" и переписывает его в вызов CreateDelegate.

Затем мы запускаем проход, который преобразует деревья выражений в последовательность вызовов метода factory, необходимых для создания деревьев выражений во время выполнения.

Затем мы запускаем проход, который переписывает всю нулевую арифметику в код, который проверяет HasValue и т.д.

Затем мы запускаем проход, который находит все ссылки на базу base.Blah() и переписывает их в код, который выполняет не виртуальный вызов метода базового класса.

Затем мы запускаем проход, который ищет инициализаторы объектов и коллекций и превращает их в соответствующие наборы свойств и т.д.

Затем мы запускаем проход, который ищет динамические вызовы (на С# 4) и перезаписывает их на динамические сайты, использующие DLR.

Затем мы запускаем проход, который ищет вызовы удаленных методов. (То есть частичные методы без фактической реализации или условные методы, которые не имеют условного символа компиляции.) Они превращены в no-ops.

Затем мы ищем недостижимый код и удаляем его из дерева. Нет смысла в кодировании IL для него.

Затем мы запускаем проход оптимизации, который перезаписывает тривиальные операторы "есть" и "как".

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

Затем мы запускаем проход, который превращает конкатенации строк в вызовы к правильной перегрузке String.Concat.

(Ах, воспоминания. Эти последние два прохода были первыми, с чем я работал, когда я присоединился к команде компилятора.)

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

Затем мы запускаем проход, который оптимизирует арифметику; например, если мы знаем, что M() возвращает int, и мы имеем 1 * M(), тогда мы просто превращаем его в M().

Затем мы генерируем код для анонимных типов, впервые используемых этим методом.

Затем мы преобразуем анонимные функции в этом теле в методы замыкающих классов.

Наконец, мы преобразуем блоки итератора в конечные машины с коммутацией.

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

Просто как пирог!

Ответ 3

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

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

Ответ 5

Вся необходимая информация может быть получена из ссылочных сборок.

Итак, нет файлов заголовков, но компилятору нужен доступ к используемой DLL.

И да, это компилятор с двумя проходами, но это не объясняет, как он получает информацию о типах библиотек.