本文是 《基于 Qt 实现消息总线》的其中一节,建议全章阅读。
弱类型能够帮助我们进一步减少组件之间通信的耦合性,特别在编译链接阶段,我们并不需要包含(include)定义消息的头文件,也不需要链接(link)实现消息的链接库。
在有些场景下,弱类型是必须的。比如在浏览器 js 中对接消息总线,就不可能包含头文件。
弱类型消息
对于弱类型消息,除了消息类型是 QVariant 外,基本的订阅、发布实现与强类型消息是一样的,都是基于 Qt 的信号-槽机制。
强类型消息通过消息的类型来关联发布和订阅,弱类型消息则必须有一个规定好的主题(字符串类型)。所以弱类型消息订阅、发布都会带上一个主题信息。
class QSimpleMessage : public QMessageBase
{
public:
QSimpleMessage(QByteArray const & topic);
typedef std::function<QVariant (QByteArray const & topic, QVariant const & message)> observ_t;
virtual bool subscribe(QObject const * c, observ_t o);
virtual bool unsubscribe(QObject const * c);
virtual void publish(QVariant const & msg);
};
弱化强类型消息
强类型消息也可以作为弱类型消息使用,只要满足下列条件:
- 必须提前定义好一个主题。
- 需要准备强弱类型之间的转换。这是通过 QMetaType 来实现的。
作为一个例子,有这样一个消息结构:
struct TestMessage
{
int i;
int j;
};
首先,需要在 QMetaType 类型系统中通过 Q_DECLARE_METATYPE 注册这个类型:
Q_DECLARE_METATYPE(TestMessage)
有了上面的注册,我们就可以用 QVariant 封装,或者从 QVariant 解封装 TestMessage 对象:
template<typename F>
static QVariant QMessage<T>::toVar(T const & msg)
{
return QVariant::fromValue(msg);
}
template<typename F>
static T QMessage<T>::fromVar(T const & msg)
{
return msg.value<T>();
}
但是,要作为弱类型使用,还需要定义并注册与弱类型之间的转换函数。弱类型可以选择 QVariantList 或者 QVariantMap 或者其他,具体用什么视情况而定,也可能需要同时注册多种类型的转换。作为一个例子,TestMessage 与 QVariantList 的转换可以这样实现(同时演示了注册方法 registerConverter):
QMetaType::registerConverter<TestMessage, QVariantList>([](auto & m) {
return QVariantList{m.i, m.j};
});
QMetaType::registerConverter<QVariantList, TestMessage>([](auto & l) {
return TestMessage{l[0].toInt(), l[1].toInt()};
});
这样,我们在发布 TestMessage 消息时,可以用下面这种弱类型的方式(主题为 "test"):
bus.publish("test", QVariantList{1, 2});
在订阅消息时,也可以用下面这样的弱类型方式:
bus.subscribe("test", [](QByteArray topic, QVariant message) {
qDebug() << "test" << topic << message.toList();
});
在上面两种场合下,都不需要强依赖 TestMessage 类型(对编译器是不可见的)。
绑定弱类型消息
处理弱类型总不是那么方便,通常将弱类型消息转换为 QVariantList,然后拆解为接收方法的一系列参数。我们用一个工具类(QSubscriber)来实现这个过程。先看看如何使用:
class TestReceiver : public QObject
{
public slots:
QString test(int i, QString s)
{
qDebug() << "TestReceiver" << i << s;
return s + i;
}
};
TestReceiver receiver;
bus.subscribe("test_topic", QSubscriber(&receiver, SLOT(test(int,QString))));
bus.subscribe("test_topic", QSubscriber(&receiver, &TestReceiver::test));
在这个例子里面,TestMessage 的两个 int 属性被拆解为接收方法的两个参数,一个是 int 类型,一个是 QString 类型,工具类可以自动完成类型转换。
这是如何实现的呢?
我们先看看 SLOT 方式的实现(省略了部分中间过程):
QVariant QSubscriber::invoke(QObject *receiver, const QByteArray &target, QVariant args)
{
QMetaObject const & meta = *receiver->metaObject();
int index = meta.indexOfSlot(target);
if (index < 0) {
return QVariant();
}
QMetaMethod method = meta.method(index);
if (method.parameterCount() >= 4)
return QVariant();
QGenericArgument argv[4];
QVariant varg[4];
QVariantList list;
if (args.canConvert(qMetaTypeId<QVariantList>())
&& args.convert(qMetaTypeId<QVariantList>())) {
list = args.toList();
} else {
list.append(args);
}
for (int i = 0; i < method.parameterCount(); ++i) {
if (i < list.size())
varg[i] = list[i];
int t = method.parameterType(i);
if (!varg[i].canConvert(t))
return QVariant();
if (!varg[i].convert(t))
return QVariant();
argv[i] = QGenericArgument(QMetaType::typeName(t), varg[i].data());
}
QVariant result;
QGenericReturnArgument rtarg(QMetaType::typeName(method.returnType()), result.data());
method.invoke(receiver, rtarg, argv[0], argv[1], argv[2], argv[3]);
return result;
}
可以看出,关键部分是 QMetaMethod 和 QVariant 的使用。通过 QMetaMethod 可以反射出方法的所有参数类型,然后将 QVariantList 的每一项(QVariant)转换为对应参数的类型,再调用 QMetaMethod::invoke 就可以跳转到处理消息的 SLOT 了。
至于泛型函数 Func 形式的实现,与 SLOT 方式的区别在于,它是通过 C++ 泛型模板参数获得函数的各个参数类型:
int t = qMetaTypeId<Arg>();
if (!arg.canConvert(t))
return false;
if (!arg.convert(t))
return false;
C++泛型处理是比较复杂的,我们不准备详细分析,有兴趣的读者可以阅读源代码。
至此,我们完成了弱类型的大部分处理逻辑,唯一缺少的是弱类型的结果反馈,这将在下一章节中介绍。