[Qt开发探幽(二)]浅谈关于元对象,宏和Q_ENUM

[Qt开发探幽(二)]深入浅出关于元对象,宏和Q_ENUM

前言

最近在开发的时候,我自己写了一套虚函数。这也是我第一次写这么大一个框架,遇到了一些有点莫名其妙的问题(也不能算莫名奇妙,只能说有点玩不明白),详情可以见

[Qt开发思想探幽]QObject、模板继承和多继承

前两天我写了一些demo验证了一些我的想法,算是在元对象编程里简单的游了一游。

一、元对象

Qt的元对象是一个让人又爱又恨的东西。让人爱是因为它确实功能强大,可以允许我们从类、枚举类型、获得一些我们在正常C++开发中可能无法正常获取到的东西。比如最简单的:在正常C++开发中,枚举类型的类型名称对于C++而言只是一个有一个的十六进制码,而不是字符串的形式,也不可能获得字符串,那么可能就有如下的奇技淫巧:

在这里插入图片描述
在这里插入图片描述
没错,以上就是通过 Qt的元对象类型将一个枚举类型的成员转换成字符串,或者将字符串转回枚举类型的值

更变态的是什么?

更变态的是,通过元对象类型我们可以实现一个更夸张的功能:让一个类和一个Json字符串之间做转换:

在这里插入图片描述
当然了,做转换的前提是使用Q_PROPERTY宏包裹着属性,这样这个属性就被注册进了这个类的元对象系统内,然后就可以通过一些奇技淫巧,来实现类成员变量和字符串之间的转换了,以下是一个例子:

#pragma region Lev_Json
/// <summary>
/// name:Lev_Json
/// 说明:此类用作辅助参数类与json字符串之间的转换,使用此类请使用Q_PROPERTY声明所有的类成员变量
/// </summary>
class Lev_Json : QObject {

public:
	template<class T1>
	static bool ValidateJsonKeys(const QString& jsonString, const T1* T_Class) {
		QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
		if (!jsonDoc.isObject()) {
			return false;
		}

		QJsonObject jsonObject = jsonDoc.object();
		const QMetaObject* metaObject = T_Class->metaObject();

		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property = metaObject->property(i);
			QString propName = property.name();
			if (propName.contains("objectName"))
				continue;
			if (!jsonObject.contains(propName)) {
				return false;
			}
		}

