反射作用
反射的作用就是可以通过字符串创建对象,操作类的成员数据,调用类的方法。
举一个场景例子:
实现一个对象工厂,工厂有个函数叫QObject* createObject(QString className);传递一个className,返回一个该类的对象。
如果没有反射:大概率是这样写
cpp
QObject* createObject(QString className){
if(className == "A"){
return new A();
}
else if(className == "B"){
return new B();
}
//......
}
这里就不是根据字符串创建对象。因为我们把A(),B()明确地写在代码里了。当有类新增时,我们需要添加新的else if分支。
如果有反射:就可以这样写
cpp
QObject* createObject(QString className){
MetaObject o = MataObject::fromName(className);
return o.newInstance();
}
这里,新增类时,工厂的代码不需要任何改动。
通过字符串新建对象意味着可以运行时才确定创建什么对象。因为字符串可以在运行时才确定,他可能来自用户的输入。没有反射,就只能在工厂里用if else 枚举所有可能的类了。
另一种用途就是可以遍历某个类所有成员变量的名字。这些成员变量的名字可能能用作其他用途,比如在运行时判断一个对象有没有名为xxx的成员变量或者成员函数。比如他们的名字刚好就是对应某个sql语句的数据字段。
QT中的反射
QT的反射是通过MOC编译器进行的,MOC编译器把QT的特殊语法转化为标准编译器认识的C++语法,并添加一些函数实现,从而支持反射。与java的反射不同。java的反射不需要程序员做任何事情就能使用反射,QT的反射则需要程序员给需要被反射的类多写几行代码才能完成。下面的段落会介绍反射类,反射字段,反射方法需要程序员多做哪些事情。
反射字段
反射字段就是通过字符串修改或者访问某个对象的字段,这个字段可以是public,private,protect的。一个字段能被反射,需要具备如下条件。
- 所属类直接或者间接继承自QObject。
- 类有Q_OBJECT宏定义。
- 有Q_PROPERTY指向。
举例如下:
cpp
class Student : public QObject
{
Q_OBJECT
Q_PROPERTY(QString mName MEMBER mName)
public:
explicit Student(QObject *parent = nullptr);
QString mName; //可以被反射,因为有Q_PROPERTY指向。(第3行)
QString mSex; //不能被反射,因为没有Q_PROPERTY指向。
};
其中mName可以被反射,但mSex不能被反射。
使用反射字段
cpp
Student stu;
stu.setProperty("mName","张三");
stu.setProperty("mSex","男");
qDebug() << stu.mName; //输出张三;
qDebug() << stu.mSex; //输出空字符串。因为mSex字段没有被反射。
qDebug() << stu.property("mSex").toString(); //输出男,这里是因为动态地给stu这个对象添加了一个字段mSex,但是它和原本的mSex没有任何关系。
stu.mName = "张三变身";
qDebug() << stu.property("mName").toString(); //输出张三变身。
stu.mSex = "女";
qDebug() << stu.property("mSex").toString(); //输出男,之前通过反射添加的字段,并写了男。这里获取的是反射添加的字段的值。
请注意:
Q_PROPERTY(QString mName MEMBER mName)
中,第一个mName是元对象系统中的名字, 第二个才是字段。这个语句把元对象系统中的mName和成员变量进行了关联。 他们名字可以不一样。如果不一样,比如我一开始不小心把第一个mName,打成了nName。结果
cpp
stu.setProperty("mName","张三");
qDebug() << stu.mName; //输出空。
stu.setProperty("nName","张三"); //注意这里是nName。
qDebug() << stu.mName; //输出张三。 //这里是mName。
反射自定义类型字段
QT的大部分类型都能反射。如果要反射自定义类型,这个自定义类型必须满足如下条件:
- 有拷贝构造函数
- 有赋值号运算符重载
- 有!=(本类型)运算符重载
- Q_DECLARE_METATYPE() 申明
第一个和第二个可以是默认的。但有时编译器不一定能生成拷贝构造函数或者赋值号构造函数。典型的如继承了QObject的类。因为QObject delete了拷贝构造函数。所以编译器不会为它的派生类生成默认的拷贝构造函数。
举例如下:
有一个自定义类
cpp
class Teacher
{
public:
explicit Teacher(QObject *parent = nullptr);
QString mName = "王老师";
bool operator!=(const Teacher& other);
signals:
};
Q_DECLARE_METATYPE(Teacher)
学生中多了一个字段mTeacher。这个字段的类型是Teacher
cpp
class Student : public QObject
{
Q_OBJECT
Q_PROPERTY(QString mName MEMBER mName)
Q_PROPERTY(Teacher mTeacher MEMBER mTeacher)
public:
explicit Student(QObject *parent = nullptr);
QString mName;
QString mSex;
Teacher mTeacher;
};
因为Teacher满足了4个条件。(默认的拷贝构造函数,默认的=重载,以及自己写的!=重载 ,Q_DECLARE_METATYPE(Teacher) 申明 )。
所以mTeacher字段可以反射(注意还要有Q_PROPERTY(Teacher mTeacher MEMBER mTeacher))。
其使用起来如下
cpp
Student stu;
qDebug() << stu.property("mTeacher").value<Teacher>().mName; //输出王老师 //通过反射得到的字段类型是QVariant,可以通过value模板函数把这个QVariant转化为真实的类型。
Teacher otherTeacher;
otherTeacher.mName = "李老师";
stu.setProperty("mTeacher",QVariant::fromValue<Teacher>(otherTeacher));
qDebug() << stu.property("mTeacher").value<Teacher>().mName; //输出李老师 //通过反射得到的字段类型是QVariant,可以通过value模板函数把这个QVariant转化为真实的类型。
还有一点需要提的是,反射字段(stu.property(mName))得到的字段对象是复制来的,也就是说修改反射得来的字段并不会影响原字段。要修改原字段,必须使用setproperty。这将导致深复制。这一点挺糟糕的。
拓展:qRegisterMetaType<Teacher>("Teacher");
谈到了Q_DECLARE_METATYPE ,顺便了聊聊qRegisterMetaType 吧。
前者是编译器发挥作用的,后者是运行时发挥作用的。
前者主要解决QVariant 不认识自定义类的问题。有了这个宏, moc就能在QVariant的模板函数中加入这个自定义类的特化。使得编译能过。
后者主要是在运行时把一个键值对加入到一个映射表中。键值对的键是字符串"Teacher"。值是类型Teacher的元对象。这样,qt才能通过字符串创建对象。也就是反射类。后面会说。
反射方法
反射方法就是通过字符串调用一个对象的方法,可以是public,private或者protect。被反射的方法需要具备如下条件。
- 类直接或间接继承自QObject
- 类有O_OBJECT宏定义
- 方法有Q_INVOKABLE 修饰,或者是slots,signals。
举例:
添加了一个showName方法。这是一个无参无返回值的方法。
添加了一个setName方法,这是一个有参数有返回值的方法。
cpp
class Student : public QObject
{
Q_OBJECT
Q_PROPERTY(QString mName MEMBER mName)
Q_PROPERTY(Teacher mTeacher MEMBER mTeacher)
public:
Q_INVOKABLE explicit Student(QObject *parent = nullptr);
QString mName;
Q_INVOKABLE bool setName(QString name);
Q_INVOKABLE void showName();
QString mSex;
Teacher mTeacher;
};
void Student::showName()
{
qDebug()<< "my name is "<<mName;
}
bool Student::setName(QString name)
{
qDebug()<< "set name " << name;
mName = name;
return true;
}
调用
cpp
Student stu;
bool result = false;
stu.metaObject()->invokeMethod(&stu,"setName",Qt::DirectConnection,Q_RETURN_ARG(bool,result),Q_ARG(QString,"张三"));
qDebug() << result; //输出true。
stu.metaObject()->invokeMethod(&stu,"showName");
反射类
反射类是通过字符串构建某个类的对象。一个类可以被反射,它必须满足以下条件。
- 类直接或间接继承自QObject
- 类有O_OBJECT宏定义
- 构造函数可以被反射,且是public。
举例:
cpp
class Student : public QObject
{
Q_OBJECT
Student(const Student& other) = delete;
Q_PROPERTY(QString mName MEMBER mName)
//Q_PROPERTY(Teacher mTeacher MEMBER mTeacher)
public:
Q_INVOKABLE explicit Student(QObject *parent = nullptr);
QString mName;
Q_INVOKABLE bool setName(QString name);
Q_INVOKABLE void showName();
QString mSex;
//Teacher mTeacher;
};
使用:
cpp
auto metaObj = &Student::staticMetaObject;
QObject* stu =metaObj->newInstance();
bool result = false;
QMetaObject::invokeMethod(stu,"setName",Qt::DirectConnection,Q_RETURN_ARG(bool,result),Q_ARG(QString,"张三"));
qDebug() << result; //输出true。
QMetaObject::invokeMethod(stu,"showName");
上面还不算是真正的反射,应为我们构建对象不是通过字符串的。实际上我们我们可以建立一个全局Map,键是字符串。值是这个字符串代表的类的元对象。
在程序一开始的时候填充这个全局Map。
refect.h
cpp
#ifndef REFLECT_H
#define REFLECT_H
#include<QMetaObject>
#include<QMap>
#include<QString>
class Reflect
{
public:
Reflect();
template<class T>
static void registerMetaObject(QString className){
metaObjects.insert(className,&T::staticMetaObject);
}
static const QMetaObject* fromName(QString className){
if(metaObjects.count(className)==0) return nullptr;
else return metaObjects[className];
}
static void init();
private:
static QMap<QString,const QMetaObject*> metaObjects;
};
#endif // REFLECT_H
refect.cpp
cpp
#include "reflect.h"
#include <student.h>
QMap<QString,const QMetaObject*> Reflect::metaObjects;
#define reg(className) registerMetaObject<className>(#className);
Reflect::Reflect() {}
void Reflect::init()
{
reg(Student);
}
使用
cpp
Reflect::init();
auto metaObj = Reflect::fromName("Student");
QObject* stu =metaObj->newInstance();
// Student stu;
bool result = false;
QMetaObject::invokeMethod(stu,"setName",Qt::DirectConnection,Q_RETURN_ARG(bool,result),Q_ARG(QString,"张三"));
qDebug() << result; //输出true。
QMetaObject::invokeMethod(stu,"showName");
qRegisterMetaType
它的用法是这样的:
cpp
qRegisterMetaType<Student>("Student");
作用类似于自己写的
Reflect::registerMetaObject(QString className);
为什么要自己写呢?原因是这个函数它要求Student有拷贝构造函数。但是Student默认是拷贝构造函数的,因为它继承自QObject。而QObject默认没有拷贝构造函数。在有些时候一些类我们不希望提供拷贝构造函数。
我的QT 版本是5.14。更高的版本的qRegisterMetaType可能没有这个问题了。qRegisterMetaType的实现中发生了对类的拷贝,所以要求有拷贝构造函数,这个违反直觉,谁能想到为什么qRegisterMetaType要拷贝对象?
反射的坏处
不会进行编译时检查。反射通过字符串创建一个类,如果字符串拼错了。编译是没有任何报错的。
运行时可能导致程序崩溃,因为不存在这么一个类。返回了一个空指针。后面也没有判空处理。运行后就只有一个崩溃。往往需要很长时间才 能排查到拼错了一个单词。这种在反射字段时更加隐蔽,因为反射字段时拼错一个单词不会造成崩溃,而且程序上没有错误。只是莫名奇妙的有个值没有赋上。