Чому я повинен уникати багатократного успадкування в C ++?


167

Це хороша концепція використання багаторазового успадкування чи я можу робити інші речі замість цього?

Відповіді:


259

Багаторазове успадкування (скорочено ІМ) пахне , а це означає, що, як правило , це робилося з поганих причин, і воно буде дути назад в обличчя обслуговуючого персоналу.

Підсумок

  1. Розглянемо склад ознак, а не спадкування
  2. Будьте обережні до Діаманту Страхи
  3. Розглянемо спадкування декількох інтерфейсів замість об'єктів
  4. Іноді множинне спадкування - це правильна річ. Якщо є, то використовуйте його.
  5. Будьте готові захищати свою багатонаспадкову архітектуру в оглядах коду

1. Можливо, композиція?

Це справедливо для успадкування, і так, це ще більше справедливо для багаторазового успадкування.

Чи справді ваш об’єкт потребує успадкування від іншого? A Carне потрібно успадковувати від Engineроботи до роботи, ні від a Wheel. A Carмає Engineі чотири Wheel.

Якщо ви використовуєте багатократну спадщину для вирішення цих проблем замість складу, то ви зробили щось не так.

2. Діамант Смерть

Зазвичай у вас є клас A, то Bі Cобидва успадковуєте A. І (не питайте мене чому) хтось тоді вирішує, що Dповинен успадкувати і від, Bі від C.

З такою проблемою я стикався двічі за 8 восьми років, і це цікаво бачити через:

  1. Скільки помилок було з самого початку (в обох випадках Dне слід було успадковувати обох Bі C), бо це була погана архітектура (насправді вона Cвзагалі не повинна була існувати ...)
  2. Скільки платників за це платили, тому що в C ++ батьківський клас Aдвічі був присутній у своєму класі онуків D, і, таким чином, оновлення одного батьківського поля A::fieldозначало або оновлення його двічі (через B::fieldі через C::field), або щось мовчазним помилкою і збоєм, пізніше (новий вказівник B::fieldі видалення C::field...)

Використання ключового слова virtual у C ++ для визначення спадкування дозволяє уникнути подвійного макета, описаного вище, якщо це не те, що ви хочете, але все одно, на мій досвід, ви, ймовірно, робите щось не так ...

В ієрархії об’єктів слід намагатися зберегти ієрархію як Дерево (у вузла ОДИН батьків), а не як графік.

Більше про Diamond (редагувати 2017-05-03)

Справжня проблема з Diamond of Dread в C ++ ( якщо припустити, що дизайн є здоровим - перегляньте ваш код! ), Це те, що вам потрібно зробити вибір :

  • Чи бажано, Aщоб у вашому макеті двічі існував клас , і що це означає? Якщо так, то, безумовно, успадковуйте від нього двічі.
  • якщо вона повинна існувати лише один раз, то наслідуйте від неї практично.

Цей вибір притаманний проблемі, і в C ++, на відміну від інших мов, ви можете це зробити, без догми, що примушує ваш дизайн на мовному рівні.

Але, як і всі повноваження, з цим повноваженням покладається відповідальність: перегляньте свій дизайн.

3. Інтерфейси

Багатократне успадкування нуля або одного конкретного класу та нульових або більше інтерфейсів зазвичай нормально, тому що ви не зустрінете описаного вище Diamond of Dread. Насправді, так відбувається все на Java.

Зазвичай, що ви маєте на увазі, коли C успадковує, Aі Bце те, що користувачі можуть використовувати, Cяк якщо б це було A, та / або як ніби це було B.

В C ++ інтерфейс - це абстрактний клас, який має:

  1. весь його метод оголошений чистим віртуальним (суфіксом = 0) (видалено 2017-05-03)
  2. немає змінних членів

Багатократне успадкування нуля одному реальному об'єкту та нульових або більше інтерфейсів не вважається "смердючим" (принаймні, не так багато).

Докладніше про абстрактний інтерфейс C ++ (редагувати 2017-05-03)

