Множинне спадкування в 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, коли я змінюю один з інтерфейсів.


EDIT

Можливо, було б краще розглянути практичний приклад:

У вас є існуючий клас (наприклад, текстовий клієнт 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 #, подивися.
Йордао

10
@MandeepJanjua Я не претендував на подібне, я сказав, що "це цілком може запровадити". Факт залишається, що стандарт CLR ECMA забезпечує механізм ІЛ для багаторазового успадкування, просто ніщо не використовує його повною мірою.
IanNorton

4
Багатократне успадкування FYI - це не погано і не робить код таким складним. Просто думав, що це згадаю.
Дмитро Нестерук

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> { }

Пост-компілятор можна запустити як події після складання 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.

У будь-якому випадку, у мене є гарне маленьке рішення для множинного спадкування, яким я хотів поділитися, він є; 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не був би еквівалентним). Чи знаєте ви будь-які підходи до збереження ідентичності?
supercat

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

2

У власній реалізації я виявив, що використання класів / інтерфейсів для ІМ, хоча і "хорошої форми", як правило, є масовим ускладненням, оскільки вам потрібно встановити все це множинне успадкування лише для кількох необхідних викликів функцій, і в моєму випадку, це потрібно було зробити буквально в десятки разів надлишково.

Натомість було легше просто зробити статичні "функції, які викликають функції, які викликають функції" в різних модульних різновидах, як своєрідну заміну OOP. Я працював над рішенням - "система заклинань" для RPG, де для ефектів потрібно сильно поєднувати і відповідати функції, що вимагає надати надзвичайну різноманітність заклинань без повторного написання коду, як це вказує приклад.

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

Кодування здається набагато швидшим і чистішим таким чином ІМО. Якщо ви просто виконуєте функції, і вам не потрібні успадковані властивості , використовуйте функції.


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 матиме доступ до всіх методів та властивостей класу Батько та мати, імітуючи множинне успадкування, успадковуючи екземпляр батьківських класів. Не зовсім те саме, але це практично.


2
Я не згоден з першим пунктом. До інтерфейсу ви додаєте лише підписи потрібних методів у ВСІМ класі. Але ви можете додати скільки завгодно додаткових методів до будь-яких класів. Також є інтерфейс вилучення правою кнопкою миші, який спрощує роботу з вилучення інтерфейсів. Нарешті, ваш приклад жодним чином не є спадковим (багаторазовим чи іншим), проте це чудовий приклад композиції. На жаль, було б більше користі, якби ви використовували інтерфейси, щоб також демонструвати DI / IOC, використовуючи конструктор / властивість введення. Поки я не хочу проголосувати, я не вважаю, що це гарна відповідь.
Френсіс Роджерс

1
Озираючись на цю тему через рік, я погоджуюся, що ви можете додати стільки методів, скільки вам потрібно в класі, не додаючи підпис в інтерфейсі, однак це зробить інтерфейс неповним. Крім того, мені не вдалося знайти інтерфейс вилучення правою кнопкою миші в моєму IDE, можливо, мені щось не вистачало. Але моя більша стурбованість полягає в тому, що коли ви вказуєте підпис в інтерфейсі, класи спадкування повинні реалізувати цю підпис. Я думаю, що це подвійна робота і може призвести до дублювання кодів.
Йогі

ЕКСТРАКТНА ІНТЕРФЕЙС: клацніть правою кнопкою миші на підписі класу, потім витягніть інтерфейс ... у VS2015 той самий процес, за винятком необхідного клацання правою кнопкою миші, а потім виберіть Quick Actions and Refactorings...це потрібно знати, це заощадить вам багато часу
Chef_Code

1

Ми всі, здається, рухаємося по цьому шляху інтерфейсу, але очевидно, що інша можливість полягає в тому, щоб зробити те, що повинен зробити OOP, і створити своє дерево спадкування ... (чи не це все, що дизайн класу про?)

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; }
}

Ця структура пропонує блоки багаторазового використання коду і, безумовно, як слід писати код OOP?

Якщо цей конкретний підхід не зовсім відповідає законопроекту, ми просто створюємо нові класи на основі необхідних об'єктів ...

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 ++, це як судити OOP за PHP або автомобілі від Pintos. Ця проблема легко вирішується: в Ейфелі, коли ви успадковуєте клас, вам також потрібно вказати, які методи ви хочете успадкувати, і ви можете перейменувати їх. Ніяких двозначностей там немає і сюрпризів теж немає.
Йорг W Міттаг

2
@mP: ні, Ейфель надає справжнє множинне успадкування. Перейменування не означає втрату ланцюга спадкування, а також не втратить катастрофічність класів.
Авель

0

Якщо X успадковує від Y, це має два дещо ортогональні ефекти:

  1. Y надасть функцію за замовчуванням для X, тому код для X повинен містити лише ті речі, які відрізняються від Y.
  2. Практично в будь-якому місці Y, як очікується, замість цього може бути використаний X.

Хоча успадкування передбачає обидві особливості, не важко уявити обставини, коли будь-яка могла бути корисною без іншої. Жодна мова .net, яку я знаю, не має прямого способу реалізації першого без другого, хоча можна отримати таку функціональність, визначивши базовий клас, який ніколи не використовується безпосередньо, і мати один або кілька класів, які успадковують безпосередньо від нього, не додаючи нічого нові (такі класи могли б ділитися всім своїм кодом, але не підлягали б заміні один одному). Однак будь-яка мова, сумісна з CLR, дозволить використовувати інтерфейси, які надають другу особливість інтерфейсів (замінюваність) без першої (повторне використання членів).


0

я знаю, що я знаю, навіть якщо це не дозволено і так далі, колись тобі справді це потрібно для тих:

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

як у моєму випадку, я хотів зробити цей клас b: Форма (так, windows.forms) клас c: b {}

тому що половина функції були однаковими, і інтерфейс u повинен переписати їх усі


1
У вашому прикладі не зображено багаторазового успадкування, тож яку проблему ви намагаєтеся вирішити? Показує справжній приклад множинного успадкування class a : b, c(впровадження будь-яких необхідних дірок у договорі). Можливо, ваші приклади трохи спрощені?
М.Бабкок

0

Оскільки час від часу виникає питання про багаторазове успадкування (МІ), я хотів би додати підхід, який вирішує деякі проблеми із структурою композиції.

Я дописувати IFirst, ISecond, First, Second, FirstAndSecondпідхід, так як він був представлений в цьому питанні. Я зводжую зразок коду до IFirst, оскільки шаблон залишається однаковим незалежно від кількості інтерфейсів / базових класів MI.

Припустимо, що з MI Firstі Secondобидва будуть походити від одного базового класу BaseClass, використовуючи лише елементи публічного інтерфейсу відBaseClass

Це можна виразити, додавши посилання на контейнер на BaseClassв Firstта 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.