反射作用

反射的作用就是可以通过字符串创建对象,操作类的成员数据,调用类的方法。
举一个场景例子:
实现一个对象工厂,工厂有个函数叫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的。一个字段能被反射,需要具备如下条件。

  1. 所属类直接或者间接继承自QObject。
  2. 类有Q_OBJECT宏定义。
  3. 有Q_PROPERTY指向。
    举例如下:
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不能被反射。

使用反射字段

	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的大部分类型都能反射。如果要反射自定义类型,这个自定义类型必须满足如下条件:

  1. 有拷贝构造函数
  2. 有赋值号运算符重载
  3. 有!=(本类型)运算符重载
  4. 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_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))。
其使用起来如下

	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。被反射的方法需要具备如下条件。

  1. 类直接或间接继承自QObject
  2. 类有O_OBJECT宏定义
  3. 方法有Q_INVOKABLE 修饰,或者是slots,signals。

举例:
添加了一个showName方法。这是一个无参无返回值的方法。
添加了一个setName方法,这是一个有参数有返回值的方法。

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;
}

调用

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");

反射类

反射类是通过字符串构建某个类的对象。一个类可以被反射,它必须满足以下条件。

  1. 类直接或间接继承自QObject
  2. 类有O_OBJECT宏定义
  3. 构造函数可以被反射,且是public。

举例:

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;
};

使用:

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要拷贝对象?

反射的坏处

不会进行编译时检查。反射通过字符串创建一个类,如果字符串拼错了。编译是没有任何报错的。
运行时可能导致程序崩溃,因为不存在这么一个类。返回了一个空指针。后面也没有判空处理。运行后就只有一个崩溃。往往需要很长时间才 能排查到拼错了一个单词。这种在反射字段时更加隐蔽,因为反射字段时拼错一个单词不会造成崩溃,而且程序上没有错误。只是莫名奇妙的有个值没有赋上。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部