приведение с наследованием интерфейса

#c# #interface

#c# #интерфейс

Вопрос:

Я пытаюсь использовать интерфейс вместо конкретной реализации, и я сталкиваюсь с проблемой проектирования с приведенным ниже кодом при попытке вызвать Get() метод из InterfaceA.

  • classA реализует interfaceA
  • interfaceB наследует от interfaceA
  • classB наследует classA и реализует interfaceB

Я знаю, как приводить, как и т.д… но приемлемо ли это? Это лучшая практика? Просто интересно, есть ли другие способы обработки таких сценариев. Спасибо

 class Program
{
    static void Main(string[] args)
    {            
        interfaceA a = new classA();
        interfaceB b = a.Get(); // compilation error
        interfaceB bb = a.Get() as interfaceB; // Ok
    }
}

interface interfaceA
{
    interfaceA Get();
}

class classA : interfaceA
{
    interfaceA ia;

    public classA()
    {
        ia = new classA();
    }

    public interfaceA Get()
    {
        return ia;
    }
}

interface interfaceB : interfaceA
{

}

class classB : classA, interfaceB
{

}
 

Комментарии:

1. ctor ClassA является рекурсивным, который также нуждается в исправлении, неясно, для чего предназначена эта логика…

2. Это определенно не лучшая практика. Вы пытаетесь выполнить рискованное понижение, которое в вашем случае не может быть успешным, поскольку созданный экземпляр classA не реализуется interfaceB .

3. Я думаю, вам было бы лучше смоделировать что-то более конкретное, например, фигуры или cat vs dog vs IAnimal, тогда такие вещи, как ваш конструктор ClassA, выглядели бы более явно странно … например, если ClassA был Dog, вы бы пытались построить dog внутри dog внутри dog … навсегда.

Ответ №1:

Проблема, с которой вы столкнулись, заключается в том, что.Get() возвращает InterfaceA и, хотя InterfaceB можно назначить из InterfaceA, InterfaceA НЕЛЬЗЯ назначить из InterfaceB. Таким образом, хотя ClassB наследуется от ClassA, ему все равно необходимо правильно реализовать InterfaceB, который будет иметь несколько других методов / свойств, чем InterfaceA. Вам нужно использовать более абстрактный класс, чтобы назначить более конкретный. например interfaceA a = new classB(); , поскольку ClassB гарантированно обладает всеми свойствами в методах InterfaceA . Я надеюсь, что это имеет смысл.

Ответ №2:

В реальном случае использования я не вызываю конструктор из ClassA. Вместо этого я использую метод, который принимает объект типа InterfaceA, еще не созданный, который создается с правильным типом (ClassB). Реальная проблема, когда я пытаюсь вызвать метод из ClassA, который возвращает InterfaceA для установки объекта InterfaceB.

Интерфейс bb = a.Get();

Код выглядит следующим образом:

 Insert(root, i);

private void Insert(interfaceB<T> node, T newItem)
{
    interfaceB<T> current = node;
    if (root == null)
    {
        interfaceB<T> newNode = new classB<T>(newItem);
        root = newNode;
        return;
    }
    // some code
    current = current.GetLeft(); // error since GetLeft() return a interfaceA<T>
    // some code 
}
 

Надеюсь, это поможет!

Ответ №3:

Ошибка компиляции здесь:

 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
 

…потому что результат функции приводится как 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 . Наш код не должен заботиться об этом. Основная цель — избежать необходимости знать или заботиться об этих деталях. Если мы знаем или заботимся об этих деталях, и мы обнаруживаем, что говорим: «Я знаю, что my ISomeInterface действительно является экземпляром этого класса или того класса», и мы приводим его, тогда что-то пошло не так.

Одним из возможных исключений из этого является понижение. В некоторых случаях это может иметь смысл:

 public interface ISomeInterface {}
public interface ISomeInheritedInterface : ISomeInterface {}

public ISomeInheritedInterface GetSomething()
{
    return new ClassThatImplementsInheritedInterface();
}

ISomeInterface foo = GetSomething();
 

В этом случае мы получаем объект и приводим его как один из его базовых типов. Это нормально, потому что это не требует каких-либо предположений. Мы знаем, что every ISomeInheritedInterface всегда является an ISomeInterface , так что все в порядке. Это также, пока компилятор позволяет это.

Кроме этого, большинство наших обычных решений обычных проблем не должны включать приведение. По возможности — а это происходит почти все время — нам не нужно этого делать. Если мы хотим сами отслеживать все типы вместо того, чтобы позволять компилятору делать это, мы могли бы просто объявить каждую переменную и каждую функцию как object , а затем привести все. Это было бы кошмаром и значительно усложнило бы программирование. (Или мы могли бы запрограммировать в VB.NET с Option Strict выключенным, что в значительной степени одно и то же.)