По-перше, модель NVI може бути використана для створення інтерфейсу, оскільки реальними критеріями є відсутність стану (тобто немає змінних членів, крім this). Суть вашого абстрактного інтерфейсу полягає в тому, щоб опублікувати договір ("ви можете мені зателефонувати так і в такий спосіб"), нічого більше, нічого менше. Обмеження наявності лише абстрактного віртуального методу повинно бути вибором дизайну, а не обов'язком.

По-друге, в C ++ є сенс успадковувати практично від абстрактних інтерфейсів (навіть з додатковою вартістю / непрямістю). Якщо ви цього не зробите, а спадкування інтерфейсу відображається багато разів у вашій ієрархії, то у вас з'являться двозначності.

По-третє, орієнтація на об'єкт чудова, але це не Єдина Істина там TM в C ++. Скористайтеся правильними інструментами, і завжди пам’ятайте, що у вас є інші парадигми в C ++, які пропонують різні види рішень.

4. Вам справді потрібна множинна спадщина?

Іноді так.

Зазвичай ваш Cклас успадковує від Aі B, і AтаB є два незв'язаних об'єктів (тобто не в тій же ієрархії, нічого спільного, різних концепцій і т.д.).

Наприклад, у вас може бути система Nodes з координатами X, Y, Z, здатна робити безліч геометричних обчислень (можливо, точка, частина геометричних об'єктів), і кожен Вузол є Автоматизованим агентом, здатним спілкуватися з іншими агентами.

Можливо, у вас вже є доступ до двох бібліотек, кожна з яких має власну область імен (ще одна причина для використання просторів імен ... Але ви використовуєте простори імен, чи не так?), Одна істота, geoа інша істотаai

Отже, у вас є власний результат own::Nodeі від, ai::Agentі від geo::Point.

Це момент, коли ви повинні запитати себе, чи не слід використовувати натомість композицію. Якщо own::Nodeце дійсно і a, ai::Agentі a geo::Point, композиція не обійдеться.

Тоді вам буде потрібно багаторазове успадкування, own::Nodeспілкуючись з іншими агентами відповідно до їх положення в тривимірному просторі.

(Ви зауважите це ai::Agentі geo::Pointє повністю, повністю, повністю НЕВІДКЛЮЧЕНО ... Це різко зменшує небезпеку багаторазового успадкування)

Інші справи (редагувати 2017-05-03)

Є й інші випадки:

  • використання (сподіваємось приватного) успадкування як деталі реалізації
  • деякі ідіоми C ++, як-от політики, можуть використовувати множинне успадкування (коли кожна частина повинна спілкуватися з іншими через this)
  • віртуальне успадкування від std :: виключення ( чи потрібна віртуальна спадщина для виключень? )
  • тощо.

Іноді можна використовувати склад, а іноді ІМ краще. Справа в тому, що у вас є вибір. Робіть це відповідально (і перегляньте свій код).

5. Отже, чи варто робити множинне спадкування?

Більшість часу, на мій досвід, ні. ІМ не є правильним інструментом, навіть якщо він, здається, працює, тому що він може використовуватися ледачим складанням функцій разом, не усвідомлюючи наслідків (як, наприклад, створення Carі " Engineі" Wheel).

Але іноді так. І в той час нічого краще не вийде, ніж ІМ.

Але оскільки MI смердючий, будьте готові захищати свою архітектуру в оглядах коду (а захищати її - це добре, адже якщо ви не в змозі її захистити, то не варто цього робити).


4
Я заперечую, що це ніколи не потрібно і настільки рідко корисно, що навіть якщо це ідеально підходить, заощадження над делегуванням не компенсують плутанину програмістам, які не звикли ним користуватися, але в іншому випадку хорошим підсумком.
Білл К

2
Додаю до пункту 5, що код, який використовує MI, повинен мати коментар прямо поруч із ним, пояснюючи причину, тому вам не доведеться пояснювати це усно в огляді коду. Без цього тоді хтось, хто не є вашим рецензентом, може поставити під сумнів ваше хороше судження, коли вони побачать ваш код, і ви, можливо, не зможете його захистити.
Тім Абелл

" тому що ви не зустрінете описаного вище Діаманту Страшного. Чому б і ні?
curiousguy

1
Я використовую ІМ для моделі спостерігача.
Кальмарій

13
@BillK: Звичайно, це не потрібно. Якщо у вас повна мова перетворення без MI, ви можете зробити все, що може зробити мова з MI. Так що так, не потрібно. Колись. Однак, це може бути дуже корисним інструментом, і так званий "Страшний Діамант" ... Я ніколи не розумів, чому слово "боїться" навіть існує. Це насправді досить просто міркувати і, як правило, не є проблемою.
Томас Едінг

145

З інтерв'ю з Bjarne Stroustrup :

Люди цілком правильно говорять, що вам не потрібно багаторазове успадкування, тому що все, що ви можете зробити з багаторазовим успадкуванням, ви також можете зробити з одним успадкуванням. Ви просто використовуєте трюк про делегацію, про який я згадав. Крім того, вам взагалі не потрібна спадщина, тому що все, що ви робите з одним успадкуванням, ви також можете зробити без спадкування, пересилаючи через клас. Насправді вам також не потрібні будь-які класи, тому що ви можете все це робити за допомогою покажчиків та структур даних. Але чому б ти хотів це зробити? Коли зручно користуватися мовними засобами? Коли ви віддасте перевагу вирішенню? Я бачив випадки, коли корисність багаторазового успадкування, і я навіть бачив випадки, коли корисне багатократне успадкування. Як правило, я вважаю за краще використовувати засоби, пропоновані мовою, щоб робити обхідні шляхи


6
Є деякі функції C ++, які я пропустив у C # та Java, хоча наявність у них ускладнить / ускладнить дизайн. Наприклад, гарантії незмінності const- мені доводилося писати незграбні обхідні шляхи (як правило, використовуючи інтерфейси та композицію), коли клас справді потребував змінних та незмінних варіантів. Однак я жодного разу не пропускав багаторазове успадкування і ніколи не відчував, що мені доведеться писати обхід через відсутність цієї функції. У цьому різниця. У будь-якому випадку, який я коли-небудь бачив, не використовувати MI - це кращий вибір дизайну, а не обхід.
BlueRaja - Danny Pflughoeft

24
+1: Ідеальна відповідь: Якщо ви не хочете ним користуватися, тоді не використовуйте.
Томас Едінг

38

Немає причини цього уникати, і це може бути дуже корисно в ситуаціях. Вам потрібно бути обізнаним з потенційними проблемами.

Найбільший - це діамант смерті:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

Тепер у вас є дві "копії" GrandParent в "Child".

Однак C ++ подумав про це і дозволяє вам робити віртуальне спадкування, щоб подолати проблеми.

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

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


21
А якщо заборонити спадкування і використовувати тільки склад замість цього, у вас завжди є два GrandParentв Child. Є страх перед ІМ, адже люди просто думають, що можуть не зрозуміти мовних правил. Але кожен, хто не може отримати ці прості правила, також не може написати нетривіальну програму.
curiousguy

1
Це звучить суворо, правила C ++ у цілому дуже чіткі. Добре, тож навіть якщо окремі правила прості, їх існує багато, це робить всю справу не простою, принаймні для людини.
Зебрафіш

11

Див. W: Множинне спадкування .

Багатократне успадкування отримало критику і як таке не реалізується багатьма мовами. Критика включає:

  • Підвищена складність
  • Семантичну неоднозначність часто називають алмазною проблемою .
  • Не маючи можливості явно успадкувати кілька разів з одного класу
  • Порядок спадкової зміни семантики класів.

Багатократне успадкування на мовах з конструкторами стилю C ++ / Java загострює проблему успадкування конструкторів та конструкторських ланцюгів, створюючи тим самим проблеми обслуговування та розширення цих мов. Об'єкти у відносинах успадкування з дуже різними способами побудови важко реалізувати в рамках парадигми ланцюга конструктора.

Сучасний спосіб вирішити це за допомогою інтерфейсу (чисто абстрактного класу), такого як інтерфейс COM та Java.

Я можу робити щось інше замість цього?

Так, ти можеш. Я збираюся вкрасти у GoF .

  • Програма на інтерфейс, а не на реалізацію
  • Віддавайте перевагу складу над спадщиною

8

Публічна спадщина - це відносини IS-A, і іноді клас буде типом декількох різних класів, а іноді важливо це відобразити.

"Міксини" також іноді корисні. Вони, як правило, невеликі класи, як правило, не успадковуючи нічого, забезпечуючи корисну функціональність.

Поки ієрархія спадщини є досить неглибокою (як це має бути майже завжди), і якщо ви добре керуєтесь, ви навряд чи отримаєте жахливу спадщину алмазів. Діамант не є проблемою для всіх мов, які використовують багатократне успадкування, але поводження з C ++ це часто незручно і іноді спантеличує.

Хоча я стикався з випадками, коли багаторазове успадкування дуже зручно, вони насправді досить рідкісні. Це, мабуть, тому, що я вважаю за краще використовувати інші методи проектування, коли мені дійсно не потрібно багаторазове успадкування. Я вважаю за краще уникати плутанини мовних конструкцій, і легко будувати випадки успадкування, коли вам доведеться дуже добре прочитати посібник, щоб зрозуміти, що відбувається.


6

Ви не повинні "уникати" багаторазового успадкування, але ви повинні знати про проблеми, які можуть виникнути, такі як "проблема з алмазами" ( http://en.wikipedia.org/wiki/Diamond_problem ) і ставитись до наданої вам сили обережно , як слід з усіма повноваженнями.


1
+1: Так само, як ви не повинні уникати покажчиків; вам просто потрібно знати про їх наслідки. Людям потрібно перестати брязкати фразами (наприклад, "ІМ - це зло", "Не оптимізуйте", "Гото є злом"), вони навчилися в Інтернеті лише тому, що деякі хіпі сказали, що це погано для їх гігієни. Найгірше, що вони ніколи навіть не намагалися використовувати подібні речі, а просто беруть це так, ніби говорять з Біблії.
Томас Едінг

1
Я згоден. Не «уникайте» цього, але знайте найкращий спосіб вирішити проблему. Можливо, я б скоріше використовував композиційні риси, але C ++ цього не має. Можливо, я б скоріше скористався автоматизованою делегуванням "ручки", але у C ++ цього немає. Тому я використовую загальний і тупий інструмент ІМ для декількох різних цілей, можливо, одночасно.
JDługosz

3

Ризикуючи стати трохи абстрактним, я вважаю ілюмінацією думати про спадщину в рамках теорії категорій.

Якщо ми думаємо про всі наші класи та стрілки між ними, що позначають спадкові відносини, то щось подібне

A --> B

означає, що class Bпоходить від class A. Зауважимо, що, дано

A --> B, B --> C

ми говоримо, що C походить від B, що походить від A, тому C також походить від A, таким чином

A --> C

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

Це трохи налаштування, але разом з цим давайте подивимось на наш Diamond of Doom:

C --> D
^     ^
|     |
A --> B

Це тінистий вигляд, але це зробить. Так Dуспадковується від всіх A, Bі C. Крім того, і наближаючись до вирішення питання ОП, Dтакож успадковується від будь-якого суперкласу A. Ми можемо намалювати схему

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

Тепер проблеми, пов’язані з Діамантом Смерти, - це коли Cі Bділитися деякими іменами властивостей / методів, а речі стають неоднозначними; однак, якщо ми переходимо до будь-якої спільної поведінкиA то неоднозначність зникає.

Розміщуючи категорично, ми хочемо A, Bі Cщоб вони були такими, що якщо Bі Cнаслідувати з Qцього Aмоменту, можна переписати як підклас Q. Це робить Aщось, що називається виштовхуванням .

Існує також симетрична конструкція, що Dназивається відкат . Це по суті найзагальніший корисний клас, який ви можете побудувати, який успадковується і від, Bі від C. Тобто, якщо у вас є будь-який інший клас, що Rмножиться у спадок від Bта C, то Dце клас, який Rможна переписати як підклас D.

Переконайтесь, що ваші наконечники алмазу - це відкатки та виштовхування, що дає нам чудовий спосіб вирішити проблеми зіткнення імен або технічного обслуговування, які можуть виникнути інакше.

Примітка Paercebal «сек відповідь надихнула це, оскільки його зауваження маються на увазі вищезгаданою моделлю, враховуючи, що ми працюємо в повному класі всіх можливих класів.

Я хотів узагальнити його аргумент до чогось, що показує, як складні множинні відносини успадкування можуть бути як потужними, так і непроблемними.

TL; DR Подумайте про спадкові відносини у вашій програмі як про формування категорії. Тоді ви можете уникнути проблем з Diamond of Doom, зробивши множинні успадковані класи та симетрично, зробивши загальний батьківський клас, який є віддачею.


3

Ми використовуємо Ейфель. У нас чудовий ІМ. Не хвилюйтесь. Немає питань. Легко керується. Бувають випадки, щоб НЕ використовувати ІМ. Однак це корисно більше, ніж люди усвідомлюють, оскільки вони: А) небезпечною мовою, яка не впорядковує це, або -БОР-В) задоволена тим, як вони працювали навколо МІ роками та роками -ОР- С) іншими причинами ( Занадто численні для переліку, я цілком впевнений - див. відповіді вище).

