13.2 Explicit conversions

本文详细介绍了 C# 中的显式类型转换,包括显式数值转换、显式枚举转换、显式引用转换、拆箱转换和用户定义的显式转换。显式数值转换可能会丢失信息或抛出异常,不同类型转换有不同处理方式。显式引用转换需运行时检查,失败会抛出异常。
The following conversions are classified as explicit conversions:
?All implicit conversions.
?Explicit numeric conversions.
?Explicit enumeration conversions.
?Explicit reference conversions.
?Explicit interface conversions.
?Unboxing conversions.
?User-defined explicit conversions.
Explicit conversions can occur in cast expressions (?4.6.6).
The set of explicit conversions includes all implicit conversions. [Note:
This means that redundant cast
expressions are allowed. end note]
The explicit conversions that are not implicit conversions are conversions
that cannot be proven to always
succeed, conversions that are known to possibly lose information, and
conversions across domains of types
sufficiently different to merit explicit notation.
13.2.1 Explicit numeric conversions
The explicit numeric conversions are the conversions from a numeric-type to
another numeric-type for
which an implicit numeric conversion (?3.1.2) does not already exist:
?From sbyte to byte, ushort, uint, ulong, or char.
?From byte to sbyte and char.
?From short to sbyte, byte, ushort, uint, ulong, or char.
?From ushort to sbyte, byte, short, or char.
?From int to sbyte, byte, short, ushort, uint, ulong, or char.
?From uint to sbyte, byte, short, ushort, int, or char.
?From long to sbyte, byte, short, ushort, int, uint, ulong, or char.
?From ulong to sbyte, byte, short, ushort, int, uint, long, or char.
?From char to sbyte, byte, or short.
?From float to sbyte, byte, short, ushort, int, uint, long, ulong, char,
or decimal.
?From double to sbyte, byte, short, ushort, int, uint, long, ulong, char,
float, or decimal.
C# LANGUAGE SPECIFICATION
116
?From decimal to sbyte, byte, short, ushort, int, uint, long, ulong, char,
float, or double.
Because the explicit conversions include all implicit and explicit numeric
conversions, it is always possible
to convert from any numeric-type to any other numeric-type using a cast
expression (?4.6.6).
The explicit numeric conversions possibly lose information or possibly
cause exceptions to be thrown. An
explicit numeric conversion is processed as follows:
?For a conversion from an integral type to another integral type, the
processing depends on the overflow
checking context (?4.5.12) in which the conversion takes place:
In a checked context, the conversion succeeds if the value of the source
operand is within the range of the
destination type, but throws a System.OverflowException if the value of the
source operand is outside
the range of the destination type.
In an unchecked context, the conversion always succeeds, and proceeds as
follows.
?If the source type is larger than the destination type, then the source
value is truncated by
discarding its ?extra? most significant bits. The result is then treated as
a value of the destination
type.
?If the source type is smaller than the destination type, then the source
value is either signextended
or zero-extended so that it is the same size as the destination type.
Sign-extension is
used if the source type is signed; zero-extension is used if the source
type is unsigned. The result
is then treated as a value of the destination type.
?If the source type is the same size as the destination type, then the
source value is treated as a
value of the destination type
?For a conversion from decimal to an integral type, the source value is
rounded towards zero to the
nearest integral value, and this integral value becomes the result of the
conversion. If the resulting
integral value is outside the range of the destination type, a
System.OverflowException is thrown.
?For a conversion from float or double to an integral type, the processing
depends on the overflowchecking
context (?4.5.12) in which the conversion takes place:
In a checked context, the conversion proceeds as follows:
?The value is rounded towards zero to the nearest integral value. If this


