понедельник, 25 апреля 2011 г.

call vs. callvirt (часть 2)

Особенности инструкций call и callvirt, рассмотренные в предыдущей части, как правило, неинтересны разработчику на C#. Это детали реализации, про которые знать необязательно. Однако, в соответствии с законом Дырявых Абстракций, эти детали иногда всё-таки вылезают наружу.

В блоге Эрика Липперта я нашёл два примера, в которых рассматриваются интересные особенности вызова виртуального метода базового класса с помощью ключевого слова «base».

Пример 1
public class Base
{
    public virtual void Foo()
    {
        Console.WriteLine("Alpha");
    }
}

public class Derived : Base {}

public class Grandchild : Derived
{
    public override void Foo()
    {
        base.Foo();
        Console.WriteLine("Charlie");
    }
}
Каждый класс из примера находится в отдельной сборке. При вызове метода Grandchild::Foo() на экран выведется:
Alpha
Charlie
Теперь измените класс Derived, чтобы он стал таким:
public class Derived : Base
{
    public override void Foo()
    {
        base.Foo();
        Console.WriteLine("Bravo");
    }
}
Перекомпилируйте только сборку с классом Derived и снова вызовите Grandchild::Foo(). Несмотря на то что новая версия класса Derived теперь переопределяет метод Foo, результат будет тем же самым:
Alpha
Charlie
Так происходит потому что на момент компиляции класса Grandchild класс Derived не переопределял метод Foo. Компилятор для вызова реализации метода базового класса сгенерировал инструкцию call, которая вызывает метод Foo из класса Alpha. И появление перегрузки в классе Derived никак это не меняет.

Пример 2
public class Base
{
    public virtual void Foo() { }
}

public class Derived : Base
{
    public override void Foo()
    {
        var i = 1;

        Action f = () =>
        {
            Console.WriteLine(i);
            base.Foo();
        };
    }
}
При компиляции этого кода компилятор C# 3 выдаёт предупреждение: «Access to member through a 'base' keyword from an anonymous method, lambda expression, query expression, or iterator results in unverifiable code. Consider moving the access into a helper method on the containing type». При проверке получившейся сборки утилитой peverify будет сообщение об ошибке: «Error: The 'this' parameter to the call must be the calling method's 'this' parameter».

Проблема в том, как компилятор C# выполняет преобразование анонимного метода. В данном случае компилятор создаст вложенный в Derived класс, у которого будет ссылка экземпляр класса Derived. Именно эта ссылка будет использоваться для невиртуального вызова виртуального метода базового класса. Тот факт что реализация виртуального метода базового класса вызывается не из наследника и приводит к ошибке.
Теперь давайте изменим код так:
public class Base
{
    public virtual void Foo() { }
}

public class Derived : Base
{
    public override void Foo()
    {
        Action f = () =>
        {
            Console.WriteLine(1);
            base.Foo();
        };
    }
}
Компилятор по-прежнему выдаёт предупреждение, но peverify ругаться перестал. Что изменилось? Анонимный метод перестал замыкаться на локальную переменную i. Отсутствие замыкания позволяет компилятору C# не создавать вложенный класс, а обойтись созданием метода в классе Derived. Теперь метод базового класса вызывается из наследника и код проходит верификацию.

Простейший способ обойти проблему указан в предупреждении компилятора — нужно перенести вызов метода базового класса во вспомогательный метод:
public class Derived : Base
{
    public override void Foo()
    {
        var i = 1;

        Action f = () =>
        {
            Console.WriteLine(i);
            Helper();
        };
    }

    private void Helper()
    {
        base.Foo();
    }
}
Преобразование очень простое и компилятор C# 4 делает его сам.

2 комментария:

  1. Casino of the Wild - Harrah's Ak-Chin Casino Resort
    Casino 사천 출장안마 of 전주 출장샵 the 동해 출장샵 Wild is located in Maricopa, Arizona. It is a casino and 부천 출장안마 hotel located in Maricopa, Arizona, and is open daily 서산 출장안마 24 hours.

    ОтветитьУдалить
  2. Из всех примеров ни в одном после объявления Action f нет процедуры вызова !!! по этому в IL коде нет ни call ни callvit

    ОтветитьУдалить