Множественное наследование в C #


212

Так как множественное наследование является плохим (это усложняет исходный код), C # не предоставляет такой шаблон напрямую. Но иногда было бы полезно иметь эту способность.

Например, я могу реализовать отсутствующий шаблон множественного наследования, используя интерфейсы и три таких класса:

public interface IFirst { void FirstMethod(); }
public interface ISecond { void SecondMethod(); }

public class First:IFirst 
{ 
    public void FirstMethod() { Console.WriteLine("First"); } 
}

public class Second:ISecond 
{ 
    public void SecondMethod() { Console.WriteLine("Second"); } 
}

public class FirstAndSecond: IFirst, ISecond
{
    First first = new First();
    Second second = new Second();
    public void FirstMethod() { first.FirstMethod(); }
    public void SecondMethod() { second.SecondMethod(); }
}

Каждый раз, когда я добавляю метод к одному из интерфейсов, мне нужно также изменить класс FirstAndSecond .

Есть ли способ внедрить несколько существующих классов в один новый класс, как это возможно в C ++?

Может быть, есть решение, использующее какую-то генерацию кода?

Или это может выглядеть так (воображаемый синтаксис c #):

public class FirstAndSecond: IFirst from First, ISecond from Second
{ }

Чтобы не было необходимости обновлять класс FirstAndSecond при изменении одного из интерфейсов.


РЕДАКТИРОВАТЬ

Возможно, было бы лучше рассмотреть практический пример:

У вас есть существующий класс (например, текстовый TCP-клиент на основе ITextTcpClient), который вы уже используете в разных местах внутри вашего проекта. Теперь вы чувствуете необходимость создания компонента вашего класса, чтобы он был легко доступен для разработчиков форм Windows.

Насколько я знаю, у вас есть два способа сделать это:

  1. Напишите новый класс, который унаследован от компонентов и реализует интерфейс класса TextTcpClient, используя экземпляр самого класса, как показано в FirstAndSecond.

  2. Напишите новый класс, который наследуется от TextTcpClient и каким-то образом реализует IComponent (на самом деле еще не пробовал).

В обоих случаях вам нужно выполнять работу по методу, а не по классу. Поскольку вы знаете, что нам понадобятся все методы TextTcpClient и Component, было бы самым простым решением просто объединить эти два в один класс.

Чтобы избежать конфликтов, это может быть сделано путем генерации кода, где результат может быть изменен впоследствии, но ввод этого вручную - чистая боль в заднице.


Если это не просто скрытое множественное наследование, как оно менее сложно?
Harpo

Думая о новых методах расширения в 3.5 и о том, как он работает (генерация статических вызовов членов), это может стать одной из следующих эволюций языка .NET.
Ларри

Иногда я удивляюсь, почему люди просто не делают ... класс A: класс B: класс C?
Chibueze Opata

@NazarMerza: ссылка изменилась. Теперь: проблема с множественным наследованием .
Крейг МакКуин,

9
Не позволяйте пропаганде обмануть вас. Ваш пример показывает, что множественное наследование полезно, а интерфейсы - всего лишь обходной путь для его отсутствия
Кемаль Эрдоган,

Ответы:


125

Так как множественное наследование является плохим (это усложняет исходный код), C # не предоставляет такой шаблон напрямую. Но иногда было бы полезно иметь эту способность.

C # и .net CLR не реализовали MI, потому что они еще не пришли к выводу, как они будут взаимодействовать между C #, VB.net и другими языками, а не потому, что «это сделает исходный код более сложным»

MI - это полезная концепция, вопросы без ответов: «Что вы делаете, когда у вас есть несколько общих базовых классов в разных суперклассах?

Perl - единственный язык, с которым я когда-либо работал, где MI работает и работает хорошо. .Net вполне может представить его однажды, но пока нет, CLR уже поддерживает MI, но, как я уже сказал, языковых конструкций для него еще нет.

До тех пор вы застряли с прокси-объектами и несколькими интерфейсами вместо этого :(


39
CLR не поддерживает множественное наследование реализации, только множественное наследование интерфейса (что также поддерживается в C #).
Джордао

4
@ Jordão: для полноты картины: компиляторы могут создавать MI для своих типов в CLR. У него есть свои предостережения, например, он не соответствует CLS. Для получения дополнительной информации см это (2004) статья blogs.msdn.com/b/csharpfaq/archive/2004/03/07/...
dvdvorle

2
@MrHappy: Очень интересная статья. Я на самом деле исследовал некоторый способ составления черт для C #, взгляните.
Jordão

10
@MandepJanjua Я не претендовал ни на одну из таких вещей, я сказал, что «вполне может представить это». Факт остается фактом, что стандарт CLR ECMA предоставляет механизм IL для множественного наследования, просто ничто не использует его полностью.
IanNorton

4
К вашему сведению, множественное наследование неплохо и не усложняет код. Просто подумал, что упомяну это.
Дмитрий Нестерук

214

Попробуйте использовать композицию вместо того, чтобы пытаться симулировать множественное наследование. Вы можете использовать интерфейсы, чтобы определить, какие классы составляют композицию, например: ISteerableподразумевает свойство типа SteeringWheel, IBrakableподразумевает свойство типа BrakePedalи т. Д.

Как только вы это сделаете, вы можете использовать функцию расширенных методов, добавленную в C # 3.0, чтобы еще больше упростить вызов методов для этих подразумеваемых свойств, например:

public interface ISteerable { SteeringWheel wheel { get; set; } }

public interface IBrakable { BrakePedal brake { get; set; } }

public class Vehicle : ISteerable, IBrakable
{
    public SteeringWheel wheel { get; set; }

    public BrakePedal brake { get; set; }

    public Vehicle() { wheel = new SteeringWheel(); brake = new BrakePedal(); }
}

public static class SteeringExtensions
{
    public static void SteerLeft(this ISteerable vehicle)
    {
        vehicle.wheel.SteerLeft();
    }
}

public static class BrakeExtensions
{
    public static void Stop(this IBrakable vehicle)
    {
        vehicle.brake.ApplyUntilStop();
    }
}


public class Main
{
    Vehicle myCar = new Vehicle();

    public void main()
    {
        myCar.SteerLeft();
        myCar.Stop();
    }
}

13
В этом все дело - идея, подобная этой, облегчит композицию.
Джон Скит

9
Да, но есть случаи использования, когда вам действительно нужны методы как часть основного объекта
Дэвид Пьер

9
К сожалению, данные переменных-членов недоступны в методах расширения, поэтому вы должны представлять их как внутренние или (ug) общедоступные, хотя я думаю, что составление по контракту - лучший способ решить множественное наследование.
cfeduke

4
Отличный ответ! Краткая, простая для понимания, очень полезная иллюстрация. Спасибо!
AJ.

3
Возможно, мы захотим проверить myCar, закончил ли рулевое управление перед вызовом Stop. Он может перевернуться, если его Stopприменить, когда myCarскорость слишком высокая. : D
Деврадж Гадхави

16

Я создал посткомпилятор C #, который включает такие вещи:

using NRoles;

public interface IFirst { void FirstMethod(); }
public interface ISecond { void SecondMethod(); }

public class RFirst : IFirst, Role {
  public void FirstMethod() { Console.WriteLine("First"); }
}

public class RSecond : ISecond, Role {
  public void SecondMethod() { Console.WriteLine("Second"); }
}

public class FirstAndSecond : Does<RFirst>, Does<RSecond> { }

Вы можете запустить посткомпилятор как событие post-build Visual Studio:

C: \ some_path \ nroles-v0.1.0-bin \ nutate.exe "$ (TargetPath)"

В той же сборке вы используете это так:

var fas = new FirstAndSecond();
fas.As<RFirst>().FirstMethod();
fas.As<RSecond>().SecondMethod();

В другой сборке вы используете это так:

var fas = new FirstAndSecond();
fas.FirstMethod();
fas.SecondMethod();

6

Вы можете иметь один абстрактный базовый класс, который реализует IFirst и ISecond, а затем наследовать только от этой базы.


Это, вероятно, лучшее решение, но не обязательно лучшая идея: p
leppie

1
Разве вам не придется редактировать абстрактный класс при добавлении методов во взаимодействия?
Рик

Рик: насколько ты ленив, когда тебе нужно сделать это только один раз?
Леппи

3
@leppie - «Каждый раз, когда я добавляю метод к одному из интерфейсов, мне нужно также изменить класс FirstAndSecond». Эта часть исходного вопроса не решается этим решением, не так ли?
Рик

2
Вам придется редактировать абстрактный класс, но вам НЕ нужно редактировать любые другие классы, которые зависят от него. Доллар останавливается там, вместо того, чтобы продолжать каскадировать всю коллекцию классов.
Джоэл Кохорн

3

MI НЕ плох, все, кто (серьезно) использовал его, ЛЮБИТ его, и это НЕ усложняет код! По крайней мере, не больше, чем другие конструкции могут усложнить код. Плохой код - это плохой код независимо от того, присутствует ли на рисунке MI.

Во всяком случае, у меня есть милое небольшое решение для множественного наследования, которым я хотел поделиться, это по адресу; http://ra-ajax.org/lsp-liskov-substitution-principle-to-be-or-not-to-be.blog или вы можете перейти по ссылке в моем подписи ... :)


Возможно ли иметь множественное наследование и при этом повышающие и понижающие значения сохраняют идентичность? Известные мне решения для задач множественного наследования вращаются вокруг наличия приведений, которые не сохраняют идентичность (если myFooэто тип Foo, который наследуется от, Mooи Gooоба из которых наследуются Boo, то (Boo)(Moo)myFooи (Boo)(Goo)myFooне будет эквивалентен). Знаете ли вы о каких-либо подходах для сохранения идентичности?
суперкат

1
Вы можете использовать web.archive.org или аналогичный, чтобы перейти по ссылке, но здесь вы найдете более подробное обсуждение решения, предложенного в оригинальном вопросе.
MikeBeaton

2

В моей собственной реализации я обнаружил, что использование классов / интерфейсов для MI, хотя и «в хорошей форме», имеет тенденцию вызывать чрезмерные сложности, поскольку вам нужно настроить все это множественное наследование только для нескольких необходимых вызовов функций, и в моем случае, нужно было сделать буквально в десятки раз излишне.

Вместо этого было проще сделать статические «функции, которые вызывают функции, которые вызывают функции» в различных модульных разновидностях, как своего рода замена ООП. Решением, над которым я работал, была «система заклинаний» для RPG, где эффекты должны сильно смешивать и сопоставлять вызовы функций, чтобы дать огромное разнообразие заклинаний без переписывания кода, во многом как показано в примере.

Большинство функций теперь могут быть статическими, потому что мне не обязательно нужен экземпляр для логики орфографии, тогда как наследование классов не может даже использовать виртуальные или абстрактные ключевые слова в то время как статические. Интерфейсы не могут использовать их вообще.

Кодирование кажется намного быстрее и чище, таким образом, IMO. Если вы просто выполняете функции и вам не нужны унаследованные свойства , используйте функции.


2

С C # 8 теперь у вас практически есть множественное наследование через реализацию интерфейса по умолчанию:

interface ILogger
{
    void Log(LogLevel level, string message);
    void Log(Exception ex) => Log(LogLevel.Error, ex.ToString()); // New overload
}

class ConsoleLogger : ILogger
{
    public void Log(LogLevel level, string message) { ... }
    // Log(Exception) gets default implementation
}

4
Да, но обратите внимание, что в приведенном выше примере вы не сможете это сделать new ConsoleLogger().Log(someEception)- это просто не будет работать, вам придется явно привести ваш объект к объекту, ILoggerчтобы использовать метод интерфейса по умолчанию. Так что его полезность несколько ограничена.
Дмитрий Нестерук

1

Если вы можете жить с тем ограничением, что методы IFirst и ISecond должны взаимодействовать только с контрактом IFirst и ISecond (как в вашем примере) ... вы можете делать то, что вы просите, с помощью методов расширения. На практике это случается редко.

public interface IFirst {}
public interface ISecond {}

public class FirstAndSecond : IFirst, ISecond
{
}

public static MultipleInheritenceExtensions
{
  public static void First(this IFirst theFirst)
  {
    Console.WriteLine("First");
  }

  public static void Second(this ISecond theSecond)
  {
    Console.WriteLine("Second");
  }
}

///

public void Test()
{
  FirstAndSecond fas = new FirstAndSecond();
  fas.First();
  fas.Second();
}

Таким образом, основная идея состоит в том, что вы определяете требуемую реализацию в интерфейсах ... этот необходимый материал должен поддерживать гибкую реализацию в методах расширения. В любое время вам нужно «добавить методы в интерфейс», вместо этого вы добавляете метод расширения.


1

Да, использование интерфейса - это хлопотно, потому что каждый раз, когда мы добавляем метод в класс, мы должны добавлять сигнатуру в интерфейс. Кроме того, что если у нас уже есть класс с кучей методов, но нет интерфейса для него? мы должны вручную создать интерфейс для всех классов, которые мы хотим наследовать. И хуже всего то, что мы должны реализовать все методы в интерфейсах дочернего класса, если дочерний класс должен наследоваться от множественного интерфейса.

Следуя шаблону проектирования Фасада, мы можем имитировать наследование от нескольких классов, используя методы доступа . Объявите классы как свойства с {get; set;} внутри класса, который должен наследоваться, и все открытые свойства и методы принадлежат этому классу, а в конструкторе дочернего класса создайте экземпляры родительских классов.

Например:

 namespace OOP
 {
     class Program
     {
         static void Main(string[] args)
         {
             Child somechild = new Child();
             somechild.DoHomeWork();
             somechild.CheckingAround();
             Console.ReadLine();
         }
     }

     public class Father 
     {
         public Father() { }
         public void Work()
         {
             Console.WriteLine("working...");
         }
         public void Moonlight()
         {
             Console.WriteLine("moonlighting...");
         }
     }


     public class Mother 
     {
         public Mother() { }
         public void Cook()
         {
             Console.WriteLine("cooking...");
         }
         public void Clean()
         {
             Console.WriteLine("cleaning...");
         }
     }


     public class Child 
     {
         public Father MyFather { get; set; }
         public Mother MyMother { get; set; }

         public Child()
         {
             MyFather = new Father();
             MyMother = new Mother();
         }

         public void GoToSchool()
         {
             Console.WriteLine("go to school...");
         }
         public void DoHomeWork()
         {
             Console.WriteLine("doing homework...");
         }
         public void CheckingAround()
         {
             MyFather.Work();
             MyMother.Cook();
         }
     }


 }

с этой структурой класс Child будет иметь доступ ко всем методам и свойствам Class Father и Mother, имитирующим множественное наследование, наследующим экземпляр родительских классов. Не совсем то же самое, но это практично.


2
Я не согласен с первым абзацем. Вы только добавляете подписи методов, которые вы хотите в КАЖДОМ классе, к интерфейсу. Но вы можете добавить столько дополнительных методов к любым классам, сколько захотите. Кроме того, есть щелчок правой кнопкой мыши, интерфейс извлечения, который делает работу извлечения интерфейсов простой. Наконец, ваш пример никоим образом не является наследованием (множественным или иным образом), но, тем не менее, является отличным примером композиции. Увы, было бы лучше, если бы вы использовали интерфейсы для демонстрации DI / IOC, используя конструктор / внедрение свойства. Хотя я не буду голосовать вниз, я не думаю, что это хороший ответ, извините.
Фрэнсис Роджерс

1
Оглядываясь назад на эту тему год спустя, я согласен, что вы можете добавить в класс столько методов, сколько захотите, без добавления подписи в интерфейсе, однако это сделает интерфейс неполным. Кроме того, я не смог найти интерфейс для извлечения правой кнопкой мыши в своей среде IDE, может быть, я что-то упустил. Но меня больше беспокоит то, что когда вы указываете сигнатуру в интерфейсе, наследующие классы должны реализовывать эту сигнатуру. Я думаю, что это двойная работа и может привести к дублированию кодов.
Йог

ДОБАВИТЬ ИНТЕРФЕЙС: щелкните правой кнопкой мыши подпись класса, затем извлеките интерфейс ... в VS2015 тот же процесс, за исключением того, что вы должны щелкнуть правой кнопкой мыши, а затем выбрать, что Quick Actions and Refactorings...это необходимо знать, это сэкономит вам много времени
Chef_Code

1

Мы все, кажется, идем по пути интерфейса с этим, но очевидная другая возможность здесь - это сделать то, что должен делать ООП, и построить ваше дерево наследования ... (разве это не то, что дизайн класса - это все около?)

class Program
{
    static void Main(string[] args)
    {
        human me = new human();
        me.legs = 2;
        me.lfType = "Human";
        me.name = "Paul";
        Console.WriteLine(me.name);
    }
}

public abstract class lifeform
{
    public string lfType { get; set; }
}

public abstract class mammal : lifeform 
{
    public int legs { get; set; }
}

public class human : mammal
{
    public string name { get; set; }
}

Эта структура обеспечивает многократно используемые блоки кода и, конечно, как должен быть написан код ООП?

Если этот конкретный подход не совсем соответствует требованиям, мы просто создаем новые классы на основе требуемых объектов ...

class Program
{
    static void Main(string[] args)
    {
        fish shark = new fish();
        shark.size = "large";
        shark.lfType = "Fish";
        shark.name = "Jaws";
        Console.WriteLine(shark.name);
        human me = new human();
        me.legs = 2;
        me.lfType = "Human";
        me.name = "Paul";
        Console.WriteLine(me.name);
    }
}

public abstract class lifeform
{
    public string lfType { get; set; }
}

public abstract class mammal : lifeform 
{
    public int legs { get; set; }
}

public class human : mammal
{
    public string name { get; set; }
}

public class aquatic : lifeform
{
    public string size { get; set; }
}

public class fish : aquatic
{
    public string name { get; set; }
}

0

Множественное наследование - это одна из тех вещей, которая обычно вызывает больше проблем, чем решает. В C ++ это соответствует схеме предоставления вам достаточного количества веревки, чтобы повеситься, но Java и C # решили пойти по более безопасному пути, не давая вам выбора. Самая большая проблема состоит в том, что делать, если вы наследуете несколько классов, у которых есть метод с той же сигнатурой, которую наследник не реализует. Какой метод класса он должен выбрать? Или это не должно компилироваться? Как правило, существует другой способ реализации большинства вещей, который не основан на множественном наследовании.


8
Пожалуйста, не судите MI по C ++, это все равно, что судить по ООП по PHP или по автомобилям по Pintos. Эта проблема легко решаема: в Eiffel, когда вы наследуете от класса, вы также должны указать, какие методы вы хотите наследовать, и вы можете переименовать их. Там нет двусмысленностей и никаких сюрпризов.
Йорг Миттаг

2
@mP: нет, Eiffel обеспечивает истинное множественное наследование реализации. Переименование не означает потерю цепочки наследования и не приведет к потере возможности преобразования классов.
Авель

0

Если X наследует от Y, это имеет два несколько ортогональных эффекта:

  1. Y обеспечит функциональность по умолчанию для X, поэтому код для X должен включать только то, что отличается от Y.
  2. Почти везде, где можно было бы ожидать Y, вместо этого можно использовать X.

Хотя наследование обеспечивает обе функции, нетрудно представить обстоятельства, при которых один из них может быть полезен без другого. Ни один из известных мне языков .net не имеет прямого способа реализации первого без второго, хотя такую ​​функциональность можно получить, определив базовый класс, который никогда не используется напрямую, и имея один или несколько классов, которые наследуются напрямую от него, не добавляя ничего new (такие классы могут совместно использовать весь свой код, но не могут заменять друг друга). Однако любой CLR-совместимый язык позволит использовать интерфейсы, которые предоставляют вторую функцию интерфейсов (заменяемость) без первой (повторное использование элемента).


0

я знаю, я знаю, хотя это не разрешено и так далее, иногда вам это действительно нужно для тех:

class a {}
class b : a {}
class c : b {}

как в моем случае я хотел сделать этот класс b: Form (да, windows.forms) класс c: b {}

потому что половина функции была идентична и с интерфейсом вы должны переписать их все


1
В вашем примере не показано множественное наследование, так какую проблему вы пытаетесь решить? Будет показан истинный пример множественного наследования class a : b, c(реализация любых необходимых дыр в контракте). Возможно, ваши примеры просто упрощены?
М.Бэбкок,

0

Поскольку время от времени возникает вопрос множественного наследования (MI), я хотел бы добавить подход, который решает некоторые проблемы с шаблоном композиции.

Я дописывать IFirst, ISecond, First, Second, FirstAndSecondподход, так как он был представлен в этом вопросе. Я сократил пример кода до IFirst, так как шаблон остается неизменным независимо от количества интерфейсов / базовых классов MI.

Предположим, что с MI Firstи Secondоба будут происходить из одного базового класса BaseClass, используя только открытые элементы интерфейса изBaseClass

Это можно выразить, добавив ссылку на контейнер BaseClassв реализацию Firstand Second:

class First : IFirst {
  private BaseClass ContainerInstance;
  First(BaseClass container) { ContainerInstance = container; }
  public void FirstMethod() { Console.WriteLine("First"); ContainerInstance.DoStuff(); } 
}
...

Все становится более сложным, когда на защищенные элементы интерфейса BaseClassссылаются или когда Firstи Secondони будут абстрактными классами в MI, требуя, чтобы их подклассы реализовали некоторые абстрактные части.

class BaseClass {
  protected void DoStuff();
}

abstract class First : IFirst {
  public void FirstMethod() { DoStuff(); DoSubClassStuff(); }
  protected abstract void DoStuff(); // base class reference in MI
  protected abstract void DoSubClassStuff(); // sub class responsibility
}

C # позволяет вложенным классам получать доступ к защищенным / закрытым элементам их содержащих классов, так что это можно использовать для связывания абстрактных битов из Firstреализации.

class FirstAndSecond : BaseClass, IFirst, ISecond {
  // link interface
  private class PartFirst : First {
    private FirstAndSecond ContainerInstance;
    public PartFirst(FirstAndSecond container) {
      ContainerInstance = container;
    }
    // forwarded references to emulate access as it would be with MI
    protected override void DoStuff() { ContainerInstance.DoStuff(); }
    protected override void DoSubClassStuff() { ContainerInstance.DoSubClassStuff(); }
  }
  private IFirst partFirstInstance; // composition object
  public FirstMethod() { partFirstInstance.FirstMethod(); } // forwarded implementation
  public FirstAndSecond() {
    partFirstInstance = new PartFirst(this); // composition in constructor
  }
  // same stuff for Second
  //...
  // implementation of DoSubClassStuff
  private void DoSubClassStuff() { Console.WriteLine("Private method accessed"); }
}

Это довольно сложный пример, но если фактическая реализация FirstMethod и SecondMethod достаточно сложна, а количество доступных закрытых / защищенных методов умеренное, то этот шаблон может помочь преодолеть отсутствие множественного наследования.


0

Это соответствует ответу Лоуренса Уэнама, но в зависимости от вашего варианта использования это может быть или не быть улучшением - вам не нужны сеттеры.

public interface IPerson {
  int GetAge();
  string GetName();
}

public interface IGetPerson {
  IPerson GetPerson();
}

public static class IGetPersonAdditions {
  public static int GetAgeViaPerson(this IGetPerson getPerson) { // I prefer to have the "ViaPerson" in the name in case the object has another Age property.
    IPerson person = getPerson.GetPersion();
    return person.GetAge();
  }
  public static string GetNameViaPerson(this IGetPerson getPerson) {
    return getPerson.GetPerson().GetName();
  }
}

public class Person: IPerson, IGetPerson {
  private int Age {get;set;}
  private string Name {get;set;}
  public IPerson GetPerson() {
    return this;
  }
  public int GetAge() {  return Age; }
  public string GetName() { return Name; }
}

Теперь любой объект, который знает, как получить человека, может реализовать IGetPerson, и он автоматически будет иметь методы GetAgeViaPerson () и GetNameViaPerson (). С этого момента в основном весь код Person идет в IGetPerson, а не в IPerson, кроме новых ivars, которые должны входить в оба. И при использовании такого кода вам не нужно беспокоиться о том, действительно ли ваш объект IGetPerson фактически является IPerson.


0

Теперь это возможно через partialклассы, каждый из которых может наследовать класс самостоятельно, в результате чего конечный объект наследует все базовые классы. Вы можете узнать больше об этом здесь .

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.