Для нас, використовуючи Ейфель, ІМ настільки ж природно, як і все інше, та ще один прекрасний інструмент у панелі інструментів. Чесно кажучи, ми зовсім не переймаємось тим, що ніхто більше не використовує Ейфеля. Не хвилюйтесь. Ми задоволені тим, що маємо, і запрошуємо вас подивитися.

Поки ви дивитесь: Візьміть особливу увагу на безпеку порожнеч та викорінення перенаправлення нульових покажчиків. Поки ми всі танцюємо навколо MI, ваші покажчики втрачаються! :-)


2

Кожна мова програмування має дещо інше трактування об'єктно-орієнтованого програмування із плюсами та мінусами. Версія C ++ прямо робить акцент прямо на продуктивність і має супутній мінус того, що писати недійсний код тривожно легко - і це стосується багатократного успадкування. Як наслідок, існує тенденція відхиляти програмістів від цієї функції.

Інші люди вирішили питання про те, що для багаторазового успадкування не годиться. Але ми побачили досить багато коментарів, які більш-менш означають, що причина цього уникати, тому що це не безпечно. Ну так і ні.

Як це часто буває в C ++, якщо ви дотримуєтесь основних положень, ви можете їх безпечно використовувати без постійного "огляду через плече". Ключова ідея полягає в тому, що ви виділяєте особливий вид визначення класу, який називається "сумішшю"; class - це суміш, якщо всі його функції-учасники є віртуальними (або чистими віртуальними). Тоді вам дозволяється успадковувати з одного основного класу та стільки «мік-інгів», скільки вам подобається, - але вам слід успадковувати міксини з ключовим словом «віртуальний». напр

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

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

