Не виртуальные методы, статическое связывание и интерфейс в C#
Я понимаю, что не виртуальные методы статически связаны, что означает, насколько я понимаю, что во время компиляции известно, какой метод будет вызываться для какого объекта. Это решение принимается на основе статического типа объекта (ов). Меня смущает интерфейс (а не класс) и статическая привязка.
Рассмотрим этот код,
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()
потому что он знает статический тип 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 ответа
Но как компилятор решает во время компиляции, что
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
не.)
Этот дамп должен ответить на все ваши другие вопросы. Вкратце: компилятор даже не пытается разрешить последний вызов метода. Это работа для времени выполнения.
Статическое связывание означает нечто иное, чем вы думаете. Также называется "раннее связывание", оно противоположно позднему связыванию, доступно в C# версии 4 с ключевым словом dynamic и во всех версиях с отражением. Основная характеристика позднего связывания заключается в том, что компилятор не может проверить, что вызываемый метод даже существует, не говоря уже о том, что переданы правильные аргументы. Если что-то не так, вы получите исключение во время выполнения. Это также медленно, потому что среда выполнения должна выполнять дополнительную работу для поиска метода, проверки аргументов и построения фрейма стека вызовов.
Это не проблема, когда вы используете интерфейсы или виртуальные методы, компилятор может проверить все заранее. Полученный код очень эффективен. Это все еще приводит к косвенным вызовам методов (также называемым "динамическая диспетчеризация"), которые необходимы для реализации интерфейсов и виртуальных методов, но все еще используются в C# для не виртуальных методов экземпляра. Документировано в этом блоге от бывшего члена команды C#. Сантехника CLR, которая выполняет эту работу, называется "таблицей методов". Примерно аналогично v-таблице в C++, но таблица методов содержит запись для каждого метода, включая не виртуальные. Ссылка на интерфейс - это просто указатель на эту таблицу.