Qt状态机框架

概述

状态机框架提供了用于创建和执行状态图的类。这些概念和符号基于Harel的Statecharts:复杂系统的可视化形式(http://www.wisdom.weizmann.ac.il/~dharel/SCANNED.PAPERS/Statecharts.pdf),也是UML状态图的基础。状态机执行的语义基于状态图XML (SCXML)(http://www.w3.org/TR/scxml/)。

状态图提供了一种图形化的方式来模拟系统对刺激的反应。这是通过定义系统可能处于的状态,以及系统如何从一个状态移动到另一个状态(状态之间的转换)来完成的。事件驱动系统(如Qt应用程序)的一个关键特征是,行为通常不仅取决于最近的或当前的事件,还取决于在它之前的事件。使用状态图,这些信息很容易表达。

状态机框架提供了一个API和执行模型,可用于在Qt应用程序中有效地嵌入状态图的元素和语义。该框架与Qt的元对象系统紧密集成;例如,状态之间的转换可以由信号触发,状态可以配置为在{QObject}上设置属性和调用方法。Qt的事件系统用于驱动状态机。

状态机框架中的状态图是分层的。状态可以嵌套在其他状态中,状态机的当前配置由当前活动的一组状态组成。状态机的有效配置中的所有状态都有一个共同的祖先

状态机框架中的类

这些类由qt提供,用于创建事件驱动的状态机。

QAbstractState

The base class of states of a QStateMachine

QAbstractTransition

The base class of transitions between QAbstractState objects

QEventTransition

QObject-specific transition for Qt events

QFinalState

Final state

QHistoryState

Means of returning to a previously active substate(返回到先前活跃的子状态的方法)

QKeyEventTransition

Transition for key events

QMouseEventTransition

Transition for mouse events

QSignalTransition

Transition based on a Qt signal

QState

通用状态 for QStateMachine

QStateMachine

Hierarchical finite state machine(层次有限状态机)

QStateMachine::SignalEvent

Represents a Qt signal event

QStateMachine::WrappedEvent

Inherits QEvent and holds a clone of an event associated with a QObject

一个简单的状态机
为了演示状态机API的核心功能,让我们看一个小示例:一个具有三种状态s1、s2和s3的状态机。状态机由单个QPushButton控制;当点击按钮时,机器切换到另一种状态。最初,状态机处于状态s1。这台机器的状态图如下:

面的代码片段显示了创建这样一个状态机所需的代码。首先,我们创建状态机和状态:

    QStateMachine machine;
    QState *s1 = new QState();
    QState *s2 = new QState();
    QState *s3 = new QState();

然后,我们使用QState::addTransition()函数创建转换:

    s1->addTransition(button, &QPushButton::clicked, s2);
    s2->addTransition(button, &QPushButton::clicked, s3);
    s3->addTransition(button, &QPushButton::clicked, s1);

接下来,我们为机器添加状态并设置机器的初始状态:

    machine.addState(s1);
    machine.addState(s2);
    machine.addState(s3);
    machine.setInitialState(s1);

最后,我们启动状态机:

machine.start();

状态机异步执行,也就是说,它成为应用程序事件循环的一部分。

在状态进入或者离开时处理事情

上面的状态机只是从一种状态转换到另一种状态,它不执行任何操作。QState::assignProperty()函数可用于在进入状态时设置一个QObject的属性。在下面的代码片段中,应该分配给QLabel的文本属性的值是为每个状态指定的:

    s1->assignProperty(label, "text", "In state s1");
    s2->assignProperty(label, "text", "In state s2");
    s3->assignProperty(label, "text", "In state s3");

当输入任何状态时,标签的文本将相应地更改。

进入状态时发出QState::entered()信号,退出状态时发出QState::exited()信号。在下面的代码片段中,按钮的showMaximized()插槽将在状态s3进入时被调用,按钮的showMinimized()插槽将在s3退出时被调用:

    QObject::connect(s3, &QState::entered, button, &QPushButton::showMaximized);
    QObject::connect(s3, &QState::exited, button, &QPushButton::showMinimized);

自定义状态可以重新实现QAbstractState::onEntry()和QAbstractState::onExit()。

结束状态机

上一节中定义的状态机永远不会结束。为了使状态机能够完成,它需要有一个顶级的最终状态(QFinalState对象)。当状态机进入顶级最终状态时,机器将发出qstatemmachine::finished()信号并停止。
要在图中引入最终状态,所需要做的就是创建一个QFinalState对象,并将其用作一个或多个转换的目标。

通过状态分组来实现共享转换

假设我们希望用户能够通过单击quit按钮随时退出应用程序。为了实现这一点,我们需要创建一个最终状态,并将其作为与Quit按钮的clicked()信号相关联的转换的目标。我们可以在s1 s2 s3中添加一个变换;然而,这似乎是多余的,并且还必须记住在将来添加的每个新状态中添加这样的转换。

通过对状态s1、s2和s3进行分组,我们可以实现相同的行为(即单击Quit按钮退出状态机,而不管状态机处于哪种状态)。这是通过创建一个新的顶级状态并将三个原始状态设置为新状态的子状态来实现的。下图显示了新的状态机。

最初的三个状态被重新命名为s11, s12和s13,以反映它们现在是新的顶级状态s1的孩子。子状态隐式继承父状态的转换。这意味着现在添加一个从s1到最终状态s2的转换就足够了。添加到s1的新状态也将自动继承此转换。

对状态进行分组所需要做的就是在创建状态时指定适当的父级。您还需要指定哪个子状态是初始状态(即,当父状态是转换的目标时,状态机应该进入哪个子状态)。

    QState *s1 = new QState();
    QState *s11 = new QState(s1);
    QState *s12 = new QState(s1);
    QState *s13 = new QState(s1);
    s1->setInitialState(s11);
    machine.addState(s1);
    QFinalState *s2 = new QFinalState();
    s1->addTransition(quitButton, &QPushButton::clicked, s2);
    machine.addState(s2);
    machine.setInitialState(s1);

    QObject::connect(&machine, &QStateMachine::finished,
                     QCoreApplication::instance(), &QCoreApplication::quit);

在本例中,我们希望应用程序在状态机结束时退出,因此机器的finished()信号连接到应用程序的quit()插槽。

子状态可以覆盖继承的转换。例如,下面的代码添加了一个转换,该转换有效地导致在状态机处于状态s12时忽略Quit按钮。

s12->addTransition(quitButton, &QPushButton::clicked, s12);

转换可以将任何状态作为其目标,即目标状态不必与源状态处于状态层次结构中的同一级别。

使用历史状态保存和恢复当前状态

想象一下,我们想要在上一节讨论的示例中添加一个“中断”机制;用户应该能够单击一个按钮,让状态机执行一些不相关的任务,之后状态机应该恢复它之前所做的任何事情(即返回到旧状态,在本例中是s11、s12和s13中的一个)。

这种行为可以很容易地使用历史状态进行建模。历史状态(QHistoryState对象)是一个伪状态,它表示父状态在父状态最后一次退出时所处的子状态。

历史状态被创建为我们希望记录当前子状态的状态的子状态;当状态机在运行时检测到存在这样的状态时,它会在父状态退出时自动记录当前(真正的)子状态。向历史状态的过渡实际上是向状态机先前保存的子状态的过渡;状态机自动将转换“转发”到真正的子状态。

下图显示了添加中断机制后的状态机。

下面的代码展示了它是如何实现的;在本例中,我们只是在输入s3时显示一个消息框,然后立即通过历史状态返回到s1的前一个子状态。

    QHistoryState *s1h = new QHistoryState(s1);

    QState *s3 = new QState();
    s3->assignProperty(label, "text", "In s3");
    QMessageBox *mbox = new QMessageBox(mainWindow);
    mbox->addButton(QMessageBox::Ok);
    mbox->setText("Interrupted!");
    mbox->setIcon(QMessageBox::Information);
    QObject::connect(s3, &QState::entered, mbox, &QMessageBox::exec);
    s3->addTransition(s1h);
    machine.addState(s3);

    s1->addTransition(interruptButton, &QPushButton::clicked, s3);

使用平行状态避免状态的组合爆炸

假设您想在单个状态机中为一辆汽车的一组互斥属性建模。假设我们感兴趣的属性是干净与脏,移动与不移动。它需要四个相互排斥的状态和八个转换才能表示并在所有可能的组合之间自由移动。

如果我们添加第三个属性(如红色vs蓝色),那么状态总数将翻倍,达到8个;如果我们再加上第四个属性(比如,封闭的vs可转换的),状态的总数将再次翻倍,达到16个。
使用并行状态,随着我们添加更多属性,状态和转换的总数呈线性增长,而不是呈指数增长。此外,可以将状态添加到并行状态或从并行状态中删除,而不会影响它们的任何兄弟状态。

要创建并行状态组,将QState::ParallelStates传递给QState构造函数。

    QState *s1 = new QState(QState::ParallelStates);
    // s11 and s12 will be entered in parallel
    QState *s11 = new QState(s1);
    QState *s12 = new QState(s1);

当进入一个并行状态组时,将同时进入它的所有子状态。各个子状态中的转换正常运行。然而,任何一个子状态都可以进行退出父状态的转换。当这种情况发生时,父状态及其所有子状态都将退出。

状态机框架中的并行性遵循交错语义。所有并行操作都将在事件处理的单个原子步骤中执行,因此没有任何事件可以中断并行操作。但是,事件仍将按顺序处理,因为机器本身是单线程的。举个例子:考虑这样一种情况:有两个转换退出同一个并行状态组,并且它们的条件同时为真。在这种情况下,两个事件中最后处理的事件将不会产生任何影响,因为第一个事件已经导致机器从并行状态退出。

检测一种复合状态是否已结束

子状态可能已经final(一个QFinalState对象);当进入最后一个子状态时,父状态发出QState::finished()信号。下图显示了一个复合状态s1,它在进入最终状态之前做了一些处理:

当进入s1的最终状态时,s1将自动发出finished()。我们使用一个信号转换来触发这个事件来触发状态改变:

s1->addTransition(s1, &QState::finished, s2);

当你想隐藏复合状态的内部细节时,在复合状态中使用最终状态是很有用的;也就是说,外部世界唯一能做的就是进入状态,并在状态完成其工作时获得通知。在构建复杂的(深度嵌套的)状态机时,这是一种非常强大的抽象和封装机制。(在上面的示例中,您当然可以直接从s1的done状态创建转换,而不是依赖s1的finished()信号,但结果是暴露并依赖s1的实现细节)。

对于并行状态组,QState::finished()信号在所有子状态进入最终状态时发出。

无目标转换

转换不需要有目标状态。没有目标的转换可以像其他任何转换一样触发;不同之处在于,当触发无目标转换时,它不会导致任何状态更改。这允许您在机器处于某种状态时对信号或事件作出反应,而不必离开该状态。例子:

QStateMachine machine;
QState *s1 = new QState(&machine);

QPushButton button;
QSignalTransition *trans = new QSignalTransition(&button, &QPushButton::clicked);
s1->addTransition(trans);

QMessageBox msgBox;
msgBox.setText("The button was clicked; carry on.");
QObject::connect(trans, QSignalTransition::triggered, &msgBox, &QMessageBox::exec);

machine.setInitialState(s1);

每次单击按钮时都会显示消息框,但状态机将保持其当前状态(s1)。但是,如果将目标状态显式地设置(setTargetState)为s1,则每次都会退出并重新进入s1(例如,会发出QAbstractState::entered()和QAbstractState::exit()信号)。

通过事件触发转换

qstatemmachine运行自己的事件循环。对于信号转换(QSignalTransition对象),当QStateMachine拦截相应的信号时,它会自动向自己发送一个QStateMachine::SignalEvent;类似地,对于QObject事件转换(QEventTransition对象),发送QStateMachine::WrappedEvent。

您可以使用qstatemmachine::postEvent()将您自己的事件发布到状态机。

在向状态机发布自定义事件时,通常还可以从该类型的事件触发一个或多个自定义转换。要创建这样的转换,您可以子类化QAbstractTransition并重新实现QAbstractTransition::eventTest(),在这里您可以检查事件是否与您的事件类型匹配(以及可选的其他标准,例如事件对象的属性)。

这里我们定义了自己的自定义事件类型StringEvent,用于向状态机发送字符串:

struct StringEvent : public QEvent
{
    StringEvent(const QString &val)
    : QEvent(QEvent::Type(QEvent::User+1)),
      value(val) {}

    QString value;
};

接下来,我们定义一个只在事件字符串匹配特定字符串时触发的转换(一个受保护的转换):

class StringTransition : public QAbstractTransition
{
    Q_OBJECT

public:
    StringTransition(const QString &value)
        : m_value(value) {}

protected:
    bool eventTest(QEvent *e) override
    {
        if (e->type() != QEvent::Type(QEvent::User+1)) // StringEvent
            return false;
        StringEvent *se = static_cast<StringEvent*>(e);
        return (m_value == se->value);
    }

    void onTransition(QEvent *) override {}

private:
    QString m_value;
};

在eventTest()重新实现中,我们首先检查事件类型是否为所需类型;如果是,则将事件强制转换为StringEvent并执行字符串比较。
下面是一个使用自定义事件和转换的状态图:

下面是状态图的实现:

    QStateMachine machine;
    QState *s1 = new QState();
    QState *s2 = new QState();
    QFinalState *done = new QFinalState();

    StringTransition *t1 = new StringTransition("Hello");
    t1->setTargetState(s2);
    s1->addTransition(t1);
    StringTransition *t2 = new StringTransition("world");
    t2->setTargetState(done);
    s2->addTransition(t2);

    machine.addState(s1);
    machine.addState(s2);
    machine.addState(done);
    machine.setInitialState(s1);

一旦机器启动,我们就可以向它发布事件。

    machine.postEvent(new StringEvent("Hello"));
    machine.postEvent(new StringEvent("world"));

未由任何相关转换处理的事件将被状态机静默地使用。对状态进行分组并提供此类事件的默认处理可能很有用;例如,如下图所示:

对于深度嵌套的状态图,您可以在最合适的粒度级别上添加这种“回退”转换。

使用恢复策略自动恢复属性

在一些状态机中,将注意力集中在分配状态属性上,而不是在状态不再活动时恢复状态,这可能会很有用。如果您知道,当机器进入没有显式地为属性赋值的状态时,属性应该始终恢复到其初始值,那么您可以将全局恢复策略设置为qstatemmachine::RestoreProperties。

QStateMachine machine;
machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

设置此恢复策略后,机器将自动恢复所有属性。如果它进入未设置给定属性的状态,它将首先搜索祖先的层次结构,以查看是否在那里定义了该属性。如果是,则属性将恢复为最近的祖先所定义的值。如果不是,它将恢复到其初始值(即在执行任何状态的属性分配之前的属性值)。
以以下代码为例:

    QStateMachine machine;
    machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

    QState *s1 = new QState();
    s1->assignProperty(object, "fooBar", 1.0);
    machine.addState(s1);
    machine.setInitialState(s1);

    QState *s2 = new QState();
    machine.addState(s2);

假设机器启动时属性fooBar是0.0。当机器处于状态s1时,属性将为1.0,因为状态显式地将此值赋给它。当机器处于状态s2时,没有显式地为属性定义值,因此它将隐式地恢复为0.0。
如果我们使用嵌套状态,父类会为该属性定义一个值,所有没有显式赋值给该属性的后代都会继承这个值

    QStateMachine machine;
    machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

    QState *s1 = new QState();
    s1->assignProperty(object, "fooBar", 1.0);
    machine.addState(s1);
    machine.setInitialState(s1);

    QState *s2 = new QState(s1);
    s2->assignProperty(object, "fooBar", 2.0);
    s1->setInitialState(s2);

    QState *s3 = new QState(s1);

这里s1有两个子节点:s2和s3。当输入s2时,属性fooBar的值将为2.0,因为这是为状态显式定义的。当机器处于状态s3时,没有为状态定义值,但是s1将属性定义为1.0,因此这是将分配给fooBar的值

动画属性分配

状态机API与Qt中的动画API连接,允许在状态中分配属性时自动动画化。
假设我们有以下代码:

    QState *s1 = new QState();
    QState *s2 = new QState();

    s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));
    s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100));

    s1->addTransition(button, &QPushButton::clicked, s2);

