Введение в MSIL - Часть 4: Определение членов типа — Архив WASM.RU

Все статьи

Введение в MSIL - Часть 4: Определение членов типа — Архив WASM.RU

В 3-ей части данного введения я рассказал о синтаксисе определения типов. Используя директиву .class, вы можете задать ссылочные типы и типы значений. Правильно выбирая атрибуты типа, вы можете получить полный контроль над его определением.

.class abstract Kerr.Sample.Object { }

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

Конструкторы

Давайте начнём с инициализаторов, известных в таких языках как C++ и C# как конструкторы. CLI поддерживает как инициализаторы типа, так и инициализаторы экземпляра. Инициализаторы типа очень удобная вещь, помогающая справиться со многими мультитредовыми проблемами, с которыми вы можете столкнуться, реализуя синглетон в "родном" (native) C++. Инициализатор типа выполняется только один раз для данного типа и среда выполнения гарантирует, что ни один метод не будет доступен, пока конструктор не завершит работу.

.method static void .cctor()
{
    .maxstack 1
    
    ldstr ".cctor"
    call void [mscorlib]System.Console::WriteLine(string)

    ret
}

.method public void .ctor()
{
    .maxstack 1
    
    ldarg.0
    call instance void [mscorlib]System.Object::.ctor()
    
    ldstr ".ctor"
    call void [mscorlib]System.Console::WriteLine(string)
    
    ret
}

.cctor и .ctor являются специальными именами метода. Инициализатор типа, который является необязательным, задаётся в качестве статического метода .cctor без возвращаемого значения. В C++/CLI и C# инициализаторы типа называются статическими конструкторами.

Инициализатор экземпляра обычно называют просто конструктором. Он задаётся как метод .ctor также без возвращаемого значения. Конструктор вызывается, когда экземпляр типа создаётся с помощью инструкции newobj. Вот пример:

.locals (class TypeName obj)
newobj void TypeName::.ctor()
stloc obj

Вышеприведённые конструкторы, будучи выполненными, выведут на экран следующие строки:

.cctor .ctor

Инструкция newobj создаёт экземпляр типа и инициализирует все его поля, присваивая им значения, эквивалентные нулю. Затем она вызвает соответствующий конструктор, передавая ему первым аргументом ссылку на созданный экземпляр. Как только конструктор завершает работу, ссылка на объект помещается в стек.

Методы

Хотя CLI задаёт конструкторы и свойства как методы (с дополнительными метаданными для свойств), в этой секции мы будем рассматривать методы, как они понимаются в C++ или C#. Конечно, большая часть того, что связанно с методами, также применима к конструкторам и функциям установки/считывания свойств. Большая часть того, что вы можете делать с метода задаётся атрибутами метода. Давайте рассмотрим некоторые из них.

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

.method static void StaticMethod() { /* impl */ }

Методы экземляра задаются через атрибут instance вместо static. Ассемблер IL присваивает этот атрибут по умолчанию, поэтому вам редко понадобится указывать его явно в определении метода.

.method void InstanceMethod() { /* impl */ }

Обратно верно и для вызова методов. Инструкция call по умолчанию предполагает, что метод статический, если не указано обратное. Вот пример для вызова обоих вышеуказанных методов:

call void TypeName::StaticMethod()

ldloc obj
call instance void TypeName::InstanceMethod()

Не забывайте помещать ссылку на экземпляр в стек перед вызовом его метода.

Вызовы виртуальных методов являются важной часть объектно-ориентированного дизайна, и CLI предоставляет значительную гибкость в том, статический или динамический тип объекта будет использован для обслуживания вызова, так же как и в том, каким образом данного поведение может быть переопределно в классах-наследниках (говоря о статических и динамических типах я имею в виду то, как эти понятия понимаются в C++: статические - известные во время компиляции, а динамические - известные во время выполнения). Это обычно называется полиморфизмом. Программируя на MSIL, вам нужно держать в уме два аспекта, связанных с виртуальными функциями. Первый - это как объявлять метод экземпляра, чтобы он поддерживал виртуальный вызов, а второй - как именно вызывать метод. Очевидно, что статические методы по определению не могут быть виртуальными.

Метод становится виртуальным с помощью добавления атрибута virtual.

.class House
{
    .method public virtual void Buy()
    {
        .maxstack 1
        
        ldstr "House::Buy"
        call void [mscorlib]System.Console::WriteLine(string)
        
        ret
    }

    /* etc */
}

.class TownHouse
    extends House
{
    .method public virtual void Buy()
    {
        .maxstack 1
        
        ldstr "TownHouse::Buy"
        call void [mscorlib]System.Console::WriteLine(string)
        
        ret
    }

    /* etc */
}

У типа House есть виртуальный метод Buy. Тип TownHouse расширяет House, и у него также есть виртуальный метод с таким же именем, поэтому говорят, что TownHouse::Buy переопределяет House::Buy. Как же мы указываем среде выполнения, какой метод выбрать. Очевидно, что если у меня экземпляр House, то я хочу вызвать House::Buy, однако если у меня экземпляр TownHouse, то мне нужно, чтобы это решение принималось во время выполнения, основываясь на типа экземпляра.

Пока что я использовал инструкцию call, которая всегда вызывает указанный метод, независимо от динамического типа. Инструкция callvirt, напротив, позволяет среде выполнения определить, какую именно реализацию виртуального метода вызывать, основываясь на типе экземпляра. Посмотрите следующий пример:

newobj instance void House::.ctor()
stloc house
newobj instance void TownHouse::.ctor()
stloc townHouse

ldloc house
call instance void House::Buy()

ldloc townHouse
call instance void TownHouse::Buy()

ldloc townHouse
call instance void House::Buy()

ldloc townHouse
callvirt instance void House::Buy()

Будучи выполненным, он выдаст на консоль следующее:

House::Buy
TownHouse::Buy
House::Buy
TownHouse::Buy

Первое обращение к методу Buy с сслыкой на house вызывает реализацию House::Buy, так как инструкция call интересуется только статическим типом. Второе обращение к Buy с ссылкой на townhouse вызовет реализацию TownHouse::Buy по той же причине. Третье обращение снова вызывает House::Buy несмотря на то, что объект, на который даётся ссылка принадлежить к типу TownHouse. Должно быть понятно, что использование инструкции call подразамевает выбор того, какой метод вызывать, только во время компиляции. В последнем случае используется инструкция callvirt для вызова виртуального метода House::Buy, и так как ссылка указывает на TownHouse, то используется TownHouse::Buy.

Если вы хотите объявить виртуальный метод, но не хотите переопределять унаследованный виртуальный метод с таким же именем, вы можете использовать атрибут newslot при объявлении нового виртуального метода в субклассе. Если вы учтёте, что вызов виртуальных методов средой выполнения не привязан к имена, тогда вы должны понять, как это возможно. Просто думайте о newslot как о добавлении нового указателя на виртуальную функцию в vtbl данного типа.

Виртуальные методы CLI очень интересны, особенно, когда видишь, в какой степени они используются C++/CLI и C#.

2002-2013 (c) wasm.ru