Не виртуальные методы, статическое связывание и интерфейс в C # - PullRequest
2 голосов
/ 27 августа 2011

Я понимаю, что не виртуальные методы статически связаны, что означает, насколько я понимаю, что во время компиляции известно, какой метод будет вызываться для какого объекта. Это решение принимается на основе статического типа объекта (ов). Что меня смущает, так это интерфейсы (а не class ) и статическое связывание.

Рассмотрим этот код,

public interface IA
{
    void f();
}
public class A : IA
{
    public void f() {  Console.WriteLine("A.f()"); }
}
public class B : A 
{
    public new void f() {  Console.WriteLine("B.f()"); }
}

B b = new B();
b.f();  //calls B.f()     //Line 1

IA ia = b as IA;
ia.f(); //calls A.f()     //Line 2

Демонстрационный код: http://ideone.com/JOVmi

Я понимаю Line 1. Компилятор может знать, что b.f() вызовет B.f(), потому что он знает тип static b, который равен B.

Но как компилятор решает во время самой компиляции , что ia.f() будет вызывать A.f()? Что такое статический тип объекта ia? Разве это не IA? Но тогда это интерфейс, и он не имеет определения f(). Тогда как же это работает?

Чтобы сделать случай более загадочным, давайте рассмотрим этот static метод:

static void g(IA ia)
{
   ia.f(); //What will it call? There can be too many classes implementing IA!
}

Как говорится в комментарии, может быть слишком много классов, реализующих интерфейс IA, тогда как компилятор статически может решить, какой метод ia.f() будет вызывать? Я имею в виду, скажем, если у меня есть класс, определенный как:

public class C : A, IA 
{
    public new void f() { Console.WriteLine("C.f()"); }
}

Как видите, C, в отличие от B, реализует IA в дополнение к A. Это означает, что у нас другое поведение здесь:

g(new B()); //inside g(): ia.f() calls A.f() as before!
g(new C()); //inside g(): ia.f() doesn't calls A.f(), rather it calls C.f()

Демонстрационный код: http://ideone.com/awCor

Как бы я понял все эти варианты, особенно как работают интерфейсы и статическое связывание?

И еще несколько ( ideone ):

C c = new C();
c.f(); //calls C.f()

IA ia = c as IA;
ia.f(); //calls C.f()

A a = c as A;
a.f(); //doesn't call C.f() - instead calls A.f()

IA iaa = a as IA;
iaa.f(); //calls C.f() - not A.f()

Пожалуйста, помогите мне понять все это и как статическое связывание выполняется компилятором C #.

Ответы [ 2 ]

6 голосов
/ 27 августа 2011

Но как компилятор решает во время компиляции , что ia.f() будет вызывать A.f()?

Это не так. Он знает, что ia.f() будет вызывать IA.f() для экземпляра объекта, содержащегося в ia. Он генерирует этот код операции вызова и позволяет среде выполнения выяснить это при выполнении вызова.

Вот IL, который будет генерироваться для нижней половины вашего примера кода:

    .locals init (
            class B   V_0,
            class IA  V_1)
    IL_0000:  newobj instance void class B::'.ctor'()
    IL_0005:  stloc.0
    IL_0006:  ldloc.0
    IL_0007:  callvirt instance void class B::f()
    IL_000c:  ldloc.0
    IL_000d:  stloc.1
    IL_000e:  ldloc.1
    IL_000f:  callvirt instance void class IA::f()
    IL_0014:  ret

Обратите внимание, что callvirt используется в обоих случаях. Это используется, потому что среда выполнения может самостоятельно определять, когда целевой метод не виртуален. (Кроме того, callvirt выполняет неявную нулевую проверку аргумента this, а call - нет.)

Этот дамп должен ответить на все ваши другие вопросы. Вкратце: компилятор даже не пытается разрешить последний вызов метода. Это работа для среды выполнения.

1 голос
/ 28 августа 2011

Статическое связывание означает нечто иное, чем вы думаете.Также называется «раннее связывание», оно противоположно позднему связыванию, доступно в C # версии 4 с ключевым словом dynamic и во всех версиях с отражением.Основная характеристика позднего связывания заключается в том, что компилятор не может проверить, что вызываемый метод даже существует, не говоря уже о том, что переданы правильные аргументы.Если что-то не так, вы получите исключение во время выполнения.Это также медленно, потому что среда выполнения должна выполнять дополнительную работу для поиска метода, проверки аргументов и построения фрейма стека вызовов.

Это не проблема, когда вы используете интерфейсы или виртуальные методы, компилятор может проверитьвсе сразу.Полученный код очень эффективен.Это по-прежнему приводит к косвенным вызовам методов (также называемым «динамическая диспетчеризация»), которые необходимы для реализации интерфейсов и виртуальных методов, но все еще используются в C # для не виртуальных методов экземпляра.Документировано в этом сообщении в блоге от бывшего члена команды C #.Сантехника CLR, которая выполняет эту работу, называется «таблицей методов».Примерно аналогично v-таблице в C ++, но таблица методов содержит запись для каждого метода, включая не виртуальные.Ссылка на интерфейс - это просто указатель на эту таблицу.

...