这里我们定义了用户界面的两种状态。在s1中,按钮很小,而在s2中,按钮更大。如果我们点击按钮从s1转换到s2,按钮的几何形状将在进入给定状态后立即设置。但是,如果我们希望过渡平滑,我们所需要做的就是创建一个QPropertyAnimation并将其添加到过渡对象中。

    QState *s1 = new QState();
    QState *s2 = new QState();

    s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));
    s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100));

    QSignalTransition *transition = s1->addTransition(button, &QPushButton::clicked, s2);
    transition->addAnimation(new QPropertyAnimation(button, "geometry"));

为有问题的属性添加动画意味着当进入状态时,属性分配将不再立即生效。相反,动画将在进入状态时开始播放,并平滑地为属性分配动画。因为我们没有设置动画的开始值或结束值,这些将隐式设置。动画的开始值将是动画开始时属性的当前值,结束值将根据为状态定义的属性分配来设置。

如果将状态机的全局恢复策略设置为qstatemmachine::RestoreProperties,则还可以为属性恢复添加动画。

检测到所有属性都已设置为某种状态

当使用动画来分配属性时,状态不再定义机器处于给定状态时属性将具有的确切值。当动画运行时,该属性可能有任何值,这取决于动画。

在某些情况下,能够检测何时为属性分配了由状态定义的值是很有用的。