Зауважте, що моє використання слова "змішання" тут не те саме, що параметризований шаблон шаблону (див. Це посилання для хорошого пояснення), але я думаю, що це справедливе використання термінології.

Зараз я не хочу створювати враження, що це єдиний спосіб безпечного використання безлічі спадків. Це просто один спосіб перевірити.


2

Ви повинні використовувати його обережно, є деякі випадки, як, наприклад, проблема з діамантами , коли все може ускладнитися.

alt текст
(джерело: learncpp.com )


12
Це поганий приклад, адже копір повинен складатися зі сканера та принтера, а не успадковувати їх.
Svante

2
У будь-якому випадку, a Printerнавіть не повинно бути PoweredDevice. A Printerпризначений для друку, а не для управління енергією. Впровадження певного принтера, можливо, доведеться виконати деяке управління живленням, але ці команди управління живленням не повинні піддаватися безпосередньо користувачам принтера. Я не уявляю жодного реального використання цієї ієрархії у світі.
curiousguy


1

Поза алмазним малюнком багаторазове успадкування має тенденцію до ускладнення розуміння об'єктної моделі, що в свою чергу збільшує витрати на обслуговування.

Композицію властиво легко зрозуміти, зрозуміти та пояснити. Писати код може набридливо, але хороший IDE (минуло кілька років, як я працював з Visual Studio, але, безумовно, всі Java IDE мають чудові інструменти автоматизації ярликів композиції), вам слід перешкодити цьому перешкоді.

