使用 DataReaders 来提高速度和减少内存使用

 

 

发布日期: 10/26/2004 | 更新日期: 10/26/2004

Rick Dobson

说到数据库连接,.NET 的支持者喜欢突出数据适配器和数据集所提供的离线访问的优势。在此过程中,DataReader 有时会被忽视。但是,正如 Rick Dobson 在此处演示的那样,DataReader 是不同的 — 它们提供对数据源的只进、只读连线访问,而且它们不支持数据操作。那么,为什么要使用如此受限的东西呢?答案是性能,对于入门者来说: DataReader 要快很多。另一个好处是占用的内存较少 — DataReader 可让您在获得数据时就处理它,每次一行。所以,DataReader 特别适用于处理过大以致于无法容纳在内存中的数据。

*

为了从 DataReader 获得最大利益,您需要了解其功能和限制。由于 DataReader 具有定义完善的限制,您还可以了解一下如何利用其他 .NET 实体(如数组)来补充 DataReader 的功能,并从中获益。本文通过三个领域中各自的一些示例应用程序来回顾 DataReader 的功能。首先,我将展示生成、填充并配置 DataReader 以便用于 Windows 窗体控件的有效代码模式。第二对示例突出说明了如何使用类型化数据来计算表达式,这将反映 DataReader 中列的数据类型的特性。我将通过 DataReader 检索分层数据的两种技术进行比较,来结束本文。

从 DataReader 到列表框

您可以用指向数据源的 DataReader 来轻松地填充列表框。本部分的示例也经常应用于组合框控件。您应首先为 Command 对象创建 DataReader 并调用 ExecuteReader 方法,该方法通常是内联创建的。

ExecuteReader 方法可以接受 CommandBehavior 枚举来自定义 Command 的行为及其关联的 DataReader。本部分中的两个示例突出了 DataReader 及其 Command 对象之间的相互作用,并提供了有关窗体和控件管理的其他有趣的应用程序详细信息。请参阅 HCVSDataReaders 项目,以访问每个示例的所有代码。

显示原始的 DataReader 数据

HCVSDataReaders 项目中的第一个 DataReader 示例是在 Form1 上 Button1 的 Click 事件中,ADONETObjects 类中的两个方法和 Form1 后面的 DataReaderForTable 函数过程也在该事件中。为方便起见,ADONETObjects 类驻留在 HCVSDataReaders 项目中。图 1 显示了单击 Populate from DataReader 按钮后的窗体。按钮的 Click 事件过程用从 SQL Server Northwind 数据库中的 Employees 表选定的列值来填充列表框。

SqlDataReader 类有很多特殊方法,用于从各种专用的 .NET 和 SQL Server 数据格式中获取数据。不过,对于简单的应用程序来说,您无须担心它们。所有 DataReader 都需要做的事情是,接受从任何非字符串数据类型到字符串的默认转换,然后将一个经过计算的字符串表达式添加到列表框。这就是下面的代码所要做的事情。它来自 Button1_Click 过程。一个 While 循环逐行读取,每次创建一个包含四个对 drd1 DataReader 的引用的 str2 表达式。这些引用中有两个是数字实例。值甚至可以为空(如 2 号雇员的 ReportsTo 列值)。不过,对每行来说,该表达式都是成功的。您可以按名称或基于零的索引来指定列。

  Do While drd1.Read
   Dim str2 As String = _
    "Employee " & drd1("EmployeeID") & _
    ", " & drd1("FirstName") & _
    " " & drd1("LastName") & _
    " reports to: " & drd1("ReportsTo")
   ListBox1.Items.Add(str2)
  Loop

第一个示例中最有趣的部分可能是如何先创建 drd1 DataReader。Button1 的 Click 事件过程将 drd1 创建为一个 SqlDataReader 类,并将来自我创建的名为 ataReaderForTable 的函数的返回值指定给它。它传递 Employees 表的名称 — DataReaderForTable 为其开发了一个 DataReader。

  Dim drd1 As SqlClient.SqlDataReader = _
   DataReaderForTable("Employees")