假设我们有以下代码:

    QMessageBox *messageBox = new QMessageBox(mainWindow);
    messageBox->addButton(QMessageBox::Ok);
    messageBox->setText("Button geometry has been set!");
    messageBox->setIcon(QMessageBox::Information);

    QState *s1 = new QState();

    QState *s2 = new QState();
    s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));
    connect(s2, &QState::entered, messageBox, SLOT(exec()));

    s1->addTransition(button, &QPushButton::clicked, s2);

当点击按钮时,机器将切换到状态s2,该状态将设置按钮的几何形状,然后弹出消息框提醒用户几何形状已更改。

在不使用动画的正常情况下,这将按预期操作。但是,如果在s1和s2之间的转换上设置了按钮几何形状的动画,则动画将在输入s2时开始,但是在动画完成运行之前,几何形状属性实际上不会达到其定义的值。在这种情况下,消息框将在按钮的几何形状实际设置之前弹出。

为了确保消息框在几何图形实际达到最终值之前不会弹出,我们可以使用状态的propertiesAssigned()信号。propertiesAssigned()信号将在属性被赋予最终值时发出,无论这是立即执行还是在动画播放完成后执行。

    QMessageBox *messageBox = new QMessageBox(mainWindow);
    messageBox->addButton(QMessageBox::Ok);
    messageBox->setText("Button geometry has been set!");
    messageBox->setIcon(QMessageBox::Information);

    QState *s1 = new QState();

    QState *s2 = new QState();
    s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));

    QState *s3 = new QState();
    connect(s3, &QState::entered, messageBox, SLOT(exec()));

    s1->addTransition(button, &QPushButton::clicked, s2);
    s2->addTransition(s2, &QState::propertiesAssigned, s3);

