Color Color
Внезапный опрос: что делает следующий код при компиляции и исполнении?
class C
{
public static void M(string x)
{
System.Console.WriteLine("static M(string)");
}
public void M(object s)
{
System.Console.WriteLine("M(object)");
}
}
class Program
{
static void Main()
{
C c = new C();
c.M("hello");
}
}
- Выводит «static M(string)»
- Выводит «M(object)»
- Э, чувак, это даже не скомпилируется, уж тем более не запустится
- Что-то еще
Подумайте над этим немножко, а потом проверьте и посмотрите, правы ли вы.
.
.
.
.
.
.
.
В варианте (1), компилятор мог бы решить, что лучший выбор – статический метод, и позволить вам вызвать его через экземпляр, игнорируя значение экземпляра. В варианте (2), компилятор мог бы решить что экземплярная версия – лучший выбор, и проигнорировать статическую, несмотря на лучшее соответствие аргумента. Но ни один из этих вариантов в действительности не происходит; правила C# гласят, что компилятор должен выбирать лучший метод, основываясь на аргументах, а потом запрещать вызов статического через экземпляр! Так что фактический результат здесь (3):
error CS0176: Member 'C.M(string)' cannot be accessed with an instance reference; qualify it with a type name instead
Что это за безумие? Если вы считаете, что правило «к статическим методам ни в коем случае нельзя обращаться через экземпляры» - хорошее правило, - то почему алгоритм разрешения перегрузок не удаляет статические методы из списка кандидатов при вызове через экземпляр? Почему он вообще позволяет «string»-версии быть применимым кандидатом, если компилятор знает, что это никак не может сработать?
Я согласен, что на первый взгляд это выглядит действительно неуклюже.
Для объяснения того, что это не так глупо, как кажется, рассмотрите следующую вариацию задачи. Класс C остаётся таким же.
class B
{
public C C = new C();
public void N()
{
C.M("hello");
}
}
Что даёт вызов N на экземпляре B?
- Вывод «static M(string)»
- Вывод «M(object)»
- Ошибку компиляции
- Что-то ещё
.
.
.
.
.
.
Теперь немножко посложнее, не так ли? Означает ли C.M «вызови экземплярный метод M на объекте, который хранится в this.C», или же это означает «вызови статический метод M класса C»? Оба подходят!
Из-за нашего неуклюжего правила мы в этом случае делаем то, что нужно. Сначала мы выбираем метод, основываясь только на аргументах, и выясняем, что статический является лучшим выбором. Затем мы проверяем, можно ли интерпретировать «получателя» как тип. Можно. Так что мы вызываем статический метод и выводим «static M(string)». Если бы лучшим выбором оказалась экземплярная версия, то мы бы успешно вызвали экземплярный метод у значения свойства.
Так что причина, по которой компилятор не выбрасывает статические методы при вызове через экземпляр, - в том, что компилятор не обязательно знает, что вызов идёт через экземпляр. Поскольку есть ситуации неоднозначности между вызовом через экземпляр и через тип, мы откладываем угадывание ваших намерений до момента, когда лучший метод уже выбран.
Теперь рассмотрим аргумент, что в нашем первом случае получатель не может быть типом. Мы могли бы усложнить семантику языка, введя три правила разрешения перегрузок: одно – для случая, когда известно, что получатель – тип, одно – для получателя, который точно не тип, и еще одно – для случая, когда получатель может как быть, так и не быть типом. Но вместо дальнейшего усложнения языка, мы сделали прагматичный выбор просто откладывать проверку статичности до окончания проверки наилучшести. Это не идеальное решение, но оно достаточно хорошо для большинства случаев и решает типичную проблему.
Типичная проблема – ситуация, когда у вас есть свойство с именем, совпадающим с его типом, и вы пытаетесь вызвать либо статический метод типа, либо экземплярный метод у свойства. (Это также может происходить с переменными и полями, но обычно имена переменных и полей отличаются регистром от имён их типов.) Канонический мотивирующий пример – это публичное свойство Color типа Color, так что мы называем это «проблемой Color Color».
В реальных ситуациях статические и экземплярные методы обычно имеют разные имена, так что в типичном сценарии вы никогда не сталкиваетесь с вызовом экземплярного метода, ожидая вызвать статический, или наоборот. Представленный мной сегодня случай коллизии имен между статическим и экземплярным методом относительно редок.
Если вам интересно почитать точные правила того, как компилятор решает проблему Color Color, посмотрите в секцию 7.5.4.1 спецификации C# 3.0.