		return true;
	}
	/// <summary>
	/// 判断这个Json字符串对于这个Object而言是否合法
	/// </summary>
	/// <typeparam name="T1"></typeparam>
	/// <param name="jsonString"></param>
	/// <returns></returns>
	template<class T1>
	static bool ValidateJsonKeys(const QString& jsonString, QSharedPointer<T1> T_Class_1) {
		QObject* T_Class = dynamic_cast<QObject*>(T_Class_1.data());
		QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
		if (!jsonDoc.isObject()) {
			return false; // Return false if JSON is not an object
		}

		QJsonObject jsonObject = jsonDoc.object();
		const QMetaObject* metaObject = T_Class->metaObject();

		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property = metaObject->property(i);
			QString propName = property.name();

			if (!jsonObject.contains(propName)) {
				return false;
			}
		}

		return true;
	}
	/// <summary>
	/// 推荐,序列化Qt对象,请用Q_PROPERTY包裹成员变量,使用内存安全的QSharedPointer
	/// </summary>
	/// <typeparam name="T1">模板对象,可以不声明,会自动识别</typeparam>
	/// <param name="T_Class_1">输入的对象</param>
	/// <returns></returns>
	template<class T1>
	static QString JsonSerialization(QSharedPointer<T1> T_Class_1) {
		QJsonObject ret;
		QObject* T_Class = dynamic_cast<QObject*>(T_Class_1.data());
		const QMetaObject* metaObject = T_Class->metaObject();

		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property_ = metaObject->property(i);
			QVariant propValue = property_.read(T_Class);

			if (!QString(property_.name()).contains("objectName")) {
				ret.insert(property_.name(), variantToJsonValue(propValue));
			}
		}

		QJsonDocument jsonDoc(ret);
		return jsonDoc.toJson(QJsonDocument::Compact);
	}
	/// <summary>
	/// 推荐,反序列化Qt对象,请用Q_PROPERTY包裹成员变量,会返回一个内存安全的QSharedPointer
	/// </summary>
	/// <typeparam name="T1"></typeparam>
	/// <param name="jsonString"></param>
	/// <returns></returns>
	template<class T1>
	static QSharedPointer<T1> JsonDeserialization(const QString& jsonString) {
		QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
		if (!jsonDoc.isObject()) {
			return QSharedPointer<T1>();
		}

		QJsonObject jsonObject = jsonDoc.object();
		QSharedPointer<T1> result = QSharedPointer<T1>::create();

		const QMetaObject* metaObject = result->metaObject();
		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property = metaObject->property(i);
			QString propName = property.name();

			if (jsonObject.contains(propName)) {
				QJsonValue propJsonValue = jsonObject[propName];
				QVariant propValue = jsonValueToVariant(propJsonValue, property.userType());

				if (propValue.isValid()) {
					property.write(result.data(), propValue);
				}
			}
		}

		return result;
	}
	/// <summary>
	/// 可以用,序列化Qt对象,请用Q_PROPERTY包裹成员变量
	/// </summary>
	/// <typeparam name="T1">模板对象,可以不声明,会自动识别</typeparam>
	/// <param name="T_Class_1">输入的对象</param>
	/// <returns></returns>
	template<class T1>
	static QString JsonSerialization(const T1* T_Class) {
		QJsonObject ret;

		const QMetaObject* metaObject = T_Class->metaObject();

		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property_ = metaObject->property(i);
			QVariant propValue = property_.read(T_Class);

			if (!QString(property_.name()).contains("objectName")) {
				ret.insert(property_.name(), variantToJsonValue(propValue));
			}
		}

		QJsonDocument jsonDoc(ret);
		return jsonDoc.toJson(QJsonDocument::Compact);
	}
	/// <summary>
	/// 不推荐使用,不安全的内存方案
	/// </summary>
	/// <typeparam name="T1"></typeparam>
	/// <param name="result"></param>
	/// <param name="jsonString"></param>
	/// <returns></returns>
	template<class T1>
	static QSharedPointer<T1> JsonDeserialization(T1* result, const QString& jsonString) {
		QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
		if (!jsonDoc.isObject()) {
			return QSharedPointer<T1>();
		}

		QJsonObject jsonObject = jsonDoc.object();
		const QMetaObject* metaObject = result->metaObject();
		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property = metaObject->property(i);
			QString propName = property.name();

			if (jsonObject.contains(propName)) {
				QJsonValue propJsonValue = jsonObject[propName];
				QVariant propValue = jsonValueToVariant(propJsonValue, property.userType());

				if (propValue.isValid()) {
					property.write(result.data(), propValue);
				}
			}
		}

		return result;
	}