在这个例子中,当点击按钮时,机器将进入s2。它将保持在状态s2,直到几何属性被设置为QRect(0,0,50,50)。然后它会变成s3。当输入s3时,会弹出消息框。如果转换到s2有一个几何属性的动画,那么机器将停留在s2,直到动画完成播放。如果没有这样的动画,它将简单地设置属性并立即进入状态s3。

无论哪种方式,当机器处于状态s3时,您都可以保证属性geometry已被分配了定义的值。

如果将全局恢复策略设置为qstatemmachine::RestoreProperties,则状态将不会发出propertiesAssigned()信号,直到这些也被执行。

如果在动画完成之前退出状态会发生什么

如果一个状态有属性赋值,并且转换到该状态时有属性的动画,那么在属性赋值给该状态定义的值之前,可能会退出该状态。当存在不依赖于propertiesAssigned()信号的状态转换时尤其如此,如前一节所述。

状态机API保证由状态机分配的属性:

  • 具有显式分配给属性的值。
  • 当前被动画化为显式分配给属性的值。

如果在动画完成之前退出状态,则状态机的行为取决于转换的目标状态。如果目标状态显式地为属性赋值,则不会采取任何其他操作。将为该属性分配由目标状态定义的值。

如果目标状态没有给属性赋任何值,有两种选择:默认情况下,属性将被赋值为它离开的状态所定义的值(如果动画被允许完成播放,它将被赋值)。但是,如果设置了全局恢复策略,则将优先考虑全局恢复策略,并且将像往常一样恢复属性。