Крім того, що стосується технічного обслуговування, "проблема з алмазами" виникає і в нелітеральних випадках успадкування. Наприклад, якщо у вас є A і B, і ваш клас C поширює їх обоє, а A має метод "makeJuice", який робить апельсиновий сік, і ви подовжуєте його, щоб зробити апельсиновий сік з поворотом лайма: що відбувається, коли дизайнер для " B 'додає метод "makeJuice", який генерує та електричний струм? "A" і "B" можуть бути сумісними "батьками" прямо зараз , але це не означає, що вони завжди будуть такими!

Загалом, максимум прагнення уникнути спадкування, а особливо багатократного спадкування, є надійним. Як і всі сентенти, є винятки, але вам потрібно переконатися, що є блимаючий зелений неоновий знак, що вказує на будь-які винятки, котрі ви кодуєте (і тренуйте свій мозок так, щоб у будь-який час ви бачили такі дерева спадщини, які ви малюєте у вашому власному миготливому зеленому неоні знак), і що ви перевіряєте, щоб переконатися, що все це має сенс раз у раз.


what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?Угу, ви отримуєте помилку компіляції звичайно (якщо використання неоднозначне).
Томас Едінг

1

Основне питання, що стосується ІМ конкретних об'єктів, полягає в тому, що рідко у вас є об'єкт, який на законних підставах повинен бути «Будьте А А Б і Б», тому рідко правильне рішення з логічних міркувань. Набагато частіше у вас є об'єкт C, який підкоряється "C може діяти як A або B", чого ви можете досягти за допомогою успадкування інтерфейсу та композиції. Але не помиляйтесь - успадкування декількох інтерфейсів все ще є MI, лише його підмножиною.