private:
	static QJsonValue variantToJsonValue(const QVariant& variant) {
		if (variant.canConvert<QString>()) {
			return QJsonValue::fromVariant(variant.toString());
		}
		else if (variant.canConvert<int>()) {
			return QJsonValue::fromVariant(variant.toInt());
		}
		else if (variant.canConvert<double>()) {
			return QJsonValue::fromVariant(variant.toDouble());
		}
		else if (variant.canConvert<bool>()) {
			return QJsonValue::fromVariant(variant.toBool());
		}
		else if (variant.userType() == qMetaTypeId<QList<int>>()) {
			return listToJsonArray<int>(variant.value<QList<int>>());
		}
		else if (variant.userType() == qMetaTypeId<QList<QString>>()) {
			return listToJsonArray<QString>(variant.value<QList<QString>>());
		}
		else if (variant.userType() == qMetaTypeId<QList<bool>>()) {
			return listToJsonArray<bool>(variant.value<QList<bool>>());
		}
		return QJsonValue::Null;
	}

	template<typename T>
	static QJsonArray listToJsonArray(const QList<T>& list) {
		QJsonArray jsonArray;
		for (const T& value : list) {
			jsonArray.append(QJsonValue::fromVariant(value));
		}
		return jsonArray;
	}
	static QVariant jsonValueToVariant(const QJsonValue& jsonValue, int userType) {
		QVariant result;
		if (jsonValue.isString()) {
			result = jsonValue.toString();
		}
		else if (jsonValue.isDouble()) {
			if (userType == QMetaType::Int) {
				result = jsonValue.toInt();
			}
			else if (userType == QMetaType::Double) {
				result = jsonValue.toDouble();
			}
		}
		else if (jsonValue.isBool()) {
			if (userType == QMetaType::Bool) {
				result = jsonValue.toBool();
			}
		}
		else if (jsonValue.isArray()) {
			QJsonArray jsonArray = jsonValue.toArray();
			if (userType == qMetaTypeId<QList<int>>()) {
				QList<int> intList;
				for (const QJsonValue& element : jsonArray) {
					intList.append(element.toInt());
				}
				result = QVariant::fromValue(intList);
			}
			else if (userType == qMetaTypeId<QList<QString>>()) {
				QList<QString> stringList;
				for (const QJsonValue& element : jsonArray) {
					stringList.append(element.toString());
				}
				result = QVariant::fromValue(stringList);
			}
			// Add more cases for other QList types if needed
		}
		return result;
	}
};
#pragma endregion

当然了,Qt的元对象类型还有很多很强大的功能,比如对象名称等等,各种各样的功能,可以拿着Qt当C#来用了(笑)

但是

Qt的元对象类型也有很多局限性。正如我在前言中提到的,正因为Q_OBJECT宏的存在,QObject的对象是不能使用模板类继承的,也不能使用模板类多继承。这个实际上相当限制了Qt程序员的开发能力。模板类作为功能非常强大的一个功能,也正是C++能如此蓬勃发展的一个重要原因,结果在Qt上用不了,这是令人扼腕叹息的。

另外,值得一提的是,我们可以看到,在自己写继承的时候,从一个继承了QObject类和声明了Q_OBJECT宏的类中继承下来的子类仍然带有Q_OBJECT宏 这件事经常会通不过编译,我不知道自己是触犯了哪个规则,但是之后我的底层框架中最底层的部分都不会使用Q_OBJECT宏,直到我搞懂这件事,因为真的为了这个问题做了太多的妥协了。

二、关于Q_OBJECT等宏属性

如果要聊这个宏,我们得看一下这个宏做了什么,找到Qt Document:

Q_OBJECT宏必须出现在类定义的私有部分中,该类定义声明自己的信号和槽,或者使用Qt的元对象系统提供的其他服务。
#include <QObject>

class Counter : public QObject
{
    Q_OBJECT

public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }

public slots:
    void setValue(int value);

signals:
    void valueChanged(int newValue);

private:
    int m_value;
};
注意:这个宏要求类是QObject的子类。使用Q_GADGET或Q_GADGET_EXPORT而不是Q_OBJECT来启用元对象系统对非QObject子类中的枚举的支持。

Q_OBJECT宏我们可以看到,主要是做了三件事:
1.将指定的类注册进入到元对象系统内,至于什么是元对象系统,我们接下来会说,你先知道是注册进元对象系统就行了
2.添加信号与槽函数的注册
3.注册Qt的属性系统

这三个功能其实也构成了Qt这套框架的全部,可以说Qt整套系统都是围绕着Q_OBJECT宏来做的。

1.元对象系统

元对象系统
Qt的元对象系统(Meta-Object System)为对象间通信、运行时类型信息和动态属性系统提供了信号和槽机制。元对象系统基于三个方面:

  1. QObject类为可以利用元对象系统的对象提供了一个基类。

  2. 类声明的私有部分中的Q_OBJECT宏用于启用元对象功能,如动态属性、信号和插槽。

  3. 元对象编译器(moc)为每个QObject子类提供实现元对象特性所需的代码。

