Подтвердить что ты не робот

Должно ли С# иметь множественное наследование?

Я столкнулся с многочисленными аргументами против включения множественного наследования в С#, некоторые из которых включают (философские аргументы в сторону):

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

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

Является ли исключение множественного наследования выражением о том, что разработчики недостаточно умны, чтобы использовать их с умом и неспособны решить сложные проблемы, когда они возникают?

Я лично приветствовал бы введение множественного наследования в С# (возможно, С##).


Добавление. Мне было бы интересно узнать ответы, которые поступают из одного (или процедурного фона) по сравнению с многократным наследованием. Я часто обнаружил, что разработчики, которые не имеют опыта работы с множественным наследованием, часто по умолчанию используют аргумент с множественным наследованием - это лишний аргумент просто потому, что у них нет опыта с парадигмой.

4b9b3361

Ответ 1

Я никогда не пропустил его ни разу, ни когда-либо. Да, это [MI] усложняется, и да, интерфейсы выполняют подобную работу по-разному - но это не самая большая точка: в общем смысле она просто не нужна большую часть времени. Даже одно наследование используется во многих случаях.

Ответ 2

Предпочитают агрегацию по наследованию!

class foo : bar, baz

часто лучше обрабатывается с помощью

class foo : Ibarrable, Ibazzable
{
  ... 
  public Bar TheBar{ set }
  public Baz TheBaz{ set }

  public void BarFunction()
  {
     TheBar.doSomething();
  }
  public Thing BazFunction( object param )
  {
    return TheBaz.doSomethingComplex(param);
  }
}

Таким образом, вы можете менять различные версии IBarrable и IBazzable для создания нескольких версий приложения без необходимости писать еще один класс.

Инъекционная инъекция может помочь с этим.

Ответ 3

Одной из проблем, связанных с множественным наследованием, является различие между наследованием интерфейсов и наследованием реализации.

С# уже имеет чистую реализацию наследования интерфейса (включая выбор неявных или явных реализаций) с использованием чистых интерфейсов.

Если вы посмотрите на С++, для каждого класса, который вы указываете после двоеточия в объявлении class, вид наследования, который вы получаете, определяется модификатором доступа (private, protected или public), С наследованием public вы получаете полную беспорядок множественного наследования и— несколько интерфейсов смешиваются с несколькими реализациями. С наследованием private вы просто получаете реализацию. Объект "class Foo : private Bar" никогда не может быть передан функции, ожидающей Bar, потому что он, как если бы класс Foo действительно имел только закрытое поле Bar и автоматически реализованное шаблон делегирования.

Чистое наследование множественного наследования (которое действительно просто автоматическое делегирование) не представляет никаких проблем и было бы замечательно иметь на С#.

Что касается множественного наследования интерфейсов от классов, существует множество различных возможных вариантов реализации этой функции. Каждый язык с множественным наследованием имеет свои собственные правила относительно того, что происходит, когда метод вызывается с тем же именем в нескольких базовых классах. Некоторые языки, такие как Common Lisp (особенно объектная система CLOS) и Python, имеют протокол метаобъектов, где вы можете указать приоритет базового класса.

Здесь одна возможность:

abstract class Gun
{ 
    public void Shoot(object target) {} 
    public void Shoot() {}

    public abstract void Reload();

    public void Cock() { Console.Write("Gun cocked."); }
}

class Camera
{ 
    public void Shoot(object subject) {}

    public virtual void Reload() {}

    public virtual void Focus() {}
}

//this is great for taking pictures of targets!
class PhotoPistol : Gun, Camera
{ 
    public override void Reload() { Console.Write("Gun reloaded."); }

    public override void Camera.Reload() { Console.Write("Camera reloaded."); }

    public override void Focus() {}
}

var    pp      = new PhotoPistol();
Gun    gun     = pp;
Camera camera  = pp;

pp.Shoot();                    //Gun.Shoot()
pp.Reload();                   //writes "Gun reloaded"
camera.Reload();               //writes "Camera reloaded"
pp.Cock();                     //writes "Gun cocked."
camera.Cock();                 //error: Camera.Cock() not found
((PhotoPistol) camera).Cock(); //writes "Gun cocked."
camera.Shoot();                //error:  Camera.Shoot() not found
((PhotoPistol) camera).Shoot();//Gun.Shoot()
pp.Shoot(target);              //Gun.Shoot(target)
camera.Shoot(target);          //Camera.Shoot(target)

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

Кроме того, вы можете реализовать множественное наследование в С# сегодня с неявными операторами преобразования:

public class PhotoPistol : Gun /* ,Camera */
{
    PhotoPistolCamera camera;

    public PhotoPistol() {
        camera = new PhotoPistolCamera();
    }

