Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
На заметку! Имейте в виду, что эта глава не планировалась быть всеобъемлющим руководством по синтаксису и семантике CIL.
Директивы, атрибуты и коды операций CIL
Когда вы начинаете изучение низкоуровневых языков, таких как CIL, то гарантированно встретите новые (и часто пугающие) названия для знакомых концепций. Например, к этому моменту приведенный ниже набор элементов вы почти наверняка посчитаете ключевыми словами языка C# (и это правильно):
{new, public, this, base, get, set, explicit, unsafe, enum, operator, partial}
Тем не менее, внимательнее присмотревшись к элементам набора, вы сможете заметить, что хотя каждый из них действительно является ключевым словом С#, он имеет радикально отличающуюся семантику Скажем, ключевое слово enum определяет производный от System.Enum тип, а ключевые слова this и base позволяют ссылаться на текущий объект и его родительский класс. Ключевое слово unsafe применяется для установления блока кода, который не может напрямую отслеживаться средой CLR, а ключевое слово operator дает возможность создать скрытый (специально именованный) метод, который будет вызываться, когда используется специфическая операция C# (такая как знак "плюс").
По разительному контрасту с высокоуровневым языком вроде C# в CIL не просто определен общий набор ключевых слов сам по себе. Напротив, набор лексем, распознаваемых компилятором CIL, подразделяется на следующие три обширные категории, основываясь на их семантике:
• директивы CIL;
• атрибуты CIL;
• коды операций CIL.
Лексемы CIL каждой категории выражаются с применением отдельного синтаксиса и комбинируются для построения допустимой сборки .NET Core.
Роль директив CIL
Прежде всего, существует набор хорошо известных лексем CIL, которые используются для описания общей структуры сборки .NET Core. Такие лексемы называются директивами. Директивы CIL позволяют информировать компилятор CIL о том, каким образом определять пространства имен, типы и члены, которые будут заполнять сборку.
Синтаксически директивы представляются с применением префикса в виде точки (.), например, .namespace, .class, .publickeytoken, .override, .method, .assembly и т.д. Таким образом, если в файле с расширением *.il (общепринятое расширение для файлов кода CIL) указана одна директива .namespace и три директивы .class, то компилятор CIL сгенерирует сборку, в которой определено единственное пространство имен .NET Core, содержащее три класса .NET Core.
Роль атрибутов CIL
Во многих случаях директивы CIL сами по себе недостаточно описательны для того, чтобы полностью выразить определение заданного типа .NET Core или члена типа. С учетом этого факта многие директивы CIL могут сопровождаться разнообразными атрибутами CIL, которые уточняют способ обработки директивы. Например, директива .class может быть снабжена атрибутом public (для установления видимости типа), атрибутом extends (для явного указания базового класса типа) и атрибутом implements (для перечисления набора интерфейсов, поддерживаемых данным типом).
На заметку! Не путайте атрибут .NET Core (см. главу 17) и атрибут CIL, которые являются двумя совершенно разными понятиями.
Роль кодов операций СIL
После того как сборка, пространство имен и набор типов .NET Core определены в терминах языка CIL с использованием различных директив и связанных атрибутов, остается только предоставить логику реализации для типов. Это работа кодов операций. В традициях других низкоуровневых языков программирования многие коды операций CIL обычно имеют непонятный и совершенно нечитабельный вид. Например, для загрузки в память переменной типа string применяется код операции, который вместо дружественного имени наподобие LoadString имеет имя ldstr.
Справедливости ради следует отметить, что некоторые коды операций CIL довольно естественно отображаются на свои аналоги в C# (например, box, unbox, throw и sizeof). Вы увидите, что коды операций CIL всегда используются внутри области реализации члена и в отличие от директив никогда не записываются с префиксом-точкой.
Разница между кодами операций и их мнемоническими эквивалентами в СIL
Как только что объяснялось, для реализации членов отдельно взятого типа применяются коды операций вроде ldstr. Однако такие лексемы, как ldstr, являются мнемоническими эквивалентами CIL фактических двоичных кодов операций CIL. Чтобы выяснить разницу, напишите следующий метод C# в проекте консольного приложения .NET Core по имени FirstSamples:
int Add(int x, int y)
{
return x + y;
}
В терминах CIL действие сложения двух чисел выражается посредством кода операции 0X58. В том же духе вычитание двух чисел выражается с помощью кода операции 0X59, а действие по размещению нового объекта в управляемой куче записывается с использованием кода операции 0X73. С учетом описанной реальности "код CIL" , обрабатываемый JIT-компилятором, представляет собой не более чем порцию двоичных данных.
К счастью, для каждого двоичного кода операции CIL предусмотрен соответствующий мнемонический эквивалент. Например, вместо кода 0X58 может применяться мнемонический эквивалент add, вместо 0X59 — sub, а вместо 0X73 — newobj. С учетом такой разницы между кодами операций и их мнемоническими эквивалентами декомпиляторы CIL, подобные ildasm.exe, транслируют двоичные коды операций сборки в соответствующие им мнемонические эквиваленты CIL. Вот как ildasm.exe представит в CIL предыдущий метод Add(), написанный на языке C# (в зависимости от версии .NET Core вывод может отличаться):
.method assembly hidebysig static int32 Add(int32 x,int32 y) cil managed
{
// Code size 9 (0x9)
.maxstack 2
.locals init ([0] int32 int32 V_0)
IL_0000: /* 00 | */ nop
IL_0001: /* 02 | */ ldarg.0
IL_0002: /* 03 | */ ldarg.1
IL_0003: /* 58 | */ add
IL_0004: /* 0A | */ stloc.0
IL_0005: /* 2B | 00 */ br.s IL_0007
IL_0007: /* 06 | */ ldloc.0
IL_0008: /* 2A | */ ret
} //end of method
Если