integral value is within
the range of the destination type, then this value is the result of the
conversion.
?Otherwise, a System.OverflowException is thrown.
In an unchecked context, the conversion always succeeds, and proceeds as
follows.
?The value is rounded towards zero to the nearest integral value. If this
integral value is within
the range of the destination type, then this value is the result of the
conversion.
?Otherwise, the result of the conversion is an unspecified value of the
destination type.
?For a conversion from double to float, the double value is rounded to the
nearest float value. If
the double value is too small to represent as a float, the result becomes
positive zero or negative zero.
If the double value is too large to represent as a float, the result
becomes positive infinity or negative
infinity. If the double value is NaN, the result is also NaN.
?For a conversion from float or double to decimal, the source value is
converted to decimal
representation and rounded to the nearest number after the 28th decimal
place if required (?1.1.6). If the
source value is too small to represent as a decimal, the result becomes
zero. If the source value is NaN,
infinity, or too large to represent as a decimal, a
System.OverflowException is thrown.
?For a conversion from decimal to float or double, the decimal value is
rounded to the nearest
double or float value. While this conversion may lose precision, it never
causes an exception to be
thrown.
13.2.2 Explicit enumeration conversions
The explicit enumeration conversions are:
?From sbyte, byte, short, ushort, int, uint, long, ulong, char, float,
double, or decimal to
any enum-type.
?From any enum-type to sbyte, byte, short, ushort, int, uint, long, ulong,
char, float,
double, or decimal.
?From any enum-type to any other enum-type.
An explicit enumeration conversion between two types is processed by
treating any participating enum-type
as the underlying type of that enum-type, and then performing an implicit
or explicit numeric conversion
between the resulting types. [Example: For example, given an enum-type E
with and underlying type of int,
a conversion from E to byte is processed as an explicit numeric conversion (
?3.2.1) from int to byte,
and a conversion from byte to E is processed as an implicit numeric
conversion (?3.1.2) from byte to
int. end example]
13.2.3 Explicit reference conversions
The explicit reference conversions are:
?From object to any reference-type.
?From any class-type S to any class-type T, provided S is a base class of
T.
?From any class-type S to any interface-type T, provided S is not sealed
and provided S does not
implement T.
?From any interface-type S to any class-type T, provided T is not sealed
or provided T implements S.
?From any interface-type S to any interface-type T, provided S is not
derived from T.
?From an array-type S with an element type SE to an array-type T with an
element type TE, provided all
of the following are true:
S and T differ only in element type. (In other words, S and T have the same
number of dimensions.)
Both SE and TE are reference-types.
An explicit reference conversion exists from SE to TE.
?From System.Array and the interfaces it implements, to any array-type.
?From System.Delegate and the interfaces it implements, to any
delegate-type.
The explicit reference conversions are those conversions between
reference-types that require run-time
checks to ensure they are correct.
For an explicit reference conversion to succeed at run-time, the value of
the source operand must be null,
or the actual type of the object referenced by the source operand must be a
type that can be converted to the
destination type by an implicit reference conversion (?3.1.4). If an
explicit reference conversion fails, a
System.InvalidCastException is thrown.
Reference conversions, implicit or explicit, never change the referential
identity of the object being
converted. [Note: In other words, while a reference conversion may change
the type of the reference, it never
changes the type or value of the object being referred to. end note]
13.2.4 Unboxing conversions
An unboxing conversion permits an explicit conversion from type object or
System.ValueType to any
value-type, or from any interface-type to any value-type that implements
the interface-type. An unboxing
operation consists of first checking that the object instance is a boxed
value of the given value-type, and then
copying the value out of the instance. A struct can be unboxed from the
type System.ValueType, since
that is a base class for all structs (?8.3.2).
Unboxing conversions are described further in ?1.3.2.


