Я прочитал несколько веток о динамическом приведении в C ++, и все люди утверждают, что это указывает на плохой дизайн. На других языках я никогда не задумывался о проверке типа объекта. Я никогда не использую его как альтернативу полиморфизму и только тогда, когда сильная связь кажется вполне приемлемой. Одна из таких ситуаций, с которой я сталкиваюсь довольно часто: наличие списка (я использую std :: vector в C ++) объектов, все они получены из общего базового класса. Список управляется объектом, которому разрешено знать различные подклассы (часто это небольшая иерархия частных классов в классе управляющих объектов). Храня их в одном списке (массив, вектор, …), я все еще могу извлечь выгоду из полиморфизма, но когда операция предназначена для воздействия на объекты определенного подкласса, я использую динамическое приведение или что-то подобное.
Есть ли другой подход к этому типу проблемы без динамического приведения или проверки типов, которые я пропускаю? Мне действительно любопытно, как программисты, которые избегают этого любой ценой, справились бы с ними.
Если мое описание слишком абстрактное, я мог бы написать простой пример на C ++ (Edit: см. Ниже).
class EntityContacts {
private:
class EntityContact {
private:
virtual void someVirtualFunction() { }; // Only there to make dynamic_cast work
public:
b2Contact* m_contactData;
};
class InternalEntityContact : public EntityContact {
public:
InternalEntityContact(b2Fixture* fixture1, b2Fixture* fixture2){
m_internalFixture1 = fixture1;
m_internalFixture2 = fixture2;
};
b2Fixture* m_internalFixture1;
b2Fixture* m_internalFixture2;
};
class ExternalEntityContact : public EntityContact {
public:
ExternalEntityContact(b2Fixture* internalFixture, b2Fixture* externalFixture){
m_internalFixture = internalFixture;
m_externalFixture = externalFixture;
};
b2Fixture* m_internalFixture;
b2Fixture* m_externalFixture;
};
PhysicsEntity* m_entity;
std::vector<EntityContact*> m_contacts;
public:
EntityContacts(PhysicsEntity* entity)
{
m_entity = entity;
}
void addContact(b2Contact* contactData)
{
// Create object for internal or external contact
EntityContact* newContact;
if (m_entity->isExternalContact(contactData)) {
b2Fixture* iFixture;
b2Fixture* eFixture;
m_entity->getContactInExFixtures(contactData, iFixture, eFixture);
newContact = new ExternalEntityContact(iFixture, eFixture);
}
else
newContact = new InternalEntityContact(contactData->GetFixtureA(), contactData->GetFixtureB());
// Add object to vector
m_contacts.push_back(newContact);
};
int getExternalEntityContactCount(PhysicsEntity* entity)
{
// Return number of external contacts with the entity
int result = 0;
for (int i = 0; i < m_contacts.size(); ++i) {
ExternalEntityContact* externalContact = dynamic_cast<ExternalEntityContact*>(m_contacts[i]);
if (externalContact != NULL && getFixtureEntity(externalContact->m_externalFixture) == entity)
result++;
}
return result;
}
};
Это упрощенная версия класса, которую я использую для обнаружения столкновений в игре, в которой используется физика box2d. Я надеюсь, что детали box2d не слишком отвлекают от того, что я пытаюсь показать. У меня есть очень похожий класс ‘Event’, который создает разные типы обработчиков событий, который структурирован одинаково (с подклассами базового класса EventHandler вместо EntityContact).
По крайней мере, с моей точки зрения, dynamic_cast
существует по причине, и бывают случаи, когда разумно его использовать. Это может быть один из тех времен.
Учитывая ситуацию, которую вы описываете, один возможный альтернативой может быть определение большего количества необходимых вам операций в базовом классе, но определите их как (возможно, молча) сбой, если вы вызываете их для базового класса или других классов, которые не поддерживают эти операции.
Реальный вопрос заключается в том, имеет ли смысл определять ваши операции таким образом. Возвращаясь к типичной иерархии на основе животных, если вы работаете с Birds, для Bird часто целесообразно определить fly
член, и для немногих птиц, которые не могут летать, просто потерпеть неудачу (теоретически следует переименовать в нечто вроде attempt_to_fly
, но это редко делает многое).
Если вы видите многое из этого, это имеет тенденцию указывать на отсутствие абстракции в ваших классах — например, вместо fly
или же attempt_to_fly
Вы, возможно, действительно хотите travel
член, и это зависит от конкретного животного, чтобы определить, делать ли это плавать, ползать, ходить, летать и т. д.
это
но когда операция предназначена для воздействия на объекты определенного
Подкласс Я использую динамическое приведение или что-то подобное
Похоже, что моделирование объекта не является правильным. У вас есть список, содержащий экземпляры подклассов, но на самом деле они не являются подклассами, поскольку вы не можете воздействовать на них всех одинаково (Лисков и т. Д.).
Одним из возможных решений является расширение базового класса таким образом, чтобы у вас был набор неоперативных методов, которые некоторые подклассы могут переопределять. Но это все еще звучит не совсем правильно.
Вопрос довольно широкий, поэтому необходимо учитывать некоторые общие моменты:
dynamic_cast
дает определенную стоимость. Если речь идет о производительности во время выполнения, сравните ее со стоимостью альтернатив.Обычно в таких ситуациях вы управляете не списком производных объектов, а списком указателей интерфейса (или базовых указателей), которые ссылаются на фактические производные объекты. Всякий раз, когда вы хотите выполнить операцию над конкретным объектом, вы обращаетесь к нему через его интерфейс / базовый класс, который должен предоставлять все необходимые функции. Производные классы должны переопределять открытую базовую функциональность с конкретной реализацией.
использование dynamic_cast является признаком желания реализовать шаблон проектирования, такой как Visitor или Command, поэтому я бы рекомендовал рефакторинг, чтобы сделать это очевидным
Как и в любом подходе, который считается плохим программированием, всегда будут некоторые исключения. Когда программист говорит, что «то-то и то-то есть зло, и вы никогда не должны этого делать», они действительно имеют в виду, что «почти никогда нет причин использовать то-то и то-то, поэтому вам лучше объяснить себя». Я сам никогда не сталкивался с ситуацией, когда dynamic_cast
абсолютно необходимо и не может быть легко реорганизовано. Однако, если он выполнит свою работу, пусть будет так.
Поскольку мы не знаем вашей конкретной проблемы, я могу давать советы только с учетом того, что вы нам сообщили. Вы говорите, есть контейнер полиморфных базовых типов. Например:
std::vector<Base*> bases;
Если вы собираетесь использовать какой-либо из объектов, которые вы помещаете в этот контейнер специфичным для производного образом образом, то это не совсем контейнер базовых объектов, не так ли? Весь смысл наличия контейнера указателей на Base
Объекты в том, что вы можете перебирать их и рассматривать их как Base
объекты. Если вам нужно dynamic_cast
для некоторых Derived
типа, то вы злоупотребили полиморфным поведением Base*
s.
Если Derived
наследуется от Base
, затем Derived
это Base
, Проще говоря, если вы используете полиморфный Base
указатель на Derived
типа, то вы должны использовать только функции Derived
что делает его Base
,