DataReaderForTable 过程创建 DataReader 的步骤有三个。

  Dim drd1 As SqlClient.SqlDataReader
  Dim ADOObjs As New ADONETObjects

  'Specify connection object
  Dim cnn1 As SqlClient.SqlConnection = _
     ADOObjs.MakeNorthwindConnection

  'Specify a command object
  Dim str1 As String = _
   "SELECT * FROM " & TableName
  Dim cmd1 As _
   SqlClient.SqlCommand = _
    ADOObjs.MakeACommand(cnn1, str1)

  'Open cnn1 and create the drd1 DataReader
  'with the ExecuteReader method  cnn1.Open()
  drd1 = cmd1.ExecuteReader _
   (CommandBehavior.CloseConnection)

  Return drd1

首先,它用我的 ADONETObjects 类的 MakeNorthwindConnection 方法创建一个到 Northwind 数据库的连接。其次,我为 DataReader 创建一个 Command 对象。DataReaderForTable 过程将两个参数传递到我的 ADONETObjects 的 MakeACommand 方法,以返回一个新的 Command 对象。这些参数用于为传递到 DataReaderForTable 过程的 TableName 参数中的所有行提取所有列的 SQL 语句,以及 MakeNorthwindConnection 方法返回的 Connection 对象。

在第三步中,该过程用 ExcecuteReader 方法为 Command 对象实际创建 DataReader。使用 CommandBehavior.CloseConnection 枚举,可以使 Button1_Click 过程关闭返回到它的 DataReader,而无须操作关联的 Connection 对象。这是因为枚举指示 .NET Framework 在 DataReader 关闭时自动关闭 Connection 对象。DataReaderForTable 过程通过返回实例化的 DataReader 来结束。

顺便提一下,DataReaderForTable 过程有一个共享访问模式声明,以便整个 HCVSDataReaders 项目的其他模块中的过程可以调用它。

处理 DataReader 数据

至少可以在两个方面改进 ListBox1 的内容。第一,没有 EmployeeID 值来指示 Andrew Fuller 向谁报告。这不是一个错误,因为他不向列表框中的其他人报告。但是,空白仍可能使人产生困惑。第二,ListBox1 按照经理的 EmployeeID 来指定雇员的经理。通过用经理的姓来替代其 EmployeeID,可以提高 ListBox1 内容的可读性。

Button2_Click 过程以使用 Button1_Click 过程处理这两种问题的方法来填充 ListBox1。图 2 显示在单击 Populate from array 按钮后改进的输出。名为 Andrew Fuller 的雇员的行表明他在列表中没有主管。ListBox1 中所有其他雇员的项显示主管的姓而非 EmployeeID。

将主管的 EmployeeID 列值转换为姓时遇到的主要挑战之一是,DataReader 一次只了解某个雇员的一行。为了转换主管的 EmployeeID 列值,应用程序需要将每个 EmployeeID 值链接到姓。通过将来自 DataReader 的值存储到字符串的数组中,过程可以查找与 EmployeeID 值相匹配的姓。(当然,这一特定问题也可以通过在查询中创建一个更复杂的 Select 语句来解决,但是,就演示将数组与 DataReader 配合使用而言,我将为您展示如何在客户端解决这个问题。)