13.2.5 User-defined explicit conversions
A user-defined explicit conversion consists of an optional standard
explicit conversion, followed by
execution of a user-defined implicit or explicit conversion operator,
followed by another optional standard
explicit conversion. The exact rules for evaluating user-defined
conversions are described in ?3.4.4.
// _GLIBCXX_RESOLVE_LIB_DEFECTS // DR 740 - omit specialization for array objects with a compile time length /** Specialization of default_delete for arrays, used by `unique_ptr<T[]>` * * @headerfile memory * @since C++11 */ template <typename _Tp> struct default_delete<_Tp[]> { public: /// Default constructor constexpr default_delete() noexcept = default; /** @brief Converting constructor. * * Allows conversion from a deleter for arrays of another type, such as * a const-qualified version of `_Tp`. * * Conversions from types derived from `_Tp` are not allowed because * it is undefined to `delete[]` an array of derived types through a * pointer to the base type. */ template <typename _Up, typename = _Require<is_convertible<_Up (*)[], _Tp (*)[]>>> _GLIBCXX23_CONSTEXPR default_delete(const default_delete<_Up[]> &) noexcept {} /// Calls `delete[] __ptr` template <typename _Up> _GLIBCXX23_CONSTEXPR typename enable_if<is_convertible<_Up (*)[], _Tp (*)[]>::value>::type operator()(_Up *__ptr) const { static_assert(sizeof(_Tp) > 0, "can't delete pointer to incomplete type"); delete[] __ptr; } }; /// @cond undocumented // Manages the pointer and deleter of a unique_ptr template <typename _Tp, typename _Dp> class __uniq_ptr_impl { template <typename _Up, typename _Ep, typename = void> struct _Ptr { using type = _Up *; }; template <typename _Up, typename _Ep> struct _Ptr<_Up, _Ep, __void_t<typename remove_reference<_Ep>::type::pointer>> { using type = typename remove_reference<_Ep>::type::pointer; }; public: using _DeleterConstraint = enable_if< __and_<__not_<is_pointer<_Dp>>, is_default_constructible<_Dp>>::value>; using pointer = typename _Ptr<_Tp, _Dp>::type; static_assert(!is_rvalue_reference<_Dp>::value, "unique_ptr's deleter type must be a function object type" " or an lvalue reference type"); __uniq_ptr_impl() = default; _GLIBCXX23_CONSTEXPR __uniq_ptr_impl(pointer __p) : _M_t() { _M_ptr() = __p; } template <typename _Del> _GLIBCXX23_CONSTEXPR __uniq_ptr_impl(pointer __p, _Del &&__d) : _M_t(__p, std::forward<_Del>(__d)) {} _GLIBCXX23_CONSTEXPR __uniq_ptr_impl(__uniq_ptr_impl &&__u) noexcept : _M_t(std::move(__u._M_t)) { __u._M_ptr() = nullptr; } _GLIBCXX23_CONSTEXPR __uniq_ptr_impl &operator=(__uniq_ptr_impl &&__u) noexcept { reset(__u.release()); _M_deleter() = std::forward<_Dp>(__u._M_deleter()); return *this; } _GLIBCXX23_CONSTEXPR pointer &_M_ptr() noexcept { return std::get<0>(_M_t); } _GLIBCXX23_CONSTEXPR pointer _M_ptr() const noexcept { return std::get<0>(_M_t); } _GLIBCXX23_CONSTEXPR _Dp &_M_deleter() noexcept { return std::get<1>(_M_t); } _GLIBCXX23_CONSTEXPR const _Dp &_M_deleter() const noexcept { return std::get<1>(_M_t); } _GLIBCXX23_CONSTEXPR void reset(pointer __p) noexcept { const pointer __old_p = _M_ptr(); _M_ptr() = __p; if (__old_p) _M_deleter()(__old_p); } _GLIBCXX23_CONSTEXPR pointer release() noexcept { pointer __p = _M_ptr(); _M_ptr() = nullptr; return __p; } _GLIBCXX23_CONSTEXPR void swap(__uniq_ptr_impl &__rhs) noexcept { using std::swap; swap(this->_M_ptr(), __rhs._M_ptr()); swap(this->_M_deleter(), __rhs._M_deleter()); } private: tuple<pointer, _Dp> _M_t; }; // Defines move construction + assignment as either defaulted or deleted. template <typename _Tp, typename _Dp, bool = is_move_constructible<_Dp>::value, bool = is_move_assignable<_Dp>::value> struct __uniq_ptr_data : __uniq_ptr_impl<_Tp, _Dp> { using __uniq_ptr_impl<_Tp, _Dp>::__uniq_ptr_impl; __uniq_ptr_data(__uniq_ptr_data &&) = default; __uniq_ptr_data &operator=(__uniq_ptr_data &&) = default; }; template <typename _Tp, typename _Dp> struct __uniq_ptr_data<_Tp, _Dp, true, false> : __uniq_ptr_impl<_Tp, _Dp> { using __uniq_ptr_impl<_Tp, _Dp>::__uniq_ptr_impl; __uniq_ptr_data(__uniq_ptr_data &&) = default; __uniq_ptr_data &operator=(__uniq_ptr_data &&) = delete; }; template <typename _Tp, typename _Dp> struct __uniq_ptr_data<_Tp, _Dp, false, true> : __uniq_ptr_impl<_Tp, _Dp> { using __uniq_ptr_impl<_Tp, _Dp>::__uniq_ptr_impl; __uniq_ptr_data(__uniq_ptr_data &&) = delete; __uniq_ptr_data &operator=(__uniq_ptr_data &&) = default; }; template <typename _Tp, typename _Dp> struct __uniq_ptr_data<_Tp, _Dp, false, false> : __uniq_ptr_impl<_Tp, _Dp> { using __uniq_ptr_impl<_Tp, _Dp>::__uniq_ptr_impl; __uniq_ptr_data(__uniq_ptr_data &&) = delete; __uniq_ptr_data &operator=(__uniq_ptr_data &&) = delete; }; /// @endcond // 20.7.1.2 unique_ptr for single objects. /// A move-only smart pointer that manages unique ownership of a resource. /// @headerfile memory /// @since C++11 template <typename _Tp, typename _Dp = default_delete<_Tp>> class unique_ptr { template <typename _Up> using _DeleterConstraint = typename __uniq_ptr_impl<_Tp, _Up>::_DeleterConstraint::type; __uniq_ptr_data<_Tp, _Dp> _M_t; public: using pointer = typename __uniq_ptr_impl<_Tp, _Dp>::pointer; using element_type = _Tp; using deleter_type = _Dp; private: // helper template for detecting a safe conversion from another // unique_ptr template <typename _Up, typename _Ep> using __safe_conversion_up = __and_< is_convertible<typename unique_ptr<_Up, _Ep>::pointer, pointer>, __not_<is_array<_Up>>>; public: // Constructors. /// Default constructor, creates a unique_ptr that owns nothing. template <typename _Del = _Dp, typename = _DeleterConstraint<_Del>> constexpr unique_ptr() noexcept : _M_t() { } /** Takes ownership of a pointer. * * @param __p A pointer to an object of @c element_type * * The deleter will be value-initialized. */ template <typename _Del = _Dp, typename = _DeleterConstraint<_Del>> _GLIBCXX23_CONSTEXPR explicit unique_ptr(pointer __p) noexcept : _M_t(__p) { } /** Takes ownership of a pointer. * * @param __p A pointer to an object of @c element_type * @param __d A reference to a deleter. * * The deleter will be initialized with @p __d */ template <typename _Del = deleter_type, typename = _Require<is_copy_constructible<_Del>>> _GLIBCXX23_CONSTEXPR unique_ptr(pointer __p, const deleter_type &__d) noexcept : _M_t(__p, __d) {} /** Takes ownership of a pointer. * * @param __p A pointer to an object of @c element_type * @param __d An rvalue reference to a (non-reference) deleter. * * The deleter will be initialized with @p std::move(__d) */ template <typename _Del = deleter_type, typename = _Require<is_move_constructible<_Del>>> _GLIBCXX23_CONSTEXPR unique_ptr(pointer __p, __enable_if_t<!is_lvalue_reference<_Del>::value, _Del &&> __d) noexcept : _M_t(__p, std::move(__d)) { } template <typename _Del = deleter_type, typename _DelUnref = typename remove_reference<_Del>::type> _GLIBCXX23_CONSTEXPR unique_ptr(pointer, __enable_if_t<is_lvalue_reference<_Del>::value, _DelUnref &&>) = delete; /// Creates a unique_ptr that owns nothing. template <typename _Del = _Dp, typename = _DeleterConstraint<_Del>> constexpr unique_ptr(nullptr_t) noexcept : _M_t() { } // Move constructors. /// Move constructor. unique_ptr(unique_ptr &&) = default; /** @brief Converting constructor from another type * * Requires that the pointer owned by @p __u is convertible to the * type of pointer owned by this object, @p __u does not own an array, * and @p __u has a compatible deleter type. */ template <typename _Up, typename _Ep, typename = _Require<__safe_conversion_up<_Up, _Ep>, __conditional_t<is_reference<_Dp>::value, is_same<_Ep, _Dp>, is_convertible<_Ep, _Dp>>>> _GLIBCXX23_CONSTEXPR unique_ptr(unique_ptr<_Up, _Ep> &&__u) noexcept : _M_t(__u.release(), std::forward<_Ep>(__u.get_deleter())) { } #if _GLIBCXX_USE_DEPRECATED #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" /// Converting constructor from @c auto_ptr template <typename _Up, typename = _Require< is_convertible<_Up *, _Tp *>, is_same<_Dp, default_delete<_Tp>>>> unique_ptr(auto_ptr<_Up> &&__u) noexcept; #pragma GCC diagnostic pop #endif /// Destructor, invokes the deleter if the stored pointer is not null. #if __cplusplus > 202002L && __cpp_constexpr_dynamic_alloc constexpr #endif ~unique_ptr() noexcept { static_assert(__is_invocable<deleter_type &, pointer>::value, "unique_ptr's deleter must be invocable with a pointer"); auto &__ptr = _M_t._M_ptr(); if (__ptr != nullptr) get_deleter()(std::move(__ptr)); __ptr = pointer(); } // Assignment. /** @brief Move assignment operator. * * Invokes the deleter if this object owns a pointer. */ unique_ptr &operator=(unique_ptr &&) = default; /** @brief Assignment from another type. * * @param __u The object to transfer ownership from, which owns a * convertible pointer to a non-array object. * * Invokes the deleter if this object owns a pointer. */ template <typename _Up, typename _Ep> _GLIBCXX23_CONSTEXPR typename enable_if<__and_< __safe_conversion_up<_Up, _Ep>, is_assignable<deleter_type &, _Ep &&>>::value, unique_ptr &>::type operator=(unique_ptr<_Up, _Ep> &&__u) noexcept { reset(__u.release()); get_deleter() = std::forward<_Ep>(__u.get_deleter()); return *this; } /// Reset the %unique_ptr to empty, invoking the deleter if necessary. _GLIBCXX23_CONSTEXPR unique_ptr & operator=(nullptr_t) noexcept { reset(); return *this; } // Observers. /// Dereference the stored pointer. _GLIBCXX23_CONSTEXPR typename add_lvalue_reference<element_type>::type operator*() const noexcept(noexcept(*std::declval<pointer>())) { __glibcxx_assert(get() != pointer()); return *get(); } /// Return the stored pointer. _GLIBCXX23_CONSTEXPR pointer operator->() const noexcept { _GLIBCXX_DEBUG_PEDASSERT(get() != pointer()); return get(); } /// Return the stored pointer. _GLIBCXX23_CONSTEXPR pointer get() const noexcept { return _M_t._M_ptr(); } /// Return a reference to the stored deleter. _GLIBCXX23_CONSTEXPR deleter_type & get_deleter() noexcept { return _M_t._M_deleter(); } /// Return a reference to the stored deleter. _GLIBCXX23_CONSTEXPR const deleter_type & get_deleter() const noexcept { return _M_t._M_deleter(); } /// Return @c true if the stored pointer is not null. _GLIBCXX23_CONSTEXPR explicit operator bool() const noexcept { return get() == pointer() ? false : true; } // Modifiers. /// Release ownership of any stored pointer. _GLIBCXX23_CONSTEXPR pointer release() noexcept { return _M_t.release(); } /** @brief Replace the stored pointer. * * @param __p The new pointer to store. * * The deleter will be invoked if a pointer is already owned. */ _GLIBCXX23_CONSTEXPR void reset(pointer __p = pointer()) noexcept { static_assert(__is_invocable<deleter_type &, pointer>::value, "unique_ptr's deleter must be invocable with a pointer"); _M_t.reset(std::move(__p)); } /// Exchange the pointer and deleter with another object. _GLIBCXX23_CONSTEXPR void swap(unique_ptr &__u) noexcept { static_assert(__is_swappable<_Dp>::value, "deleter must be swappable"); _M_t.swap(__u._M_t); } // Disable copy from lvalue. unique_ptr(const unique_ptr &) = delete; unique_ptr &operator=(const unique_ptr &) = delete; };
最新发布
08-27
### 关于BigInt与其他类型混合使用的错误及生成ID为负数的问题 在JavaScript中,`BigInt`与常规数字类型(如`number`)之间的操作需要特别注意。如果直接将`BigInt`与`number`类型混合使用而没有进行显式转换,会导致`Cannot mix BigInt and other types. Use explicit conversions`的错误[^1]。 #### 问题原因 当雪花算法生成的ID超出JavaScript安全整数范围时,可能会出现精度丢失或符号错误。此外,如果在计算过程中未正确处理`BigInt`和`number`类型的转换,也会导致类似的错误。例如,在某些情况下,时间戳部分可能被错误解释为负数,从而影响最终生成的ID。 #### 解决方案 ##### 1. 确保所有运算均使用`BigInt` 为了防止类型混淆,应确保所有涉及的数值均为`BigInt`类型。可以通过在每个数值后添加`n`来明确指定其为`BigInt`类型。以下是一个示例代码片段: ```javascript function addBigInt(a, b) { return a + b; // 如果a和b都是BigInt,则不会报错 } const result = addBigInt(123456789012345678901234567890n, 987654321098765432109876543210n); console.log(result.toString()); ``` ##### 2. 使用显式转换 如果必须混合使用`BigInt`和`number`类型,则需要通过显式转换来避免类型错误。可以使用`BigInt()`函数将`number`类型转换为`BigInt`类型,或者使用`Number()`函数将`BigInt`类型转换为`number`类型(但需注意精度限制)。以下是一个示例: ```javascript function safeAdd(a, b) { if (typeof a === 'bigint' && typeof b === 'number') { return a + BigInt(b); // 显式将number转换为BigInt } else if (typeof a === 'number' && typeof b === 'bigint') { return BigInt(a) + b; // 显式将number转换为BigInt } return a + b; } const mixedResult = safeAdd(123n, 456); // 不会报错 console.log(mixedResult.toString()); ``` ##### 3. 避免生成负数ID 确保雪花算法的第一位始终为0以表示正数。如果时间戳部分超出范围或发生溢出,可能会导致最高位被错误解释为符号位,从而使生成的ID变为负数。以下是一个改进的雪花算法实现: ```javascript class Snowflake { constructor(workerId, datacenterId) { this.twepoch = BigInt(1672531200000); // 起始时间戳 this.workerIdBits = 5n; this.datacenterIdBits = 5n; this.sequenceBits = 12n; this.maxWorkerId = (1n << this.workerIdBits) - 1n; this.maxDatacenterId = (1n << this.datacenterIdBits) - 1n; this.workerIdShift = this.sequenceBits; this.datacenterIdShift = this.sequenceBits + this.workerIdBits; this.timestampLeftShift = this.sequenceBits + this.workerIdBits + this.datacenterIdBits; this.sequenceMask = (1n << this.sequenceBits) - 1n; this.workerId = BigInt(workerId); this.datacenterId = BigInt(datacenterId); this.sequence = 0n; this.lastTimestamp = -1n; } getCurrentTimestamp() { return BigInt(Date.now()); } tillNextMillis(lastTimestamp) { let timestamp = this.getCurrentTimestamp(); while (timestamp <= lastTimestamp) { timestamp = this.getCurrentTimestamp(); } return timestamp; } nextId() { let timestamp = this.getCurrentTimestamp(); if (timestamp < this.lastTimestamp) { throw new Error("Clock moved backwards. Refusing to generate id"); } if (this.lastTimestamp === timestamp) { this.sequence = (this.sequence + 1n) & this.sequenceMask; if (this.sequence === 0n) { timestamp = this.tillNextMillis(this.lastTimestamp); } } else { this.sequence = 0n; } this.lastTimestamp = timestamp; return ((timestamp - this.twepoch) << this.timestampLeftShift) | (this.datacenterId << this.datacenterIdShift) | (this.workerId << this.workerIdShift) | this.sequence; } } const snowflake = new Snowflake(1, 1); console.log(snowflake.nextId().toString()); ``` 上述代码中,所有涉及的数值均被明确指定为`BigInt`类型,从而避免了因类型混淆而导致的错误。同时,通过合理设置时间戳范围和位移值,确保生成的ID始终为正数[^2]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值