默认的动画

如前所述,您可以向过渡添加动画,以确保目标状态中的属性分配是动画的。如果你想要一个特定的动画用于一个给定的属性,而不管哪个转换被采用,你可以将它作为默认动画添加到状态机。当构造机器时,不知道由特定状态分配(或恢复)的属性时,这特别有用。

QState *s1 = new QState();
QState *s2 = new QState();

s2->assignProperty(object, "fooBar", 2.0);
s1->addTransition(s2);

QStateMachine machine;
machine.setInitialState(s1);
machine.addDefaultAnimation(new QPropertyAnimation(object, "fooBar"));

嵌套状态机

QStateMachine是QState的子类。这允许一个状态机成为另一个机器的子状态。QStateMachine重新实现了QState::onEntry()并调用了QStateMachine::start(),这样当进入子状态机时,它将自动开始运行。

父状态机将子状态机视为状态机算法中的原子状态。子状态机是自包含的;它维护自己的事件队列和配置。特别要注意的是,子机器的配置()不是父机器配置的一部分(只有子机器本身是)。

子状态机的状态不能指定为父状态机中转换的目标;只有子状态机本身可以。相反,不能将父状态机的状态指定为子状态机中转换的目标。子状态机的finished()信号可用于触发父状态机中的转换。

The State Machine Framework | Qt Core 5.15.17

