В языках .NET Core высокого уровня (таких как С#) предпринимается попытка насколько возможно скрыть из виду низкоуровневые детали CIL. Один из особенно хорошо скрываемых аспектов — тот факт, что CIL является языком программирования, основанным на использовании стека. Вспомните из исследования пространств имен коллекций (см. главу 10), что класс Stack
Stack для загрузки и выгрузки вычисляемых значений, но применяемый ими образ действий похож на заталкивание и выталкивание.Формально сущность, используемая для хранения набора вычисляемых значений, называется
В мире CIL невозможно напрямую получать доступ к элементам данных, включая локально определенные переменные, входные аргументы методов и данные полей типа. Вместо этого элемент данных должен быть явно загружен в стек и затем извлекаться оттуда для использования в более позднее время (запомните упомянутое требование, поскольку оно содействует пониманию того, почему блок кода CIL может выглядеть несколько избыточным).
На заметку!
Вспомните, что код CIL не выполняется напрямую, а компилируется по требованию. Во время компиляции кода CIL многие избыточные аспекты реализации оптимизируются. Более того, если для текущего проекта включена оптимизация кода (на вкладке Build (Сборка) окна свойств проекта в Visual Studio), то компилятор будет также удалять разнообразные избыточные детали CIL.Чтобы понять, каким образом CIL задействует модель обработки на основе стека, создайте простой метод C# по имени PrintMessage()
void. Внутри его реализации будет просто выводиться значение локальной переменной в стандартный выходной поток:void PrintMessage()
{
string myMessage = "Hello.";
Console.WriteLine(myMessage);
}
Если просмотреть код CIL, который получился в результате трансляции метода PrintMessage()
.locals. Затем локальная строка загружается и сохраняется в этой локальной переменной с применением кодов операций ldstr (загрузить строку) и stloc.0 (сохранить текущее значение в локальной переменной, находящейся в ячейке 0).Далее с помощью кода операции ldloc.0
0) значение (по индексу 0) загружается в память для использования в вызове метода System.Console.WriteLine(), представленном кодом операции call. Наконец, посредством кода операции ret производится возвращение из функции. Ниже показан (прокомментированный) код CIL для метода PrintMessage() (ради краткости из листинга были удалены коды операций nop):.method assembly hidebysig static void PrintMessage() cil managed
{
.maxstack 1
// Определить локальную переменную типа string (по индексу 0).
.locals init ([0] string V_0)
// Загрузить в стек строку со значением "Hello."
ldstr " Hello."
// Сохранить строковое значение из стека в локальной переменной.
stloc.0
// Загрузить значение по индексу 0.
ldloc.0
// Вызвать метод с текущим значением.
call void [System.Console]System.Console::WriteLine(string)
ret
}
На заметку!
Как видите, язык CIL поддерживает синтаксис комментариев в виде двойной косой черты (и вдобавок синтаксис/*...*/). Подобно компилятору C# компилятор CIL игнорирует комментарии в коде.Теперь, когда вы знаете основы директив, атрибутов и кодов операций CIL, давайте приступим к практическому программированию на CIL, начав с рассмотрения темы возвратного проектирования.
Возвратное проектирование