    public void Focus() { camera.Focus(); }

    class PhotoPistolCamera : Camera 
    { 
        public override Focus() { }
    }

    public static Camera implicit operator(PhotoPistol p) 
    { 
        return p.camera; 
    }
}

Он не идеален, хотя он не поддерживается операторами is и as, а System.Type.IsSubClassOf().

Ответ 4

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

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

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

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

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

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

Если мы думаем, что у нас есть эта проблема в большом смысле, мы выбираем для себя богатую модель событий, которая, я думаю, является, вероятно, правильным ответом на С#, но не каждая проблема решена таким образом - иногда вы хотите, чтобы простой публичный интерфейс и более богатый для расширителей.

Ответ 5

С# поддерживает одиночные наследования, интерфейсы и методы расширения. Между ними они обеспечивают практически все, что обеспечивает множественное наследование, без головных болей, которые приносит множественное наследование.

Ответ 6

Множественное наследование не поддерживается CLR любым способом, о котором я знаю, поэтому я сомневаюсь, что его можно было бы эффективно поддерживать, как в С++ (или Eiffel, что может сделать это лучше, учитывая, что язык специально разработан для ИМ).

Хорошая альтернатива множественному наследованию называется Traits. Это позволяет смешивать различные единицы поведения в один класс. Компилятор может поддерживать черты как расширение времени компиляции для системы с одним наследованием. Вы просто объявляете, что класс X включает в себя черты A, B и C, а компилятор ставит те черты, которые вы просите вместе, чтобы сформировать реализацию X.

Например, предположим, что вы пытаетесь реализовать IList (из T). Если вы посмотрите на различные реализации IList (из T), они часто используют один и тот же код. Это были черты. Вы просто объявляете черту с общим кодом, и вы можете использовать этот общий код в любой реализации IList (из T) - даже если в реализации уже есть какой-то другой базовый класс. Вот как выглядит синтаксис:

/// This trait declares default methods of IList<T>
public trait DefaultListMethods<T> : IList<T>
{
    // Methods without bodies must be implemented by another 
    // trait or by the class
    public void Insert(int index, T item);
    public void RemoveAt(int index);
    public T this[int index] { get; set; }
    public int Count { get; }

    public int IndexOf(T item)
    {
        EqualityComparer<T> comparer = EqualityComparer<T>.Default;
        for (int i = 0; i < Count; i++)
            if (comparer.Equals(this[i], item))
                return i;
        return -1;
    }
    public void Add(T item)
    {
        Insert(Count, item);
    }
    public void Clear()
    {   // Note: the class would be allowed to override the trait 
        // with a better implementation, or select an 
        // implementation from a different trait.
        for (int i = Count - 1; i >= 0; i--)
            RemoveAt(i);
    }
    public bool Contains(T item)
    {
        return IndexOf(item) != -1;
    }
    public void CopyTo(T[] array, int arrayIndex)
    {
        foreach (T item in this)
            array[arrayIndex++] = item;
    }
    public bool IsReadOnly
    {
        get { return false; }
    }
    public bool Remove(T item)
    {
        int i = IndexOf(item);
        if (i == -1)
            return false;
        RemoveAt(i);
        return true;
    }
    System.Collections.IEnumerator 
        System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
    IEnumerator<T> GetEnumerator()
    {
        for (int i = 0; i < Count; i++)
            yield return this[i];
    }
}

И вы используете эту черту следующим образом:

class MyList<T> : MyBaseClass, DefaultListMethods<T>
{
    public void Insert(int index, T item) { ... }
    public void RemoveAt(int index)       { ... }
    public T this[int index] {
        get { ... }
        set { ... }
    }
    public int Count {
        get { ... }
    }
}

Конечно, я просто царапаю поверхность здесь. Более полное описание см. В статье Черты: составные единицы поведения (PDF).

Язык Rust (из Mozilla) реализовал черты интересным образом: они заметили, что черты сходны с реализациями интерфейса по умолчанию, поэтому унифицированные интерфейсы и черты объединяются в одну функцию (которую они называют чертами). Основное различие между чертами и реализациями интерфейса по умолчанию (в настоящее время Java) состоит в том, что черты могут содержать частные или защищенные методы, в отличие от традиционных методов интерфейса, которые должны быть общедоступными. Если черты и интерфейсы не унифицированы в одну функцию, то другое отличие состоит в том, что вы можете иметь ссылку на интерфейс, но вы не можете иметь ссылку на признак; признак сам по себе не является типом.

Ответ 7

Я действительно пропустил множественное наследование по одной конкретной причине... шаблон dispose.

