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

ОГЛАВЛЕНИЕ

Атрибуты можно использовать, чтобы предоставить функциональную возможность множественного наследования для классов C#

Введение

Данная статья показывает возможный способ реализовать множественное наследование (MI) внутри кода C#. Этот способ не имеет таких классических проблем MI, как порядок наследования.

Предпосылки

Есть немало способов избежать или преодолеть необходимость использования MI в C#. Но иногда, особенно для маленьких проектов (игры и т.д.), было бы более подходящим использовать его преимущества. Мы надеемся найти более разумный путь, на котором нас не будут беспокоить препятствия, которые собственный код C# помещает на пути множественного наследования.

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

Класс

Сначала нужно определить изящный родительский класс, который будет обрабатывать всю функциональность, необходимую для реализации MI. Это не очень сложно. Отсутствующее наследование будет компенсировано классом MI attribute.

using System;
using System.Collections.Generic;

namespace CSharpMultipleInheritance
{
    public class Ancestor : MarshalByRefObject
    {

Когда создается класс, унаследованный от класса Ancestor, набор экземпляров его пользовательского класса attribute собирается и сохраняется в словаре. Этот хэш позже будет использоваться для доступа к унаследованным свойствам (методы и т.д.)

private readonly Dictionary<Type, Object> attributes = null;

public Dictionary<Type, Object> Attributes
{
    get { return attributes; }
}

public Ancestor()
{
    attributes = GetAttributes(GetType());
}

private Dictionary<Type, Object> GetAttributes(Type sourceType)
{
    Object[] collection = sourceType.GetCustomAttributes(true);
    Dictionary<Type, Object> result = new Dictionary<Type, Object>();

    foreach (Object attribute in collection)
    {
        Type attributeType = attribute.GetType();

        if (result.ContainsKey(attributeType))
        {
            throw new Exception(string.Format(
            STR_DupliciteAttributeFound, attributeType.Name, sourceType.Name));
        }
        else
        {
            result.Add(attributeType, attribute);
        }
    }

    return result;
}


Методы

Метод Check проверяет, является ли класс "унаследованным" от определенного класса attribute. Это эквивалентно собственному оператору C# IS в нашем MI. Как показано в коде, он только проверяет, присутствует ли тип атрибута в списке пользовательских атрибутов.

public Boolean Check<TAttribute>()
{
return attributes.ContainsKey(typeof(TAttribute));
}

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

public Boolean Is<TAncestor>() where TAncestor : Ancestor
{
    Boolean result = true;
    Dictionary<Type, Object> sourceList = Attributes;
    Dictionary<Type, Object> destinationList = GetAttributes(typeof(TAncestor));

    foreach (KeyValuePair<Type, Object> destinationPair in destinationList)
    {
        result = result && sourceList.ContainsKey(destinationPair.Key);
    }

    return result;
}

Наиболее важной функцией, которая делает возможным MI непосредственно, является метод Use. Он извлекает класс attribute, таким образом разрешая доступ к его внутренним свойствам (методам и т.д.).

public TAttribute Use<TAttribute>()
{
    if (Check<TAttribute>())
    {
        return (TAttribute)attributes[typeof(TAttribute)];
    }
    else
    {
        throw new ArgumentNullException(string.Format(
            STR_AttributeNotFound, typeof(TAttribute).Name, GetType().Name));
    }
}

Примеры использования

Мы выбрали классический пример множественного наследования из Википедии. Другой пример можно посмотреть в присоединенных архивных файлах.

Классы attribute (будущие наследуемые кандидаты) определяются как стандартные attributes (атрибуты). Только поля public (вместо полей private со свойствами, объявленными как public) используются в этой статье для того, чтоб она была очевидной (понятной).

Пусть у нас есть два класса: класс Person, который показывает имя и возраст человека, и класс Worker, который определяет зарплату рабочего.

public class Person : Attribute
{
    public String Name;
    public Int32 Age;
}

public class Worker : Attribute
{
    public Double Sallary;
}

Теперь attributes назначены нашему классу с MI. Этот класс должен быть унаследован от класса Ancestor (определен выше), чтобы использовать свой потенциал MI.

 [Person, Worker]
public class Musician : Ancestor
{
    public String Name; // this can be used as a musician stage name
}

Суперклассы доступны через методы класса Ancestor (Check, Is и Use), как показано в следующем примере:

static void Main()
{
    Musician bono = new Musician();

    bono.Use<Worker>().Name = "Bono";
    bono.Use<Person>().Name = "Paul David Hewson";
    bono.Use<Person>().Age = 47;
       
    if (bono.Is<Musician>()) Console.WriteLine("{0} is musician.",
    bono.Use<Worker>().Name);
        
    if (bono.Check<Person>()) Console.WriteLine("His age is {0}",
    bono.Use<Person>().Age);
}

Возможное улучшение

  1. Все использованные классы attribute могут иметь общий интерфейс или быть унаследованы от общего класса attribute, чтобы отделить их от других классов attributes, не поддерживающих множественное наследование.
  2. Список классов FieldInfo (PropertyInfo и т.д.) может быть кэширован через все стандартные поля и также поля attribute, чтобы создать еще более реальное множественное наследование.

Ограничения

  • Очевидное ограничение состоит в том, что MI нельзя использовать для уже унаследованных классов. Мы пытались сделать расширение методов C# 3.0, которые могли бы получить доступ к любому объекту. Мы также убедились, что это невозможно, потому что нет способа получить пользовательские атрибуты экземпляра определенного класса из общего объекта.
  • Другим ограничением является то, что используется один "слот" наследования – оригинальный из C#. Это должно компенсироваться непосредственно самим MI.
  • Надоедает необходимость использовать круглые скобки для методов MI. Это вызвано недостатком общих свойств, которые запрещены конструкцией C#.
  • Компилятор неспособен уловить несоответствия в классе Use<>. Это необходимо устранить с помощью проверок Is<> или непосредственно самому программисту.

Загрузить исходный код (Visual Studio 2005) - 7.3 KB 

Загрузить исходный код (Visual Studio 2008) - 7.38 KB