我们可以理解为,元对象系统就是Qt的一个“C#化”的尝试,即将原来在C++中不可见的一切

moc工具读取一个C++源文件。如果它找到一个或多个包含Q_OBJECT宏的类声明,它将生成另一个C++源文件,该文件包含每个类的元对象代码。这个生成的源文件要么被#包含到类的源文件中,要么更常见的是,被编译并链接到类的实现中。

除了提供用于对象之间通信的信号和槽机制(引入该系统的主要原因)之外,元对象代码还提供以下附加功能:

  • QObject::metaObject()返回类的关联元对象。

  • QMetaObject::className()在运行时以字符串形式返回类名,而不需要通过C++编译器支持本机运行时类型信息(RTTI)。

  • 函数返回对象是否是继承QObject继承树中指定类的类的实例。

  • QObject::tr()转换字符串以进行国际化。

  • QObject::setProperty()和QOobject::property()按名称动态设置和获取属性。

  • QMetaObject::newInstance()构造类的一个新实例。

还可以使用qobject_cast()对qobject类执行动态强制转换。qobject_cast()函数的行为类似于标准C++dynamic_cast(),其优点是不需要RTTI支持,并且可以跨动态库边界工作。它尝试将其参数强制转换为尖括号中指定的指针类型,如果对象的类型正确(在运行时确定),则返回非零指针,如果对象类型不兼容,则返回nullptr。

虽然可以在没有Q_OBJECT宏和元对象代码的情况下使用QObject作为基类,但如果不使用Q_OBJECT宏,则信号和插槽以及此处描述的其他功能都不可用。

从元对象系统的角度来看,一个没有元代码的QObject子类等价于它最接近的有元对象代码的祖先。

这意味着,例如,QMetaObject::className()不会返回类的实际名称,而是返回该祖先的类名。
因此,我们强烈建议QObject的所有子类使用Q_OBJECT宏,无论它们是否实际使用信号、槽和属性。

2.信号与槽

在Qt中的信号与槽可以说是Qt的头牌系统,也是Qt这套东西能够如此流行的重要原因,也是整个Qt框架最重要的基石。

当然了,其实自己实现一套Qt的Signal - Slot的系统其实并不复杂,而且肯定很多人已经能开发一套类似的东西了。比如我简单打个样:

class Caller {
public:
	using CallMethod = void(*)(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra);
	using SendCMD = void(*)(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra);

	void RegisterCallMethod(CallMethod callback) {
		callbacks_.append(callback);
	}
	void RegisterSendCMD(SendCMD callback) {
		sendcmds_.append(callback);
	}

	void Signal_CallMethod(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) {
		for (CallMethod callback : callbacks_) {
			if (callback) {
				callback(sModule, sDescribe, sVariable, extra);
			}
		}
	}

	void Signal_SendCMD(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) {
		for (SendCMD callback : callbacks_) {
			if (callback) {
				callback(sModule, sDescribe, sVariable, extra);
			}
		}
	}

private:
	QList<CallMethod> callbacks_;
	QList<SendCMD> sendcmds_;
};

但是Qt的signal - slot 强大的地方就在于它的封装性和灵活性,各种注销注册操作相对自己写回调函数还是简单很多很多的。你想啊,原先需要这么多代码的地方,现在只需要一个宏,或者一句话,难易程度几乎无法比较。

由于Qt独特的signal索引机制,导致其网络相关的库效率可能是C++回调函数的百分之一,这是非常夸张的性能损失,但是这在某些性能不关键的场景仍然是可以接受的。

Signals & Slots

Signals 和Slots用于对象之间的通信。Signals 和Slots机制是Qt的一个核心功能,可能也是与其他框架提供的功能最不同的部分。Qt的元对象系统使Signals 和Slots成为可能。

其他工具包使用回调来实现这种通信。回调是指向函数的指针,因此,如果您希望处理函数通知您某个事件,您可以将指向另一个函数的指针(回调)传递给处理函数。然后,处理函数在适当的时候调用回调。虽然使用这种方法的成功框架确实存在,但回调可能是不直观的,并且在确保回调参数的类型正确性方面可能会遇到问题。