qml:The Declarative State Machine Framework | Qt QML 5.15.17

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/714305.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

消息队列-分布式消息队列技术选型

Kafka Kafka 是 LinkedIn 开源的一个分布式流式处理平台&#xff0c;已经成为 Apache 顶级项目&#xff0c;早期被用来用于处理海量的日志&#xff0c;后面才慢慢发展成了一款功能全面的高性能消息队列。 流式处理平台具有三个关键功能&#xff1a; 消息队列&#xff1a;发布和…

SQLServer使用 PIVOT 和 UNPIVOT行列转换

在SQL Server中&#xff0c;PIVOT是一个用于将行数据转换为列数据的操作。它特别适用于将多个行中的值转换为多个列的情况&#xff0c;并在此过程中执行聚合操作。以下是关于SQL Server中PIVOT操作的详细解释和示例&#xff1a; 1、本文内容 概述语法备注关键点简单 PIVOT 示…

Linux命令2

文章目录 移动文件或目录mv格式 查找命令/文件存放位目录置which格式 查找文件或目录find格式查找类型多个查找条件逻辑运算符 移动文件或目录 mv 将文件或者目录移动到指定的位置 如果目标的位置和源位置相同&#xff0c;相当于改名操作 跨目录移动相当于window的剪切 格式…

MacOS之解决:开盖启动问题(七十四)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

2024.6.16周报

目录 摘要 ABSTRACT 一、文献阅读 一、题目 二、摘要 三、创新点 四、模型架构 五、文章解读 1、Introduction 2、实验 3、结论 二、代码复现 1、模型代码 2、实验结果 三、总结 摘要 本周我阅读了一篇题目为《Contaminant Transport Modeling and Source Att…

工厂方法模式实战之某商场一次促销活动

目录 1.5.1、前言1.5.2、实战场景简介1.5.3、开发环境1.5.4、用传统的if-else语句实现1.5.4.1、工程结构1.5.4.2、if-else需求实现1.5.4.3、测试验证 1.5.5、工厂模式优化代码1.5.5.1、工程结构1.5.5.2、代码实现1.5.5.2.1、定义各种商品发放接口及接口实现1.5.5.2.2、定义工厂…

项目经理,请勇敢Say No~

为什么要say no&#xff1f; 培养say no的勇气 优雅的say no&#xff01; say no 三部曲&#xff0c;项目经理&#xff0c;你准备好了吗&#xff1f; 为什么要say no&#xff1f; 保护项目完整性的屏障 项目管理的核心在于平衡时间、成本与质量三大要素&#xff0c;任何一项的…

STL——set、map、multiset、multimap的介绍及使用

文章目录 关联式容器键值对树形结构与哈希结构setset的介绍set的使用set的模板参数列表set的构造set的使用set的迭代器使用演示 multisetmultiset演示 mapmap的定义方式map的插入map的查找map的[ ]运算符重载map的迭代器遍历multimapmultimap的介绍multimap的使用 在OJ中的使用…

全球“抱团”美股,美股“抱团”AI

内容提要 过去一个月内&#xff0c;全球约有300亿美元新资金流入股票基金&#xff0c;其中高达94%投向了美国资产&#xff1b;一季度&#xff0c;海外投资者购入了1870亿美元美国公司债券&#xff0c;同比增长61%。 文章正文 尽管美国面临债务问题和大选带来的政治分歧&#…

