反射作用
反射的作用就是可以通过字符串创建对象,操作类的成员数据,调用类的方法。
举一个场景例子:
实现一个对象工厂,工厂有个函数叫QObject* createObject(QString className);传递一个className,返回一个该类的对象。
如果没有反射:大概率是这样写
QObject* createObject(QString className){if(className == "A"){return new A();}else if(className == "B"){return new B();}//……
}
这里就不是根据字符串创建对象。因为我们把A(),B()明确地写在代码里了。当有类新增时,我们需要添加新的else if分支。
如果有反射:就可以这样写
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指向。
举例如下:
class Student : public QObject
{Q_OBJECTQ_PROPERTY(QString mName MEMBER mName)
public:explicit Student(QObject *parent = nullptr);QString mName; //可以被反射,因为有Q_PROPERTY指向。(第3行)QString mSex; //不能被反射,因为没有Q_PROPERTY指向。
};
其中mName可以被反射,但mSex不能被反射。
使用反射字段
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。结果
stu.setProperty("mName","张三");qDebug() << stu.mName; //输出空。stu.setProperty("nName","张三"); //注意这里是nName。qDebug() << stu.mName; //输出张三。 //这里是mName。
反射自定义类型字段
QT的大部分类型都能反射。如果要反射自定义类型,这个自定义类型必须满足如下条件:
- 有拷贝构造函数
- 有赋值号运算符重载
- 有!=(本类型)运算符重载
- Q_DECLARE_METATYPE() 申明
第一个和第二个可以是默认的。但有时编译器不一定能生成拷贝构造函数或者赋值号构造函数。典型的如继承了QObject的类。因为QObject delete了拷贝构造函数。所以编译器不会为它的派生类生成默认的拷贝构造函数。
举例如下:
有一个自定义类
class Teacher
{
public:explicit Teacher(QObject *parent = nullptr);QString mName = "王老师";bool operator!=(const Teacher& other);signals:
};
Q_DECLARE_METATYPE(Teacher)
学生中多了一个字段mTeacher。这个字段的类型是Teacher
class Student : public QObject
{Q_OBJECTQ_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))。
其使用起来如下
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方法,这是一个有参数有返回值的方法。
class Student : public QObject
{Q_OBJECTQ_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;
}
调用
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。
举例:
class Student : public QObject
{Q_OBJECTStudent(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;
};
使用:
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
#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
#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);
}
使用
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
它的用法是这样的:
qRegisterMetaType<Student>("Student");
作用类似于自己写的
Reflect::registerMetaObject(QString className);
为什么要自己写呢?原因是这个函数它要求Student有拷贝构造函数。但是Student默认是拷贝构造函数的,因为它继承自QObject。而QObject默认没有拷贝构造函数。在有些时候一些类我们不希望提供拷贝构造函数。
我的QT 版本是5.14。更高的版本的qRegisterMetaType可能没有这个问题了。qRegisterMetaType的实现中发生了对类的拷贝,所以要求有拷贝构造函数,这个违反直觉,谁能想到为什么qRegisterMetaType要拷贝对象?
反射的坏处
不会进行编译时检查。反射通过字符串创建一个类,如果字符串拼错了。编译是没有任何报错的。
运行时可能导致程序崩溃,因为不存在这么一个类。返回了一个空指针。后面也没有判空处理。运行后就只有一个崩溃。往往需要很长时间才 能排查到拼错了一个单词。这种在反射字段时更加隐蔽,因为反射字段时拼错一个单词不会造成崩溃,而且程序上没有错误。只是莫名奇妙的有个值没有赋上。