КАЖДЫЙ раз, когда мне нужно реализовать шаблон dispose, я говорю себе: "Мне жаль, что я не могу просто проистекать из класса, который реализует шаблон dispose с несколькими виртуальными переопределениями". Я копирую и вставляю один и тот же код котельной в каждый класс, который реализует IDispose, и я ненавижу его.

Ответ 8

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

Ответ 9

ДА! ДА! и ДА!

Серьезно, я разрабатываю библиотеки GUI всю свою карьеру, а MI (Multiple Inheritance) делает этот FAR проще, чем SI (Single Inheritance).

Сначала я сделал SmartWin ++ в С++ (MI сильно использовал), тогда я сделал Gaia Ajax и, наконец, теперь Ra-Ajax, и я могу с предельно уверенным утверждением, что правила MI для некоторых мест. Одним из таких мест является библиотека GUI...

И аргументы, утверждающие, что MI "слишком сложны", и такие, как правило, ставятся там людьми, пытающимися построить языковые войны и попадая в лагерь, который "в настоящее время не имеет MI"...

Так же, как и функциональные языки программирования (например, Lisp), преподаватели (не-Lispерс) были "слишком сложны" не функциональными защитниками языка программирования...

Люди боятся неизвестного...

MI ПРАВИЛА!

Ответ 10

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

interface I
{
    void F();
    void G();
}


class DefaultI : I
{
    void F() { ... }
    void G() { ... }
}

class C : I = DefaultI
{
    public void F() { ... } // implements I.F
}

В этом случае ((I)new C()).F() вызовет C реализацию I.F(), а ((I)new C()).G() вызовет DefaultI реализацию I.G().

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

Ответ 11

Я работаю с С#, поскольку он был впервые доступен как релиз альфа/бета и никогда не пропускал множественное наследование. MI хорош для некоторых вещей, но почти всегда есть другие способы достижения одного и того же результата (некоторые из которых на самом деле упрощаются или создают более понятную реализацию).

Ответ 12

Множественное наследование вообще может быть полезным, и многие языки OO реализуют его так или иначе (С++, Eiffel, CLOS, Python...). Это важно? Нет. Приятно ли иметь? Да.

Ответ 13

Обновление
Я бросаю вызов всем, кто проголосует за меня, чтобы показать мне пример множественного наследования, который я не могу легко переносить на язык с единственным наследованием. Если кто-либо не может показать такой образец, я утверждаю, что его не существует. Я портировал тонны кода С++ (MH) на Java (no-MH), и это никогда не было проблемой, независимо от того, сколько MH использует код С++.


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

На самом деле множественное наследование обычно злоупотребляют. Если вы используете OO-дизайн для того, чтобы каким-то образом моделировать реальный мир в классах, вы никогда не дойдете до такой степени, что на самом деле имеет смысл множественное наследование. Можете ли вы предоставить полезный пример для множественного наследования? Большинство примеров, которые я видел до сих пор, на самом деле "ошибочны". Они делают что-то подклассом, это на самом деле просто дополнительное свойство и, следовательно, фактически интерфейс.

Взгляните на Sather. Это язык программирования, где интерфейсы имеют множественное наследование, так как почему бы и нет (он не может создать проблему с алмазом), однако классы, которые не являются интерфейсами, не имеют никакого наследования. Они могут реализовывать только интерфейсы, и они могут "включать" другие объекты, что делает эти другие объекты неотъемлемой частью их, но это не то же самое, что наследование, а скорее форма делегирования (вызов метода "унаследован" путем включения объектов на самом деле просто перенаправлены на экземпляры этих объектов, инкапсулированных в ваш объект). Я думаю, что эта концепция довольно интересна, и она показывает, что вы можете иметь полный чистый язык OO без какого-либо наследования реализации вообще.

Ответ 14

Нет.

(для голосования)

Ответ 15

одна из действительно приятных и в то время новых вещей о DataFlex 4GL v3 + (я знаю, я знаю, Data what?) была поддержкой mixin наследования - методы из любого другие классы могут быть повторно использованы в вашем классе; до тех пор, пока ваш класс предоставил свойства, используемые этими методами, он работал отлично, и не было проблем с "проблемами с алмазами" или другого "наследования" с множественным наследованием.

Я хотел бы увидеть что-то подобное в С#, поскольку это упростит некоторые виды проблем с абстракцией и конструкцией

Ответ 16

Вместо множественного наследования вы можете использовать mixins, что является лучшим решением.

Ответ 17

Я думаю, что это слишком усложняло бы вещи, не обеспечив достаточной рентабельности инвестиций. Мы уже видим, что люди обрабатывают .NET-код с слишком глубокими деревьями наследования. Я могу представить себе зверства, если люди обладают способностью делать многократное наследование.