索引-定义、创建(CREATE INDEX)、删除(DROP INDEX)

一、概述 1、索引是SQL语言定义的一种数据对象&#xff0c;是大多数DBMS为数据库中基本表创建的一种辅助存取结构&#xff0c;用于响应特定查询条件进行查询时的查询速度&#xff0c;DBMS根据查询条件从数据库文件中&#xff0c;选择出一条或者多条数据记录以供检索&#xff0…

【JS重点17】原型继承

目录 一&#xff1a;什么是原型继承 二&#xff1a;通过赋值方式实现原型继承 三&#xff1a;通过构造函数实现原型继承 四&#xff1a;如何赚钱 一&#xff1a;什么是原型继承 通过往构造函数上的原型对象添加属性和方法&#xff0c;再new一个实例对象&#xff0c;从而实例…

18. 第十八章 继承

18. 继承 和面向对象编程最常相关的语言特性就是继承(inheritance). 继承值得是根据一个现有的类型, 定义一个修改版本的新类的能力. 本章中我会使用几个类来表达扑克牌, 牌组以及扑克牌性, 用于展示继承特性.如果你不玩扑克, 可以在http://wikipedia.org/wiki/Poker里阅读相关…

CSS期末复习速览(二)

1.元素显示模式分为三种&#xff1a;块元素&#xff0c;行内元素&#xff0c;行内块元素 2.块元素&#xff1a;常见的块元素&#xff1a;<h1>~<h6> <p> <div> <ul> <ol> <li>&#xff0c;特点&#xff1a;自己独占一行&a…

需求:如何给文件添加水印

今天给大家介绍一个简单易用的水印添加框架&#xff0c;框架抽象了各个文件类型的对于水印添加的方法。仅使用几行代码即可为不同类型的文件添加相同样式的水印。 如果你有给PDF、图片添加水印的需求&#xff0c;EasyWatermark是一个很好的选择&#xff0c;主要功能就是传入一…

嵌入式实训day5

1、 from machine import Pin import time # 定义按键引脚控制对象 key1 Pin(27,Pin.IN, Pin.PULL UP) key2 Pin(26,Pin.IN, Pin.PULL UP)led1 Pin(15,Pin.ouT, value0) led2 Pin(2,Pin.ouT, value0) led3 Pin(0,Pin.ouT, value0) # 定义key1按键中断处理函数 def key1 ir…

2.线上论坛项目

一、项目介绍 线上论坛 相关技术&#xff1a;SpringBootSpringMvcMybatisMysqlSwagger项目简介&#xff1a;本项目是一个功能丰富的线上论坛&#xff0c;用户可编辑、发布、删除帖子&#xff0c;并评论、点赞。帖子按版块分类&#xff0c;方便查找。同时&#xff0c;用户可以…

【CT】LeetCode手撕—121. 买卖股票的最佳时机

目录 题目1- 思路2- 实现⭐121. 买卖股票的最佳时机——题解思路 2- ACM实现 题目 原题连接&#xff1a;121. 买卖股票的最佳时机 1- 思路 模式识别 模式1&#xff1a;只能某一天买入 ——> 买卖一次 ——> dp 一次的最大利润 动规五部曲 1.定义dp数组&#xff0c;确…

跻身中国市场前三,联想服务器的“智变”与“质变”

IDC发布的《2024年第一季度中国x86服务器市场报告》显示&#xff0c;联想服务销售额同比增长200.2%&#xff0c;在前十厂商中同比增速第一&#xff0c;并跻身中国市场前三&#xff0c;迈入算力基础设施“第一阵营”。 十年砺剑联想梦&#xff0c;三甲登榜领风骚。探究联想服务器…

IDEA模版快速生成Java方法体

新建模版组myLive 在模版组下新建模版finit 在模版text内输入以下脚本 LOGGER.info("$className$.$methodName$>$parmas1$", $parmas2$); try {} catch (Exception e) {LOGGER.error("$className$.$methodName$>error:", e); }LOGGER.info("$c…

redis未授权到getshell

0 前言 现在是redis数据库未授权访问到getshell的部分了,不好意思&#xff0c;因为个人原因&#xff0c;和上篇mysql的getshell文章间隔较久. 1 漏洞产生原因 redis安装完之后&#xff0c;默认情况下绑定在 0.0.0.0:6379&#xff0c;且没有对登录IP做限制&#xff0c;并且没…