直接调用函数 vs 发射信号 (Qt - 信号和槽)

3
此时我陷入了一个困境,不知道何时发出信号与直接调用另一个类中的方法(同一线程)之间的区别。例如,在我正在进行的教程中,我将仪器类(模型)的NotifyConnected信号连接到“this”即视图管理器的onConnected插槽中,请参阅SetupViewManager :: WireButtons()中的第三行代码。(我正在使用MVVM设计模式)。在这里,信号和插槽很有意义,因为仪器类(模型)不应该知道有关视图管理器的任何信息。(即向模型传递视图管理器的引用是不可取的,因为它会破坏MVVM设计模式。)太棒了。
我的问题是,在教程中的下一步中,ViewManager的onConnected插槽随后会发出其他信号,然后我必须手动连接到另一个View类(即SetupTab)的插槽中(请参阅代码中的void SetupViewManager :: onConnected和void SetupViewManager :: WireDisplayUpdate())。
我的问题是,为什么不直接用SetupTab的公共函数(信号)调用onConnected插槽中的所有发射?感觉对我来说这样过于复杂了。
采取额外措施发出信号并连接所有内容的优点是什么呢?从另一个我拥有引用的类中简单地调用公共函数(信号)而不必连接所有内容。这不是多线程应用程序(我知道信号和插槽是线程安全的)。
请为我解惑。
谢谢。
setupviewmanager.cpp:
#include "setupviewmanager.h"
#include "View/setuptab.h"
#include "Model/instrument.h"
#include "Model/settings.h"
#include "utils.h"

namespace Ps
{
    SetupViewManager::SetupViewManager(QObject *parent,
                                       SetupTab &tab,
                                       Instrument &inst,
                                       Settings &config) :
        QObject(parent),
        m_setupTab(tab),
        m_instrument(inst)
    {
        WireSettings(config);
        config.ParseJsonData();
        WireHostAndPort();
        WireMessages();
        WireButtons();
        WireDisplayUpdate();

        m_setupTab.SetHostName(config.getHostName());
        m_setupTab.SetPort(config.getPortNumber());
        m_setupTab.SetCommands(config.getCommandsAsModel());
        auto long_wait = config.getLongWaitMs();
        auto short_wait = config.getShortWaitMs();
        m_instrument.SetlongWaitMs(long_wait);
        m_instrument.SetShortWaitMs(short_wait);
        emit NotifyStatusUpdated(tr("Long wait Ms: %1").arg(long_wait));
        emit NotifyStatusUpdated(tr("Short Wait Ms: %1").arg(short_wait));
        onDisconnected();
    }

    SetupViewManager::~SetupViewManager()
    {
        Utils::DestructorMsg(this);
    }

    void SetupViewManager::WireSettings(Settings &config)
    {
        connect(&config, &Settings::NotifyStatusMessage, &m_setupTab, &SetupTab::onStatusUpdated);
    }

    void SetupViewManager::WireHostAndPort()
    {
        connect(&m_setupTab, &SetupTab::NotifyHostNameChanged, &m_instrument, &Instrument::onHostNameChanged);
        connect(&m_setupTab, &SetupTab::NotifyPortChanged, &m_instrument, &Instrument::onPortChanged);
    }

    void SetupViewManager::WireMessages()
    {
        connect(&m_instrument, &Instrument::NotifyErrorDetected, &m_setupTab, &SetupTab::onStatusUpdated);
        connect(&m_instrument, &Instrument::NotifyStatusUpdated, &m_setupTab, &SetupTab::onStatusUpdated);
        connect(this, &SetupViewManager::NotifyStatusUpdated, &m_setupTab, &SetupTab::onStatusUpdated);
    }

    void SetupViewManager::WireButtons()
    {
        connect(&m_setupTab, &SetupTab::NotifyConnectClicked,&m_instrument, &Instrument::Connect);
        connect(&m_instrument, &Instrument::NotifyConnected, &m_setupTab, &SetupTab::onConnected);
        connect(&m_instrument, &Instrument::NotifyConnected, this, &SetupViewManager::onConnected);

        connect(&m_setupTab, &SetupTab::NotifyDisconnectClicked,&m_instrument, &Instrument::Disconnect);
        connect(&m_instrument, &Instrument::NotifyDisconnected, &m_setupTab,&SetupTab::onDisconnected);
        connect(&m_instrument, &Instrument::NotifyDisconnected, this, &SetupViewManager::onDisconnected);

        connect(&m_setupTab, &SetupTab::NotifySendClicked,&m_instrument, &Instrument::onSendRequest);
        connect(&m_instrument, &Instrument::NotifyDataSent,&m_setupTab, &SetupTab::onDataSent);

        connect(&m_setupTab, &SetupTab::NotifyReceiveClicked,&m_instrument, &Instrument::onReceiveRequest);
        connect(&m_instrument, &Instrument::NotifyDataReceived,&m_setupTab, &SetupTab::onDataReceived);
    }

