Ошибка компиляции здесь:
interfaceB b = a.Get(); // compilation error
^^ return value is cast as interfaceA.
... потому что метод возвращает объект, приведенный как interfaceA
, и пытается привести его как interfaceB
. Из-за наследования все, что реализует interfaceB
, также реализует interfaceA
. Но обратное не верно.
Например, ClassA
реализует interfaceA
, но не реализует interfaceB
. Так что вы не можете сделать это:
interfaceB b = new ClassA();
По той же причине вы получите ту же ошибку компилятора.
Это верно, даже если ваш метод выглядел так:
public interfaceA Get()
{
return new ClassB();
}
Этот метод будет компилироваться, потому что ClassB
реализует interfaceA
.
Но это все равно не скомпилируется:
interfaceB b = a.Get(); // compilation error
... потому что результатом функции является cast as interfaceA
. Другими словами, когда вы вызываете этот метод, единственное, что знает вызывающий объект, - это то, что он получает interfaceA
. Он не знает, что такое конкретный класс. Это может или не может быть то, что реализует interfaceB
.
Ошибка компилятора заставляет вас вычислять все это заранее, вместо того, чтобы запускать программу, а затем получать ошибку. Это потрясающе. Мы хотим этого. Гораздо сложнее выяснить проблему во время работы программы. Намного лучше, если компилятор покажет нам проблему.
В своем вопросе вы упомянули кастинг. Хотя есть случаи для приведения, он часто используется как способ обойти компилятор, заставляя наш код компилироваться, чтобы мы могли его запустить.
Если мы сделаем это, эта строка будет скомпилирована:
interfaceB b = (interfaceB)a.Get();
^^ whatever this method returns, cast it as interfaceB.
// just like this one compiles
interfaceB bb = a.Get() as interfaceB; // Ok
Это замечательно, но когда мы запустим его, мы получим исключение , если возвращенный объект на самом деле не реализует interfaceB
. И это будет исключением, которого мы могли бы избежать, если бы мы позволили компилятору выполнить свою работу и предупредили нас.
Решение
Там не только один. Или, можно сказать, может быть один, но он специфичен для кода, который вы пишете прямо сейчас, а не что-то общее. Но вот несколько советов:
Во-первых, хорошо избегать введения наследования, если оно нам действительно не нужно. Наследование не плохо, но мы часто имеем тенденцию использовать его без необходимости.
Во-вторых, когда мы наследуем или реализуем интерфейсы, мы обычно должны писать код так, чтобы единственным типом, который нас интересует, был объявленный тип. Так, например, если мы вызовем этот метод:
ISomeInterface GiveMeAnObject()
{
return new ThingThatImplementsTheInterface();
}
... единственное, что нас волнует, когда мы получаем этот объект, это то, что он реализует ISomeInterface
. Мы вообще не хотим ничего знать о конкретном классе, других классах, которые реализуют интерфейс, или других интерфейсах, которые наследуются от ISomeInterface
. Наш код не должен заботиться об этом. Основная цель состоит в том, чтобы избежать необходимости знать или заботиться об этих деталях. Если мы знаем или заботимся об этих деталях и обнаруживаем, что говорим: «Я знаю, что мой ISomeInterface
является действительно экземпляром этого класса или этого класса», и мы разыгрываем его, то что-то пошло не так .
Одним из возможных исключений является downcasting . В некоторых случаях это может иметь смысл:
public interface ISomeInterface {}
public interface ISomeInheritedInterface : ISomeInterface {}
public ISomeInheritedInterface GetSomething()
{
return new ClassThatImplementsInheritedInterface();
}
ISomeInterface foo = GetSomething();
В этом случае мы получаем объект и приводим его в качестве одного из базовых типов. Это нормально, потому что не предполагает никаких предположений. Мы знаем, что каждые ISomeInheritedInterface
это всегда и ISomeInterface
, так что все в порядке. Это также, когда компилятор позволяет это.
Кроме этого, большинство наших обычных решений обычных проблем не должны включать в себя кастинг. Когда бы ни было возможно - что почти всегда - нам не нужно этого делать. Если мы хотим отслеживать все типы самостоятельно, а не позволять компилятору делать это, мы можем просто объявить каждую переменную и каждую функцию как object
, а затем привести все. Это было бы кошмаром и усложнило бы программирование. (Или мы могли бы программировать в VB.NET с выключенным Option Strict
, что в значительной степени одно и то же.)