在Qt中,我们有一种替代回调技术的方法:我们使用Signals 和Slots。当特定事件发生时,会发出一个信号。Qt的小部件有许多预定义的Signals ,但我们总是可以对小部件进行子类化,以向它们添加我们自己的Signals。Slots是响应特定信号而调用的函数。Qt的小部件有许多预定义的Slots,但通常的做法是对小部件进行子类化,并添加自己的Slots,以便处理您感兴趣的Signals。

Signal和Slot机制是类型安全的:Signal的签名必须与接收Slot的签名匹配。(事实上,Slot的签名可能比它接收到的Signal更短,因为它可以忽略额外的参数。)

由于签名是兼容的,编译器可以在使用基于函数指针的语法时帮助我们检测类型不匹配。基于字符串的SIGNAL和SLOT语法将在运行时检测类型不匹配。

Signal和Slot是松散耦合的:发出Signal的类既不知道也不关心哪个Slot接收Signal。Qt的Signal和Slot机制确保,如果您将Signal连接到Slot,Slot将在正确的时间使用Signal的参数进行调用。Signal和Slot可以采用任何类型的任意数量的参数。

它们是完全类型安全的。 所有继承自QObject或其子类之一(例如,QWidget)的类都可以包含Signal和Slot。当对象以其他对象可能感兴趣的方式改变其状态时,它们会发出Signal。

这就是对象所做的所有通信。它不知道或不关心是否有任何东西正在接收它发出的Signal。这是真正的信息封装,并确保对象可以用作软件组件。

Slot可以用于接收Signal,但它们也是正常的成员功能。就像一个对象不知道是否有任何东西接收到它的Signal一样,一个Slot也不知道它是否有任何Signal连接到它。这确保了可以用Qt创建真正独立的组件。

您可以将任意数量的Signal连接到单个Slot,也可以将Signal连接到任意数量的Slot。甚至可以将一个Signal直接连接到另一个Signal。(无论何时发出第一个Signal,都会立即发出第二个Signal。)

Signal和Slot共同构成了一个强大的组件编程机制。 Signal 当对象的内部状态以某种可能对对象的客户端或所有者感兴趣的方式发生变化时,对象会发出Signal。Signal是公共访问函数,可以从任何地方发出,但我们建议只从定义Signal及其子类的类发出Signal。

当一个Signal发出时,连接到它的Slot通常会立即执行,就像正常的函数调用一样。当这种情况发生时,Signal和Slot机制完全独立于任何GUI事件循环。一旦所有Slot都返回,就会执行emit语句后面的代码。使用排队连接时,情况略有不同;

在这种情况下,emit关键字后面的代码将立即继续,稍后将执行Slot。 如果多个Slot连接到一个Signal,则当Signal发出时,这些Slot将按照连接的顺序依次执行。 Signal由moc自动生成,不得在.cpp文件中实现。它们永远不能有返回类型(即使用void)。

关于arguments的注意事项:我们的经验表明,如果Signal和Slot不使用特殊类型,它们将更易于重用。如果QScrollBar::valueChanged()使用一种特殊类型,如假设的QScrollBar::Range,则它只能连接到专门为QScrollBar设计的Slot。

将不同的输入小部件连接在一起是不可能的。 Slot 当连接到Slot的Signal发出时,就会调用该Slot。Slot是正常的C++函数,可以正常调用;它们唯一的特点是Signal可以连接到它们。 由于Slot是正常的成员函数,因此当直接调用时,它们遵循正常的C++规则。

但是,作为Slot,它们可以由任何组件通过SignalSlot连接调用,而不管其访问级别如何。这意味着,从任意类的实例发出的Signal可以导致在不相关类的实例中调用专用Slot。 您还可以将Slot定义为虚拟Slot,我们发现这在实践中非常有用。

