.class public MyBaseClass {
.field private string stringField
.field private int32 intField
.method public hidebysig specialname rtspecialname instance void .ctor(string s, int32 i) cil managed {
// Задача: добавить необходимый программный код.
}
}
Обратите внимание на то, что директива .ctor сопровождается атрибутом instance (поскольку это не статический конструктор). Атрибуты cil managed означают, что в контексте этого метода содержится программный код CIL (а не программный код, не являющийся управляемым), который может использоваться в межплатформенных запросах.
Определение свойств
Свойства и методы также имеют специальные представления в CIL. Чтобы в нашем примере обеспечить в MyBaseClass поддержку открытого свойства TheString, можно использовать следующий CIL-код (заметьте, что здесь опять используется атрибут specialname).
.class public MyBaseClass {
…
.method public hidebysig specialname instance string get_TheString() cil managed {
// Задача: добавить необходимый программный код…
}
.method public hidebysig specialname instance void set_TheString(string 'value') cil managed {
// Задача: добавить необходимый программный ход.…
}
.property instance string TheString() {
.get instance string MyNamespace.MyBaseClass::get_TheString()
.set instance void MyNamespace.MyBaseClass::set_TheString(string)
}
}
Напомним, что в терминах CIL свойства будут представлены парой методов, имеющих префиксы get_ и set_. Директива .property использует соответствующие директивы .get и .set, чтобы связать синтаксис свойства со "специально именованными" методами.
Замечание. Указанные выше определения свойств компилироваться не будут, поскольку пока что не реализована сама логика чтения и модификации данных.
Определение параметров членов
Теперь предположим, что нужно определить методы, имеющие аргументы. По сути, указание аргументов в CIL (приблизительно) соответствует аналогичной операции в C#. Например, аргумент определяется с помощью указания типа данных после имени соответствующего параметра. К тому же, как и в C#, в CIL обеспечиваются возможности ввода, вывода и передачи параметров по ссылке. Также в CIL позволяется определять аргумент массива параметров (в C# это делается с помощью ключевого слова params) и необязательные параметры (которые в C# не поддерживаются, но допускаются в VB .NET).
Чтобы показать пример определения параметров непосредственно в CIL, предположим, что нам нужно построить метод, который получает int32 (по значению), int32 (по ссылке), [mscorlib] System.Collections.ArrayList и имеет единственный выходной параметр (типа int32). В терминах C# этот метод должен выглядеть приблизительно так.
public static void MyMethod(int inputInt, ref int refInt, ArrayList ar, out int outputInt) {
outputInt = 0; // Просто чтобы удовлетворить компилятор C#…
}
Если спроецировать этот метод в CIL-код, вы обнаружите, что ссылки на параметры C# будут обозначены знаком амперсанда (&), добавленного в виде суффикса к типу данных, соответствующему параметру (int32&). Для выходных параметров тоже используется суффикс &, но, кроме того, они обозначены маркером CIL [out], Также обратите внимание на то, что в том случае, когда параметр является ссылочным типом (как тип [mscorlib]System.Collections.ArrayList в нашем примере), ему предшествует лексема class (не путайте с директивой .class!).
.method public hidebysig static void MyMethod(int32 inputInt, int32& refInt, class [mscorlib]System.Collections.ArrayList ar, [out] int32& outputInt) cil managed {
…
}
Анализ кодов операций CIL
Заключительной темой нашего обсуждения в этой главе в отношении программного кода CIL будет роль кодов операций. Напомним, что код операции – это просто лексема CIL, используемая для построения логики реализации данного члена. Полный набор кодов операций CIL (который сам по себе довольно велик) можно разбить на следующие большие категории.
• Коды операций для управления программой
• Коды операций для оценки выражений
• Коды операций для осуществления доступа к значениям в памяти (через параметры, локальные переменный и т.п.)
Чтобы продемонстрировать некоторые возможности реализации членов средствами CIL, в табл. 15.5 предлагаются описания некоторых из наиболее часто используемых кодов операций, непосредственно связанных с логикой реализации членов. Кроме того, коды операций в данной таблице сгруппированы по функциональности.
Таблица 15.5. Коды операций CIL, связанные с реализацией членов
Коды операций Описание add, sub, mul, div, rem Позволяют выполнять сложение, вычитание, умножение и деление для пар значений (rem возвращает остаток от деления) and, or, not, xor Позволяют выполнять соответствующие бинарные операции для пар значений ceq, cgt, clt Позволяют сравнивать пару значений из стека различными способами, например: ceq: сравнение в отношении равенства cgt: сравнение в отношении "больше" clt: сравнение в отношении "меньше" box, unbox Используются для конвертирования ссылочных типов и типов, характеризуемых значениями ret Используется для выхода из метода и (если это необходимо) возвращения значения вызывающей стороне beq, bgt, ble, blt, switch Используются (в дополнение к множеству других родственных кодов операций) для управления логикой ветвления в методах, например: beq: переход к заданной метке, если выполняется равенство bgt: переход к заданной метке, если больше ble: переход к заданной метке, если меньше или равно blt: переход к заданной метке, если меньше Все коды операций, связанные с ветвлением, требуют указания метки CIL-кода, по которой должен осуществляться переход в том случае, когда соответствующее сравнение возвращает true call Используется для вызова члена указанного типа newarr, newobj Позволяет разместить в памяти новый массив или новый объект (cоответственно)
Следующая большая категория кодов операций CIL (подмножество которой показано в табл. 15.6) используется для загрузки аргументов в виртуальный стек выполнения. Обратите внимание на то, что эти относящиеся к загрузке коды операций имеют префикс ld (load – загрузка).
Таблица 15.6. Коды операций CIL для помещения данных в стек
Код операции Описание ldarg (с множеством вариаций) Помещает в стек аргумент метода. Вдобавок к общей операции ldarg (для которой требуется указать индекс, идентифицирующий аргумент), есть множество ее вариаций. Например, ldarg с числовым суффиксом (ldarg_0) используется для загрузки соответствующего аргумента. Другие вариации ldarg позволяют с помощью кодов констант CIL из табл. 15.4 указать конкретный тип загружаемых данных (например, ldarg_I4 для int32), а также тип данных и значение (ldarg_I4_5 для загрузки int32 со значением 5) ldc (с множеством вариаций) Помещает в стек значение константы ldfld (с множеством вариаций) Помещает в стек значение поля уровня экземпляра ldloc (с множеством вариаций) Помещает в стек значение локальной переменной ldobj Читает все значения объекта, размещенного в динамической памяти, и помещает их в стек ldstr Помещает в стек строковое значение
Вдобавок к множеству специальных кодов операций загрузки, CIL предлагает набор кодов операций, которые непосредственно "выталкивают" из стека самое верхнее значение. Как продемонстрировали первые несколько примеров этой главы, удаление значения из стека обычно выполняется с целью последующего сохранения этого значения в локальной памяти для дальнейшего использования (например, в качестве параметра при последующем вызове метода). С учетом этого становится ясно, почему многие коды операций, связанные с удалением текущего значения из виртуального стека выполнения, имеют префикс st (store – сохранять). Соответствующие описания приведены в табл. 15.7.