Зокрема, для C ++ ключовою слабкістю функції є не ІСТИЧНІСТЬ множинної спадковості, але деякі конструкції, які вона допускає, майже завжди є неправильними. Наприклад, успадкування декількох копій одного і того ж об'єкта, як-от:

class B : public A, public A {};

неправильно формується ВИЗНАЧЕННЯ. У перекладі на англійську це "B - це A і A". Так, навіть у людській мові існує сувора неоднозначність. Ви мали на увазі "B має 2 As" або просто "B - це A" ?. Дозвіл такого патологічного коду, а ще гірше використання його прикладом використання, зробив C ++ не прихильним, коли справа стосувалася збереження функції на мовах-наступниках.


0

Ви можете використовувати композицію, віддаючи перевагу спадку.

Загальне відчуття, що композиція краща, і це дуже добре обговорюється.


4
-1: The general feeling is that composition is better, and it's very well discussed.Це не означає, що композиція краща.
Томас Едінг

Окрім того, що насправді краще.
Алі Афшар

12
За винятком випадків, коли це не краще.
Томас Едінг

0

він займає 4/8 байт на кожен клас. (Один цей покажчик на клас).

Це ніколи не може викликати занепокоєння, але якщо одного дня у вас буде мікроструктура даних, яка встановлена ​​мільярдами часу, це буде.


1
Впевнені в цьому? Зазвичай для будь-якого серйозного успадкування потрібен вказівник у кожного члена класу, але чи обов'язково це масштабування із спадковими класами? Більше того, скільки разів у вас були мільярди маленьких структур даних?
Девід Торнлі

3
@DavidThornley " скільки разів у вас були мільярди підліткових структур даних? ", Коли ви складаєте аргументи проти MI.
curiousguy
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.