与回调相比,Signal和Slot的速度稍慢,因为它们提供了更大的灵活性,尽管实际应用程序的差异并不显著。通常,发射连接到某些Slot的Signal比直接调用接收器(使用非虚拟函数调用)慢大约十倍。这是定位连接对象、安全地迭代所有连接(即检查后续接收器在发射过程中是否未被破坏)以及以通用方式整理任何参数所需的开销。虽然十个非虚拟函数调用听起来可能很多,但它的开销比任何新操作或删除操作都要小得多。

一旦执行了一个字符串、向量或列表操作,而该操作在后台需要新建或删除,则Signal和Slot开销只占整个函数调用成本的一小部分。无论何时进行系统调用都是如此

3.属性系统

Qt提供了一个复杂的属性系统,类似于一些编译器供应商提供的属性系统。然而,作为一个独立于编译器和平台的库,Qt不依赖于__property或[property]等非标准编译器功能。Qt解决方案可与Qt支持的每个平台上的任何标准C++编译器配合使用。它基于元对象系统,该系统还通过信号和插槽提供对象间通信。

他其实更像是C#中的一个get set方法,相当于是将这个属性注册到元对象系统中去,并且给每个对象提供了一个get set方法(当然了,get set方法也只是你定义的,这又不是真的c#)

具体的属性系统这里我不做过多介绍,详情可以参考Qt Document

The Property System

其中有非常详尽的解释。

三、关于Q_ENUMS

Q_ENUM这个宏经过了几次修改,早期貌似可以随意注册Q_ENUMS,但是在后续貌似只剩下了两种枚举类型的注册方法:

一个是在类内声明枚举类型,然后在类内声明这个Q_ENUM,当然了,用这个宏去注册枚举类型的前提是使用了Q_OBJECT宏

现在假设我们想在元对象系统中使用这个枚举类,也就是我想通过它的int值获得其映射的key(字符串形式),比如如下这个枚举类型

在这里插入图片描述

test_enum::Test_Enum_1 tester = test_enum::Test_Enum_1::none;

我现在可能是传递Json字符串,或者是别的什么,反正我就是要获得none这个关键字,那我该怎么做?

这个时候你有两个做法,但是实际上都是将其注册到元对象

1.将其注册到Q_NAMESPACE下

启用一个单独的namespace,通过Q_NAMESPACE宏的形式将这个命名空间注册到Qt的元对象系统内,举个例子:

namespace test_enum {
	Q_NAMESPACE	//Q_NAMESPACE宏将整个命名空间注册进元对象列表中去
		enum class Test_Enum_1 {
		none,
		open,
		close,
		stop
	};
	Q_ENUM_NS(Test_Enum_1) //Q_ENUM_NS宏将我们需要的枚举类型对象注册进
}

2.类内注册

除此之外,还有另一种方法,那就是将枚举类型写入到用Q_OBJECT, Q_GADGET or Q_GADGET_EXPORT这三个宏之一标记的类内

需要注意的一点:Q_GADGET是Q_OBJECT宏的轻量化版本,用Q_GADGET意味着这个类不一定需要继承QObject类了

适用于不继承QObject但仍希望使用QMetaObject提供的一些反射功能的类。就像Q_OBJECT宏一样,它必须出现在类定义的私有部分中。

Q_GADGET可以有Q_ENUM、Q_PROPERTY和Q_INVOKABLE,但不能有信号或插槽。
Q_GADGET使类成员staticMetaObject可用。staticMetaObject的类型为QMetaObject,并提供对用Q_ENUM声明的枚举的访问。

如以下代码:

class TSG_Device : public TSG_Caller {
	/// <summary>
/// 设备状态
/// </summary>

public:
	enum class DeviceState
	{
		DS_None,
		DS_Unknown,
		DS_Disconnected,
		DS_Connected,
		DS_Working,
		DS_Pause,
		DS_Stop
	}; Q_ENUM(DeviceState)

		enum class DeviceOpen {
		DO_Open,
		DO_Close
	}; Q_ENUM(DeviceOpen)
}

这样一个内嵌的枚举类,也可以用QMetaEnum做到之前我们想要做的事

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值