以下 Button2_Click 过程的代码片段显示如何用来自 drd1 DataReader 的值填充字符串值的 MyEmps 数组,这是以 Button1_Click 中的同一方法定义的。

  Const RowsCount As Integer = 99
  Dim MyEmps(RowsCount, 3) As String

  Do While drd1.Read
   If int1 <= RowsCount Then
    For int2 = 0 To drd1.FieldCount() - 1
     Select Case drd1.GetName(int2)
      Case "EmployeeID"
       MyEmps(int1, 0) = drd1(int2)
      Case "FirstName"
       MyEmps(int1, 1) = drd1(int2)
      Case "LastName"
       MyEmps(int1, 2) = drd1(int2)
      Case "ReportsTo"
       'ToString method forces conversion --
       'even for DBNull value to string
       MyEmps(int1, 3) = drd1(int2).ToString
     End Select
    Next
    int1 += 1
   Else
    MessageBox.Show( _
     "Reset RowsCount to a larger number and re-run.", _
     "Terminal Error Message", _
     MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
    Exit Sub
   End If
  Loop

MyEmps 数组有 4 列,用于储存 EmployeeID、FirstName、LastName 和 ReportsTo 列值。对于 Northwind 数据库的默认行数(9 行)来说,其最大的行规格足足有余。阅读行的 While 循环中具有执行各种任务的代码。

For 循环可循环访问所有列值,以便为 MyEmps 中的存储选择 DataReader 列值的一个子集。FieldCount 属性返回 DataReader 的列数。Select...End Select 语句用 GetName 方法检查 DataReader 的列名称,以标识要将列值存储到哪个 MyEmps 列。

除了 ReportsTo 列值以外,这段代码将应用默认的 Visual Basic .NET 转换,将 SQL Server 数据格式转换为 MyEmps 数组中的 .NET 字符串格式。由于 ReportsTo 列可以包含空值 (DBNULL),因此这个过程必须显式指定 ToString 方法,以将 DBNULL 强制为字符串值 — 即,空字符串 ("")。

在收集 MyEmps 数组中的所有 drd1 列值以后,Button2_Click 将关闭 DataReader 并释放这些资源。下面显示的主代码片段将依次通过 MyEmps 的行来计算字符串表达式,以便显示在 ListBox1 中。该代码再一次依次通过 MyEmps 数组来查找匹配 ReportsTo 列值的 LastName 列值,而非只显示行的原始 ReportsTo 列值。在进入循环以将 ReportsTo 列值解码为 LastName 列值之前,该代码会确定第 4 列中的 ReportsTo 值是否为空字符串。

  For int1 = 0 To 99
   If MyEmps(int1, 0) <> "" Then
    If MyEmps(int1, 3) <> "" Then
     strSupvrEmpID = MyEmps(int1, 3)
     For int2 = MyEmps.GetLowerBound(0) To _
      MyEmps.GetUpperBound(0)
      If MyEmps(int2, 0) = strSupvrEmpID Then
       strEmpID = MyEmps(int2, 2)
       Exit For
      End If
     Next
    Else
     strEmpID = " no one in list box"
    End If
    str1 = "EmployeeID" & MyEmps(int1, 0) & _
     ", " & MyEmps(int1, 1) & " " & _
     MyEmps(int1, 2) & " reports to: " & _
     strEmpID
    ListBox1.Items.Add(str1)
   Else
    Exit For
   End If
  Next

处理类型化数据

Form1 的应用程序将每个 DataReader 列的内容转换为一个字符串值,而不管数据源中列的基础数据类型是什么。有时您需要使用原始数据类型,比如当您需要对列值执行数值或数据算法时。如果您还不了解基础数据类型,那么在表达式中使用它们之前,您需要使用一种技术来找出原始数据类型。

报告列名称和数据类型

Form2 中的 Button1_Click 过程演示了一种写出任何 DataReader 的列名称和数据类型的技术。尽管 .NET 为此任务提供了其他方法,但该技术构建于您对 DataReader 的了解以及如何将它们与数组一起使用的基础之上。该过程首先根据 Form1 中的 DataReaderForTable 过程,为 Northwind 数据库中的 Orders 表创建 DataReader。由于 DataReaderForTable 过程是用共享访问模式声明的,因此 Form2 可用以下代码调用:

  Dim drd1 As SqlClient.SqlDataReader = _
   Form1.DataReaderForTable("Orders")

您还需要一个数组来保存 drd1 DataReader 的列名称和数据类型。数组列将保存为 drd1 DataReader 指定列的名称及其数据类型的字符串值。下面的代码摘录显示了如何应用 Array 类的 CreateInstance 共享方法,以创建一个名为 OrdersColNamesTypes 的数组。在 drd1 DataReader 中有多少列,这个数组就有多少行,另外还有两个列。For 循环可循环访问 DataReader 的列,以便用列名称和数据类型元数据填充该数组。SetValue 方法为数组元素指定值。您可以从上述示例了解如何使用 GetName 方法返回列名称。这个过程阐释了如何应用 GetDataTypeName 方法来恢复 DataReader 中列的原生数据类型名称。

  Dim OrdersColNamesTypes As Array = _
   Array.CreateInstance(GetType(String), _
   drd1.FieldCount, 2)

  For int1 As Integer = 0 To drd1.FieldCount - 1
   OrdersColNamesTypes.SetValue _
    (drd1.GetName(int1), int1, 0)
   OrdersColNamesTypes.SetValue _
    (drd1.GetDataTypeName(int1), int1, 1)
  Next

如您在图 3 中看到的那样,Form2 上的 Button1_Click 过程的最后代码片段只是依次通过 OrdersColNamesTypes 数组中每个连续行的列值,并将列名称和数据类型打印到“Output”窗口。图中的报表表明 Order 表有 14 列。Order 表的第一列名为 OrderID,数据类型为 SQL Server int。其他列包含变化和固定长度的字符串数据类型(nvarchar 和 nchar)以及 datetime 和 money 数据类型。

执行算法

对 DataReader 列值执行算法的窍门是,将它们保存为与其原生数据库数据类型相匹配的 Visual Basic .NET 数据类型。不过,数组会将所有元素成员强制为同一类型。使用数组存储 DataReader 的值、但仍保持数据源数据类型的一种方法是,将 DataReader 列值保存到一个具有 Object 数据类型元素的数组中。从本质上说,这个过程将 DataReader 列值装箱 为不将其强迫为另一种数据类型的 Object 实例。稍后,您可以通过将数组元素指定给用适当数据类型声明的变量,来恢复基本的基础数据格式。从本质上说,这个指定取消装箱 了数据类型。

Form2 之后的代码包括一个过程 — PopArray,它将 DataReader 列值装箱到一个带有 Object 元素的数组中。如果您对这个过程的详细信息感兴趣,请查看 HCVSDataReaders 项目中的 PopArray 列表。

在本文中,PopArray 过程的一个主要目的是,用 Windows 应用程序中的 Orders 表的列值来演示 integer 和 datetime 算法。Form2 的 Button2_Click 过程有两个主代码片段。第一个演示了如何计算 Orders 数组中第一行和最后一行 OrderID 列值之间的差,这将镜像 Northwind 数据库中的 Orders 表。在开始执行第一个主代码片段之前,该过程会调用 PopArray 过程来填充 Orders 数组。如果您想知道的话,Orders 表有 830 行。对名为 int1 和 int2 的两个变量的指定为 Orders 数组第一列中的第一行和最后一行取消装箱了 OrderID 列值。WriteLine 方法的参数包括一个从其他 Integer 变量中减去一个 Integer 变量的简单表达式。

  Dim Orders As Array = PopArray("Orders", 830)

  Dim int1 As Integer = _
   Orders(Orders.GetLowerBound(0), _
   Orders.GetLowerBound(1))
  Dim int2 As Integer = _
   Orders(Orders.GetUpperBound(0), _
   Orders.GetLowerBound(1))
  Console.WriteLine(ControlChars.CrLf & _
   "An example with integer arithmetic:")
  Console.WriteLine( _
   "There are {2} order numbers between " & _
   "the first order number({0}) and the " & _
   "last order number({1})", _
   int1, int2, int2 - int1)

Button2_Click 的第二个主代码片段对 Orders 数组第一行中的 ShippedDate 和 RequiredDate 列值执行 datetime 算法。这段代码将两列取消装箱为 Date 数据类型,而不是将 Object 元素取消装箱为 Integer 数据类型的变量。您可以交替使用 Date 和 Datetime 关键字,在 Visual Basic .NET 中指定 datetime 值。DateDiff 函数计算两个 datetime 变量之间的天数差。Console 类的 WriteLine 方法将结果显示在“Output”窗口中。

  'Demonstrate arithmetic with dates
  Dim datRequired As Date = Orders(0, 4)
  Dim datShipped As Date = Orders(0, 5)
  Console.WriteLine(ControlChars.CrLf & _
   "An example with date arithmetic")
  Console.WriteLine( _
   "Required date({1}) - ShippedDate({0}) " & _
   "= {2} days", _
   datShipped.ToString("M/d/yyyy"), _
   datRequired.ToString("M/d/yyyy"), _
   DateDiff(DateInterval.Day, datShipped, _
   datRequired))

生成分层数据

应用程序要求分层数据(如属于某个订单的行项)是很常见的。最后的两个示例展示了两种通过 DataReader 返回分层数据的方法。一种方法演示了如何使用专用的 MSDataShape 提供程序。第二种方法在本文前面所演示的工具的基础上,使用了更多常规工具。另外,通过在相关表中添加值的查找功能以及阐释 datetime 和 currency 值的格式化语法,第二种技术可以建立在第一种之上。

使用 MSDataShape 提供程序

正如我之前指出的,MSDataShape 提供程序是一种用于返回分层数据的专用提供程序。这个提供程序要回溯到 Visual Basic 6,但 Microsoft 发表了一篇知识库文章,描述如何在 Visual Basic .NET 和 ADO.NET 中使用 MSDataShape 提供程序 (http://support.microsoft.com/default.aspx?scid=kb;[LN];308045)。虽然 MSDataShape 提供程序在返回分层结果集方面格外有效,但它依赖于 SQL 的子集以及专用关键字和其他语法约定。另外,这个提供程序不能与 .NET SQL Server 数据提供程序一起使用。相反,您将被迫改为使用 OleDb .NET 数据提供程序 — 即使是在处理 SQL Server 数据库的时候。

使用 MSDataShape 提供程序建立到数据库的连接略有不同。下面的代码来自 Form3 中的 Button1_Click 过程。请注意,该代码在 OleDb 命名空间中指定了一个 Connection 对象。尽管服务器、集成安全性和初始目录的最后 3 个参数与 SqlConnection 对象连接字符串的那些参数一样,但最初的两个参数截然不同。最前面的参数指定了 MSDataShape 提供程序,该提供程序与第二个参数中指定的 SQLOLEDB 数据提供程序协同工作。

   New OleDb.OleDbConnection( _
   "Provider=MSDataShape;Data Provider=SQLOLEDB;" & _
   "server=(local);Integrated Security=SSPI;" & _
   "Initial Catalog=northwind")

接下来的 3 个代码块阐释了指定 Command 对象的语法,该对象基于 Northwind 数据库的 Orders 表和 Order Details 表生成分层结果集。

  Dim cmd1 As OleDb.OleDbCommand = _
   New OleDb.OleDbCommand( _
   "SHAPE {SELECT OrderID, OrderDate " & _
   "FROM Orders " & _
   "WHERE OrderID=" & TextBox1.Text & "} " & _
   "  APPEND ({SELECT OrderID, ProductID, " & _
   "UnitPrice, Quantity, Discount " & _
   "FROM [Order Details]} " & _
   "  RELATE OrderID TO OrderID)", cnn1)

  cnn1.Open()
  Dim drd1 As OleDb.OleDbDataReader = _
   cmd1.ExecuteReader(CommandBehavior.CloseConnection)
  drd1.Read()
  Console.WriteLine("{0}, {1}", _
   drd1(0), drd1(1))

  Dim drd2 As OleDb.OleDbDataReader = drd1(2)
  Do While drd2.Read
   Console.WriteLine("{0}, {1}, {2}, {3}, {4}", _
    drd2(0), drd2(1), drd2(2), drd2(3), drd2(4))
  Loop

请注意专用关键字 SHAPE、APPEND 和 RELATE。SHAPE 子句的 SQL 语句指定主结果集的行。这个语句引用 TextBox1 的 Text 属性,该属性应该始终指定有效的 OrderID 列值。APPEND 子句的 SQL 语句指定分层结果集的明细成员的结果集。RELATE 子句指示在哪些列上匹配主数据源和明细数据源中的行。

在实例化 Command 对象后,代码将通过打开 cnn1 Connection 对象来准备生成几个 DataReader。drd1 DataReader 从主数据源返回数据,drd2 DataReader 从明细数据源提取数据。主数据源的 Console.WriteLine 语句打印主数据源的前两个列值,它们是 OrderID 和 OrderDate。明细数据源的 Console.WriteLine 语句打印 Order Details 表的所有行,其 OrderID 匹配 TextBox1 中显示的值。

图 4 显示了在单击 Shape 按钮后的 Form3。窗体下的“Output”窗口表示分层结果集。第一行显示主数据源的行,包括 OrderID 和 OrderDate 列值。接下来的 3 行显示 OrderID 值为 10248 的订单的明细行项目。第二列和第三列是用于 ProductID 和 UnitPrice 列值的。打印 ProductID 列值(而非 ProductName 列值)使得辨别每个行项目引用了哪个产品变得更困难。此外,从输出不能明显看出 UnitPrice 列值是货币值。

用常规工具返回分层结果集

返回分层数据的第二个示例依赖于常规工具,如那些已经在本文中展示过的工具的改编本。第二个示例的详细代码显示在 Button2_Click 过程中,以及 HCVSDataReaders 项目的 Form3 模块中名为 ComputerArrayIndex 的相关过程中。返回分层数据的第二种方法基于 Northwind 数据库中的 Orders、Order Details 和 Products 表的关联 DataReader,创建了三个数组。以这种方法使用数组可以减少数据库服务器上的负载,这是因为它允许应用程序关闭 DataReader 及其到数据源的关联 Connection。

下面的代码片段来自 Form3 模块中的 Button2_Click,它阐释了用来生成 Orders 数组的方法。Form2 中的 PopArray 过程已经在前面简要描述过了。它基于 Northwind 数据库的 DataReader 生成数组。您将要阅读的最大行数以及表名称传递给它。顺便说一下,PopArray 过程在填充数组后会关闭它的 DataReader。ComputeArrayIndex 过程从一个二维数组(如 Orders)的第一列生成一个一维数组 — IdxOrders。

   Dim intMaxOrdersRows = 830
   Orders = Form2.PopArray("Orders", _
    intMaxOrdersRows)
   IdxOrders = ComputeArrayIndex(Orders, _
    intMaxOrdersRows)

一维索引数组可以加速二维数组中行的查找,其速度快于在二维数组中扫描所有行,以查找匹配某个条件的值。这是因为 Visual Basic .NET 为它的 Array 类提供了一个 IndexOf 共享方法,该方法可返回与一维数组中的某个值相对应的索引。下面的代码示例显示了将此方法用于 IdxOrders 数组以便从 Orders 数组恢复 OrderID 和 OrderDate 列值的语法。该代码片段还设置了 OrderDate 列值的格式,以排除 datetime 值的不相关时间段。

  Dim intIdx As Integer = _
   Array.IndexOf(IdxOrders, _
   Integer.Parse(TextBox1.Text))
  Console.WriteLine("{0}, {1}", _
   Orders(intIdx, 0), _
   DateTime.Parse( _
   Orders(intIdx, 3)).ToString("M/dd/yy"))

图 5 显示了图 4 中出现的 OrderID 值在 Button2_Click 过程中的最终输出。请注意,此过程执行对 ProductID 值的查询,并改为显示 ProductName 列值。基于 ProductID 列值恢复 ProductName 列值的查找逻辑,是本文第二个示例中基于 ReportsTo 列值查找 LastName 列值的代码以及上述代码片段的扩展。将 UnitPrice 的格式设置为货币值的方法只需调用常见的 FormatCurrency 函数。虽然您可以使用更为可靠的方法来设置货币值的格式,但知道 Visual Basic .NET 支持常见且易于使用的 FormatCurrency 函数也是很好的。

小结

对于对远程数据源的数据访问来说,DataReader 是一种快速、灵活且强大的工具。本文突出说明了 .NET 应用程序中 DataReader 的 3 个特定类型的应用程序,实际上还有许多其他的应用程序。在您的自定义解决方案中使用 DataReader 可以使这些方案运行得更快,甚至还可能会加强您的 .NET 基本开发技能。通过将 DataReader 与数组协同使用,您通常能够从其获得附加价值。

Download 407DOBSON.ZIP

有关 Hardcore Visual Studio 和 Pinnacle Publishing 的详细信息,请访问其网站 http://www.pinpub.com/

注:这不是 Microsoft Corporation 的网站。Microsoft 对该网站的内容不承担责任。

本文是从 Hardcore Visual Studio 2004 年 7 月号转载的。版权所有 2004,Pinnacle Publishing, Inc.(除非另行说明)。保留所有权利。Hardcore Visual Studio 是 Pinnacle Publishing, Inc. 独立发行的产品。未经 Pinnacle Publishing, Inc. 事先同意,不得以任何形式使用或复制本文的任何部分(评论文章中的简短引用除外)。要联系 Pinnacle Publishing, Inc.,请致电 1-800-788-1900。

转到原英文页面

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值