    void SetupViewManager::WireDisplayUpdate()
    {
       connect (this, &SetupViewManager::NotifyConnectEnabled, &m_setupTab, &SetupTab::onConnectEnabled);
       connect (this, &SetupViewManager::NotifyDisconnectEnabled, &m_setupTab, &SetupTab::onDisconnectEnabled);
       connect (this, &SetupViewManager::NotifyDirectCommandsEnabled, &m_setupTab, &SetupTab::onDirectCommandsEnabled);
       connect (this, &SetupViewManager::NotifyControlTabEnabled, &m_setupTab, &SetupTab::onControlTabEnabled);
    }

    void SetupViewManager::onConnected()
    {
        emit NotifyConnectEnabled(false); // HERE. Why not just call method directly with m_setupTab.onConnectEnabled(false); etc...?
        emit NotifyDisconnectEnabled(true);
        emit NotifyDirectCommandsEnabled(true);
        emit NotifyControlTabEnabled(true);
    }

    void SetupViewManager::onDisconnected()
    {
        emit NotifyConnectEnabled(true);
        emit NotifyDisconnectEnabled(false);
        emit NotifyDirectCommandsEnabled(false);
        emit NotifyControlTabEnabled(false);
    }
}

你能改变文本格式,使其更易于阅读吗?此外,一些代码可能会更清晰地解释问题。 - JefGli
你是否将视图设置为 ViewManager?难道不应该在那里建立这些连接吗?如果我们对这些类一无所知,那么很难判断。 - thuga
已按要求完成。请参考代码。谢谢! - Nokiaowner
那么你的意思是,在SetupViewManager::onConnected()中直接使用m_setupTab.functionName(param)调用Setuptab中的方法,而不是发射信号并连接信号/槽。是这样吗? - Nokiaowner
是的。这两种解决方案都可以。 - thuga
显示剩余6条评论
2个回答

3

信号槽机制的优点:

  • 当您的类没有关于其客户端的信息时,易于使用;
  • 可用于线程安全调用;
  • 您不必手动记住要通知的所有对象;
  • 连接两个对象的唯一规则是它们都必须是QObject子类。

缺点:

  • 较慢的调用(每个信号发射都要扫描所有连接的对象列表);
  • 可能会变得非常复杂,你不知道谁何时会调用任何槽或谁会获取发出的信号。

您应该根据自己的情况进行思考。如果SetupViewManager外部没有信号“监听器”,请尝试直接调用。如果其他人可以连接到此信号,则选择发出它们。

还可能有其他原因使用信号。但是,在至少一个线程中仅调用函数没有理由使用它们。


2
信号和槽用于解耦类,使它们不需要明确知道谁使用其功能以及如何使用。在许多情况下,解耦是软件设计中理想的特性。当然,这并不是一个终极目标,只有在它有助于您推断代码正确性并使其更易于维护时才有用。解耦有助于理解/推断代码,因为它导致了可以独立分析的较小代码单元。另一种看待它的方式是关注点分离:让一个代码单元只做一件事情,例如让一个类专注于一方面的功能。
当您有一对类并希望决定是否将它们耦合在一起时,请考虑它们是否可以与其他类一起使用。A 可以与 B 耦合,但将这对耦合的接口用于 C 而不是 B 是否可行?如果是这样,那么必须使用一些解耦模式,而信号槽模式就是其中之一。
例如,让我们比较这两个接口对用户代码的耦合程度。目标很简单:向对象的析构函数添加调试输出。
class QObject {
  ...
  Q_SIGNAL void destroyed(QObject * obj = Q_NULLPTR);
};

class QObjectB {
  ...
  virtual void on_destroyed();
};

int main() {
  QObject a;
  struct ObjectB : QObjectB {
    void on_destroyed() override { qDebug() << "~QObjectB"; }
  } b;
  QObject::connect(&a, &QObject::on_destroyed, []{ qDebug() << "~QObject"; });
}

信号槽接口允许您轻松地向现有对象添加功能,而无需对它们进行子类化。它是观察者模式的一个特别灵活的实现。这将使您的代码与对象的代码解耦。
第二种实现使用类似于模板方法的模式,强制更紧密的耦合:要在ObjectB销毁时执行操作,您必须拥有一个派生类的实例,在其中实现所需的功能。

很棒的答案。干杯 :) - Nokiaowner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接