Я не буду отрицать, что у него есть потенциал, но я просто не вижу достаточной выгоды.

Ответ 18

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

Ответ 20

Я думаю, что это просто. Как и любая другая сложная парадигма программирования, вы можете злоупотреблять ею и причинять себе боль. Можете ли вы неправильно использовать объекты (о да!), Но это не значит, что OO плохо само по себе.

Аналогично MI. Если у вас нет большого "дерева" унаследованных классов или много классов, которые предоставляют одинаковые именованные методы, тогда вы отлично справитесь с MI. Фактически, поскольку MI обеспечивает реализацию, вам часто будет лучше, чем реализация SI, когда вам нужно перекодировать или вырезать и вставлять методы в делегированные объекты. Меньше кода в этих случаях лучше. Вы можете сделать всемогущий беспорядок обмена кодом, пытаясь повторно использовать объекты через наследование интерфейса. И такие обходные пути не пахнут правильно.

Я думаю, что однонамерная модель .NET ошибочна: они должны были идти только с интерфейсами или только с MI. Имея "половину и половину" (т.е. Одно наследование реализации плюс множественное наследование интерфейса), более запутанно, чем должно быть, а не элегантно вообще.

Я родом из MI-фона, и я не боюсь и не сгорел.

Ответ 21

Я разместил это здесь пару раз, но я просто думаю, что это действительно круто. Вы можете узнать, как подделать MI здесь. Я также думаю, что в статье подчеркивается, почему МИ является такой болью, даже если это не предназначено.

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

Ответ 22

Я тоже использовал множественную наследование в С++, но вам действительно нужно знать, что вы делаете, чтобы не попасть в неприятности, особенно если у вас есть два базовых класса, которые делятся с бабушкой и дедушкой. Затем вы можете столкнуться с проблемами с виртуальным наследованием, вынуждая объявлять каждый конструктор, который вы собираетесь вызвать цепочку (что значительно усложняет повторное использование двоичных файлов)... это может быть беспорядок.

Что еще более важно, способ создания CLI не позволяет легко реализовать MI. Я уверен, что они могли бы сделать это, если бы захотели, но у меня есть другие вещи, которые я предпочитаю видеть в CLI, чем множественное наследование.

Вещи, которые я хотел бы увидеть, включают некоторые функции SpeС#, например, типы с нулевыми значениями. Я также хотел бы видеть больше безопасности объектов, имея возможность объявлять параметры как const и возможность объявлять функцию const (что означает, что вы гарантируете, что внутреннее состояние объекта не будет изменено методом и компилятор дважды проверяет вас).

Я думаю, что между Single Inheritence, Multiple Interface Inheritence, Generics и Extension Methods вы можете делать практически все, что вам нужно. Если что-то может улучшить ситуацию для кого-то, желающего МИ, я думаю, что потребуется какая-то языковая конструкция, которая позволила бы упростить агрегацию и состав. Таким образом, вы можете иметь общий интерфейс, но затем делегируйте свою реализацию частному экземпляру класса, который вы обычно наследуете. Прямо сейчас, для этого требуется много кодовых табличек. Наличие более автоматизированной языковой функции для этого поможет значительно.

Ответ 23

Нет, мы ушли от него. Теперь вам это нужно.

Ответ 24

Я предпочитаю С++. Я использовал Java, С# и т.д. По мере того, как мои программы становятся более сложными в такой среде OO, я нахожусь без множественного наследования. Это мой субъективный опыт.

Он может сделать для удивительного кода спагетти... он может сделать для удивительно элегантного кода.

Ответ 25

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

Вы решили использовать те API, которые уже написал разработчик, и у вас тоже нет.

Ответ 26

Дайте С# implicits, и вы не пропустите множественное наследование или любое наследование в этом отношении.

Ответ 27

Нет, я этого не делаю. Я использую все другие функции OO для разработки того, что я хочу. Я использую инкапсуляцию интерфейса и объекта, и я никогда не ограничиваюсь тем, что хочу делать.

Ответ 28

Я стараюсь не использовать наследование. Чем меньше я могу каждый раз.

Ответ 29

Нет, если проблема с алмазом не решена. и вы можете использовать композицию, пока это не будет разрешено.

Ответ 30

Если мы вводим Multiple Inheritance, то мы снова сталкиваемся со старой проблемой Diamond С++...

Однако для тех, кто считает это неизбежным, мы все же можем вводить множественные эффекты наследования с помощью композиции (создавать несколько объектов в объекте и публиковать общедоступные методы, которые делегируют обязанности составленному объекту и возвращаются)...

Так зачем беспокоиться о множественном наследовании и сделать ваш код уязвимым для неизбежных исключений...