深入.NET DataTable

 

1、ADO.NET相关对象一句话介绍

1)DataAdapter:

DataAdapter实际是一个SQL语句集合,因为对Database的操作最终需要归结到SQL语句。

2)Dataset:

DataSet可以理解成若干DataTable的集合,DataSet在内存里面维护一个表集合包括表间关系。对于.NET Framework 2.0之前的版本,DataSet在ADO.NET中拥有至关重要的作用,但在其后的版本中,由于DataTable类的完备(例如与XML相关的几个方法以及Merge方法),其作用稍有削弱,甚至于有些情况下你去初始化一个DataSet对象本身就是多余的。

3)DataView:

与数据库中的视图在概念上是类似的。DataView本身并不真正包含数据行,而只是包含指向源DataTable中数据行的引用,这一点你可以通过object.ReferenceEquals()方法来验证。

4)DataTable:

ADO.NET的核心对象。它是位于内存中的一张表,是你执行SQL查询之后的结果集,可以形象地把它理解为一张包含若干行若干列的表格。

 

2、如何更新数据到Database

从本质上来说,你对Database操作总是归结到SQL语句,但是从表面上我们可以作一点区分,

1)直接使用SQL命令

在.NET中,最常见的是拼接SQL字符串,使用Command对象来执行此命令以达到操作Database的目的,例如, 

 

string sql = "update table1 setfvalue=" + this.textBox1.Text + " where fname='x'";

SqlCommand cmd = new SqlCommand(sql,conn);

cmd.ExecuteNonQuery();

 

这是一种最直接浅显的方式,因为SQL语句就在你眼前,反过来说,这需要你对SQL命令有一定的了解。

 

2)使用DataAdapter.Update()

另外一种方式,是使用DataAdapter.Update()方法,这并不是说我们不需要SQL语句了,只是SQL语句拼接的工作已经交给了DataAdapter(实际上是交给了CommandBuilder)来完成(以参数的形式),例如,  

 

string c = "select fname,fvalue fromtable1";

SqlCommand cmd = new SqlCommand(c,conn);

SqlDataAdapter da = newSqlDataAdapter(cmd);

SqlCommandBuilder scb = newSqlCommandBuilder(da); //(1)

DataTable dt = new DataTable();

da.Fill(dt);

dt.Rows[0].Delete();//(2)

da.Update(dt);

在这里,你看不到SQL语句,因为在你初始化SqlCommandBuilder的过程中,将自动根据表结构(基于你的Select语句)构造insert,update,delete语句。对于上面的代码,你可以获得SQL语句内容,

DELETE FROM [table1] WHERE (([fname] = @p1)AND ((@p2 = 1 AND [fvalue] IS NULL) OR ([fvalue] = @p3)))

而执行时候,会传入相应的参数值,

exec sp_executesql N'DELETE FROM [table1]WHERE (([fname] = @p1) AND ((@p2 = 1 AND [fvalue] IS NULL) OR ([fvalue] =@p3)))',N'@p1 varchar(1),@p2 int,@p3 int',@p1='a',@p2=0,@p3=100

xec sp_executesql N'DELETE FROM [table1]WHERE (([fname] = @p1) AND ((@p2 = 1 AND [fvalue] IS NULL) OR ([fvalue] =@p3)))',N'@p1 varchar(1),@p2 int,@p3 int',@p1='b',@p2=1,@p3=NULL

由于表中只有两个列,列fname为主键列,fvalue列可空,至于为什么会出现三个参数,看看上面的SQL你就会明白了。

以下则分别是update语句、insert语句,

UPDATE [table1] SET [fname] = @p1, [fvalue]= @p2 WHERE (([fname] = @p3) AND ((@p4 = 1 AND [fvalue] IS NULL) OR ([fvalue] =@p5)))

INSERT INTO [table1] ([fname], [fvalue])VALUES (@p1, @p2)

另外,上述C#代码中的dt.Rows[0].Delete()行写在这里只是示例作用,实际的系统中,你可能会有一个叫“Delete”的按钮,这样你可以在按钮的事件中执行Delete()操作,然后叫某个叫“Save”的按钮里写上Update(),这很常见,不多说了。

再另外,由于这些语句的构造过程中依赖于你的Select语句,所以你的Select语句中必须包含主键列,否则无法正常生成其它SQL命令。

以下我们的讨论,将主要针对第二种方式,即使用Update()进行数据更新过程中涉及的各种问题。

 

3、行状态

为了后续的数据操作,DataTable中引入了一个“行状态”的概念(事实上该属性属于DataRow类)。每一个DataRow都有一个状态标志,你可以通过DataTable.Rows[i].RowState查看,对DataRow的不同操作将导致该行处于不同的状态,同时,不同的状态又导致保存数据时的不同行为。参见下图,

 

 

1)初始状态差异

从数据库中查询并通过DataAdapter.Fill()方法填充的DataTable,其所有行的状态初始都为Unchanged(我们可以认为在Fill()方法的内部调用了AcceptChanges()方法),然而对于在程序中手工构造并添加的数据行,在未接受AcceptChanges()方法前,都为Added(行状态的不同在DataTable中是一个比较隐蔽的但又需要十分关注的问题,后续会有相应的说明),参见以下代码。 

 

private void button1_Click(object sender,EventArgs e)

{

      try

      {

              dataAdapter1.Fill(dt);

              DataRowState s =dt.Rows[0].RowState;//unchanged

      }

      catch

      {

      }

}

private void button2_Click(object sender,EventArgs e)

{

      DataTable dt = new DataTable();

      dt.Columns.Add("fname");

      dt.Columns.Add("fvalue");

      dt.Rows.Add("zhang", 100);

      DataRowState s = dt.Rows[0].RowState;//added

}

 

 2)理解Delete()

此方法并未真正移除DataRow(除非此行原状态为Added),而只是将RowState状态变成了Deleted(当然这会导致你无法使用正常的索引方式访问此行的数据)。对于Added状态的行执行Delete()操作,将导致DataTable行数减少,这点需要注意,因为它可能导致你在使用for循环遍历时出现索引越界异常。 

 

private void button7_Click(object sender,EventArgs e)

{

      DataTable dt = new DataTable();

      dt.Columns.Add("fname");

      dt.Columns.Add("fvalue");

      dt.Rows.Add("zhang", 100);

      //

      dt.Rows[0].Delete();

      int c = dt.Rows.Count;//0

}

private void button8_Click(object sender,EventArgs e)

{

      DataTable dt = new DataTable();

      dt.Columns.Add("fname");

      dt.Columns.Add("fvalue");

      dt.Rows.Add("zhang", 100);

      //

      dt.AcceptChanges();

      dt.Rows[0].Delete();

      int c = dt.Rows.Count;//1

}

 

3)Exception:Deleted row information cannotbe accessed through the row. 

 

private void button8_Click(object sender,EventArgs e)

{

      DataTable dt = new DataTable();

      dt.Columns.Add("fname");

      dt.Columns.Add("fvalue");

      dt.Rows.Add("zhang", 100);

      //

       dt.AcceptChanges();

      dt.Rows[0].Delete();

      DataRow dr = dt.Rows[0]; //No error

      object o = dt.Rows[0]["fvalue"];//Exception,row can be accessed,but row datacannot

}

 

4)理解AcceptChanges()

此方法容易给人误解,以为在调用它之后对DataTable所做的所有更改将会被提交到Database。事实上,此方法跟Database没有直接的关系(注意),它只直接影响各DataRow的RowState(具体地说来是将所有状态为Deleted的行真正移除,所有状态为Added或Modified的行都变成Unchanged)。与Database有直接相关的是DataAdapter.Update()方法,它是真正负责执行相关SQL命令的地方。

但是,从另一方面来说,没有直接的影响,言外之意就是有间接的影响,由于它影响了所有DataRow的RowState,而DataAdapter.Update()方法在执行SQL命令时必须依据RowState以确定使用insert、update、或delete 命令。举个例子,如果你在DataAdapter.Update()调用之前执行AcceptChanges()方法,这将阻止所有对Database的更改,因此对这两个方法调用的顺序应有充分的考虑。

另外,DataSet、DataTable、DataRow都有AcceptChanges()方法,这些方法除了影响的范围大小不同之外,没有本质的区别。

 

5)DataRowState与Update()

不同的数据行状态,将导致最终DataAdapter.Update()出现不同的行为,例如对于Added状态的行,将导致insert操作、Modified状态将导致update操作、Deleted状态将导致delete操作。

 

6)使用DataRowState

除了Update()方法内部使用DataRowState外,在我们自己写的代码中,也可以将它与GetChanges()方法配合使用,以获取DataTable的当前变化,参见以下代码,在你获得所有发生更新的行后,实际上你可以自己构造Update SQL命令,而不使用CommandBuilder,当然这需要用到稍后会提到的DataRowVersion。  

 

private void button4_Click(object sender,EventArgs e)

{

      DataTable dt = new DataTable();

       dt.Columns.Add("fname");

      dt.Columns.Add("fvalue");

      dt.Rows.Add("zhang", 100);

      dt.AcceptChanges();

      dt.Rows[0]["fvalue"] = 101

      //get all Modified rows,then you can use UPDATE SQL to save data.

      DataTable dt1 = dt.GetChanges(DataRowState.Modified);

}

 

7)状态Detached

除了上图中给出的几种行状态外,还有一种特殊的状态Detached,这种状态表示已初始化但未添加到DataTable中的数据行,此状态我们不必太关心。参见,  

 

private void button3_Click(object sender,EventArgs e)

{

      DataTable dt = new DataTable();

      dt.Columns.Add("fname");

      dt.Columns.Add("fvalue");

      DataRow dr = dt.NewRow();

      DataRowState s = dr.RowState;//detached

}

 

 

4、行状态、行版本、行数据版本

行版本(DataRowVersion)描述数据行的版本;

行数据版本(DataViewRowState)描述数据行中数据的版本。

这两个概念令人困惑,我认为可以仅仅从用法上对它们进行了解,毕竟我们使用它们的机会并非很大。  

 

1)使用DataRowVersion

关于DataRowVersion,以状态为Modified的行为例,它包含两个DataRowVersion(即存储了两行数据):Current,Original,分别存储该行修改后与修改前的数据,也就是说,行版本实际可以帮助RejectChanges()等方法实现一个类似于“回滚”的功能。 

 

private void button4_Click(object sender,EventArgs e)

{

      DataTable dt = new DataTable();

      dt.Columns.Add("fname");

      dt.Columns.Add("fvalue");

      dt.Rows.Add("zhang", 100);

      dt.AcceptChanges();

      dt.Rows[0]["fvalue"] = 101;

      int i = Convert.ToInt32(dt.Rows[0]["fvalue",DataRowVersion.Original]);//100

      int i2 = Convert.ToInt32(dt.Rows[0]["fvalue",DataRowVersion.Current]);//101

}

同理你可以借助DataRowVersion来访问Deleted的数据,前面我们提到了对于Deleted的数据,使用dt.Rows[0]["fvalue"]访问将引发异常,可以使用

dt.Rows[0]["fvalue",DataRowVersion.Original]。

 

2) DataRowVersion与Update()

现在我们回想一下,当我们使用CommandBuilder构造完Update,Insert,Delete命令之后,那些SQL命令中的参数怎么办?我们知道在SQL命令执行之前,我们必须为所有输入参数指定参数值,那么Update()方法内部是如何工作的?这就有赖于DataRowVersion了。

 

我们可以简单看一下Update()方法使用过程中涉及的相关.NET源码,

System.Data.Common.DbDataAdapter

protected virtual int Update(DataRow[]dataRows, DataTableMapping tableMapping);

在Update()方法中,调用了ParameterInput(),下面是该方法的摘要, 

  

System.Data.Common.DbDataAdapter

private voidParameterInput(IDataParameterCollection parameters, StatementType typeIndex,DataRow row, DataTableMapping mappings)

{

   foreach (IDataParameter parameter in parameters)

    {

       if (column != null)

        {

           DataRowVersion parameterSourceVersion =GetParameterSourceVersion(typeIndex, parameter);

           parameter.Value = row[column, parameterSourceVersion];

       }

    }

}

在ParameterInput()方法中,调用了GetParameterSourceVersion()方法, 

 

System.Data.Common.DbDataAdapter

private static DataRowVersionGetParameterSourceVersion(StatementType statementType, IDataParameterparameter)

{

   switch (statementType)

    {

       case StatementType.Select:

       case StatementType.Batch:

           throw ADP.UnwantedStatementType(statementType);

       case StatementType.Insert:

           return DataRowVersion.Current;

       case StatementType.Update:

           return parameter.SourceVersion;

       case StatementType.Delete:

           return DataRowVersion.Original;

    }

   throw ADP.InvalidStatementType(statementType);

}

以行被更新的情况为例,在为参数的赋值的过程中,系统会将相应要更新的DataRow一并传入,同时对于Update语句,

UPDATE [table1] SET [fname] = @p1, [fvalue]= @p2 WHERE (([fname] = @p3) AND ((@p4 = 1 AND [fvalue] IS NULL) OR ([fvalue] =@p5)))

我们要了解的一点是,5个参数中@p1,@p2是一类,@p3, @p5是一类,它们的区别在于,前一类的SourceVersion是Current,而后一类的SourceVersion是Original,这在上述的GetParameterSourceVersion()方法中被用到,所以!!,针对传入的需要更新的DataRow,Update()方法内部将使用当前值(即修改后的值)填充@p1,@p2,而使用原始值(即修改前的值)填充@p3, @p5。Insert,delete同理。

 

3)理解DataRowVersion.Default

对于Added、Modified、Deleted状态的行,其Default版本实际是Current版本,对于Unchanged则无甚区别。

 

4)使用DataViewRowState

(1)配合DataTable.Select() 

 

private void button9_Click(object sender,EventArgs e)

{

      DataTable dt = new DataTable();

      dt.Columns.Add("fname");

      dt.Columns.Add("fvalue");

      dt.Rows.Add("zhao", 100);

      dt.Rows.Add("qian", 100);

      dt.Rows.Add("sun", 100);

      dt.AcceptChanges();

      //

      dt.Rows[1]["fvalue"] = 101;

      dt.Rows[2].Delete();

      dt.Rows.Add("li", 100);

      //object o = dt.Rows[2]["fvalue", DataRowVersion.Original];

      //

      StringBuilder sb = new StringBuilder();

      DataRow[] drs = dt.Select(null, null, DataViewRowState.Added);

      sb.AppendLine("-----------------------------------------------");

      sb.AppendLine("Added:");

      for (int i = 0; i < drs.Length; i++)

      {

             sb.AppendLine(drs[i]["fname"].ToString() +": "+drs[i]["fvalue"].ToString());

      }

      drs = dt.Select(null, null, DataViewRowState.CurrentRows);

      sb.AppendLine("-----------------------------------------------");

      sb.AppendLine("CurrentRows:");

      for (int i = 0; i < drs.Length; i++)

      {

             sb.AppendLine(drs[i]["fname"].ToString() + ": " +drs[i]["fvalue"].ToString());

      }

      drs = dt.Select(null, null, DataViewRowState.Deleted);

      sb.AppendLine("-----------------------------------------------");

      sb.AppendLine("Deleted:");

      for (int i = 0; i < drs.Length; i++)

      {

             sb.AppendLine(drs[i]["fname", DataRowVersion.Original].ToString()+ ": " + drs[i]["fvalue",DataRowVersion.Original].ToString());

      }

      drs = dt.Select(null, null, DataViewRowState.ModifiedCurrent);

      sb.AppendLine("-----------------------------------------------");

      sb.AppendLine("ModifiedCurrent:");

      for (int i = 0; i < drs.Length; i++)

      {

             sb.AppendLine(drs[i]["fname"].ToString() + ": " +drs[i]["fvalue"].ToString());

      }

      drs = dt.Select(null, null, DataViewRowState.ModifiedOriginal);

      sb.AppendLine("-----------------------------------------------");

      sb.AppendLine("ModifiedOriginal:");

      for (int i = 0; i < drs.Length; i++)

      {

             sb.AppendLine(drs[i]["fname"].ToString() + ": " +drs[i]["fvalue"].ToString());

      }

       drs = dt.Select(null, null,DataViewRowState.OriginalRows);

      sb.AppendLine("-----------------------------------------------");

      sb.AppendLine("OriginalRows:");

      for (int i = 0; i < drs.Length; i++)

      {

              if (drs[i].RowState ==DataRowState.Deleted)

              {

                    sb.AppendLine(drs[i]["fname",DataRowVersion.Original].ToString() + ": " +drs[i]["fvalue", DataRowVersion.Original].ToString());

              }

              else

              {

                     sb.AppendLine(drs[i]["fname"].ToString()+ ": " + drs[i]["fvalue"].ToString());

              }

      }

      drs = dt.Select(null, null, DataViewRowState.Unchanged);

      sb.AppendLine("-----------------------------------------------");

      sb.AppendLine("Unchanged:");

      for (int i = 0; i < drs.Length; i++)

      {

             sb.AppendLine(drs[i]["fname"].ToString() + ": " +drs[i]["fvalue"].ToString());

      }

      MessageBox.Show(sb.ToString());

}

 

结果输出:

-----------------------------------------------

Added:

li: 100

-----------------------------------------------

CurrentRows:

zhao: 100

qian: 101

li: 100

-----------------------------------------------

Deleted:

sun: 100

-----------------------------------------------

ModifiedCurrent:

qian: 101

-----------------------------------------------

ModifiedOriginal:

qian: 101

-----------------------------------------------

OriginalRows:

zhao: 100

qian: 101

sun: 100

-----------------------------------------------

Unchanged:

zhao: 100

 

(2)配合DataView.RowFilter  

 

private void button10_Click(object sender,EventArgs e)

{

      DataTable dt = new DataTable();

      dt.Columns.Add("fname");

      dt.Columns.Add("fvalue");

      dt.Rows.Add("zhao", 100);

      dt.Rows.Add("qian",100);

      dt.Rows.Add("sun", 100);

      dt.AcceptChanges();

      //

      dt.Rows[1]["fvalue"] = 101;

      dt.Rows[2].Delete();

      dt.Rows.Add("li", 100);

      //

      DataView dv = new DataView(dt);

      dv.RowStateFilter = DataViewRowState.Added |DataViewRowState.ModifiedCurrent;

      StringBuilder sb = new StringBuilder();

      sb.AppendLine("-----------------------------------------------");

      sb.AppendLine("Added & ModifiedCurrent:");

      for (int i = 0; i < dv.Count; i++)

      {

             sb.AppendLine(dv[i]["fname"].ToString() + ": " +dv[i]["fvalue"].ToString());

      }

      sb.AppendLine("-----------------------------------------------");

      MessageBox.Show(sb.ToString());

}

//-----------------------------------------------

Added & ModifiedCurrent:

qian: 101

li: 100

-----------------------------------------------

 

5)DataViewRowState中的“复合版本”

DataViewRowState包含多个枚举成员,我可以给出每个枚举成员对应的int值,

Added         4

CurrentRow               22

Deleted               8

ModifiedCurrent  16

ModifiedOriginal         32

None                  0

OriginalRow              42

Unchanged          2

你可以发现,其中的两个状态CurrentRow、OriginalRow实际是经由其它几种状态二进制或运算的结果,

CurrentRow=Added|ModifiedCurrent|Unchanged,

OriginalRow=Deleted|ModifiedOriginal|Unchanged。

 

5、了解其它几个方法

1)Delete()、Remove()、Clear()

DataRow.Delete();

DataRowCollection.Remove();

DataTable.Clear()、DataSet.Clear()

正如前面所述,对于DataRow的Delete()方法,其内部的处理并未真正删除此行,而只是将行标识为Deleted,并“移除”了它的Current版本。这样,当使用DataAdapter的Update()进行更新时,其内部机制可以根据仍然存在的Original版本数据,为DeleteCommand填充参数,完成更新数据库的操作。

而Clear()方法则完全删除了堆上的数据行对象,并且将对数据引用置空(这点可以参见Clear()方法的反编译代码),这种情况下无法生成可执行的DeleteCommand,这就是说,当你用Clear()方法“清空”DataTable后,使用Update()方法并不能像你预想的一样将对应的数据库表数据删除。

另外,需要注意一点是Delete()并不导致数据行减少(除非原行是Added状态),当然,如果是对Added状态的行执行Delete(),则导致行数减少。当你使用for循环时,这可能会造成问题。

另外,我们还有一个方法:DataRowCollection.Remove(),其作用类似于Clear(),是彻底地移除行,假设你是使用DataAdapter.Update()方法更新Database,那么你将没有机会将你的删除操作同步到Database中。

 

2)Copy()、Clone()

.NET中有两类拷贝,浅拷贝(Shadow copy)、深拷贝(Deep copy),对于大多数我们所见的类(比如常见的集合类等等),没有深拷贝方法,大多数会有一个浅拷贝方法Clone()。我唯一所见的一个深拷贝方法是DataTable.Copy(),同时,DataTable.Clone()方法也比较特殊,因为它并非是浅拷贝方法,而是拷贝DataTable的结构(不包含数据)。

顺便提一下深、浅拷贝的区别,浅拷贝创建原对象类型的一个新实例,复制原对象的所有值类型成员,对于引用类型成员,则只复制该值的指针。深拷贝则复制原对象的所有成员,对于引用类型成员,亦复制其所指的堆上的对象。  

 

static void Main(string[]args)

{

      DataTable dt = new DataTable();

      dt.Columns.Add("fname", typeof(System.String));

      dt.Columns.Add("fvalue", typeof(System.Int32));

      for (int i = 1; i <= 10; i++)

 

      {

              dt.Rows.Add("p" + i,i);

      }

      dt.AcceptChanges();

      //

       DataTable dtc = dt.Copy();

      //

      bool b = object.ReferenceEquals(dt.Rows[0], dtc.Rows[0]);//false

      DataRowState s = dtc.Rows[0].RowState;//Unchanged

      //Clone() and ImportRow()

      DataTable dtc2 = dt.Clone();

      for (int i = 0; i < 5; i++)

      {

              dtc2.ImportRow(dt.Rows[i]);

      }

      bool b2 = object.ReferenceEquals(dt.Rows[0], dtc2.Rows[0]);//false

      DataRowState s2 = dtc2.Rows[0].RowState;//Unchanged

      //ItemArray

      DataTable dtc3 = dt.Clone();

      for (int i = 0; i < 5; i++)

      {

             dtc3.Rows.Add(dt.Rows[i].ItemArray);

              //dtc3.Rows.Add(dt.Rows[i]);//runtime exception

      }

      bool b5 = object.ReferenceEquals(dt.Rows[0], dtc3.Rows[0]);//false

      DataRowState s5 =dtc3.Rows[0].RowState;//Added

      //

      ArrayList al = new ArrayList();

      al.Add("xy");

      ArrayList alc = al.Clone() as ArrayList;

      if (alc != null)

      {

              bool b3 =object.ReferenceEquals(al, alc);//false

              bool b4 =object.ReferenceEquals(al[0], alc[0]);//true

      }

}

 

3)Select()、Compute()

这两个方法在很多情况下都有助于你简化代码,避免每一次使用循环遍历DataTable,参见以下,

对于这两个方法中可用的表达式,参见,

http://msdn.microsoft.com/en-us/library/system.data.datacolumn.expression.aspx 

 

class Program

{

      static void Main(string[] args)

      {

              DataTable dt = new DataTable();

             dt.Columns.Add("fname",typeof(System.String));

             dt.Columns.Add("fvalue",typeof(System.Int32));

              for (int i = 1; i <= 10; i++)

              {

                     dt.Rows.Add("p"+ i, i);

              }

              dt.AcceptChanges();

              //

              DataRow[] drs =dt.Select("fvalue>6");

              PrindRows(drs);

              //

              drs = dt.Select("fvalue>6and fvalue<9");//AND OR NOT

              PrindRows(drs);

              //

              drs = dt.Select("fname like'p1%'");

              PrindRows(drs);

              //

              drs = dt.Select("fname in('p1','p3')");//< > <= >= <> = IN LIKE

              PrindRows(drs);

              //

              drs =dt.Select("fvalue=max(fvalue)");//SUM AVG MIN MAX COUNT STDEV VAR

              PrindRows(drs);

              //

              drs = dt.Select("fvalue%2=0");//+- * / %

              PrindRows(drs);

              //

              drs =dt.Select("len(fname)=3");//LEN(expression) ISNULL(expression,replacementvalue) IIF(expr, truepart, falsepart) TRIM(expression)SUBSTRING(expression, start, length)

             PrindRows(drs);

 

              object o =dt.Compute("count(fname)", "fvalue>6");//4

              Console.WriteLine(o.ToString());

              Console.Read();

      }

 

      static void PrindRows(DataRow[] pDrs)

      {

              Console.WriteLine("----------------------------------");

              foreach (DataRow dr in pDrs)

              {

                    Console.WriteLine(dr["fname"].ToString().PadRight(8, ' ') +dr["fvalue"].ToString());

              }

              Console.WriteLine("----------------------------------");

      }

}

 

----------------------------------

p7     7

p8     8

p9     9

p10    10

----------------------------------

 

----------------------------------

p7     7

p8     8

----------------------------------

 

----------------------------------

p1     1

p10    10

----------------------------------

 

----------------------------------

p1     1

p3     3

----------------------------------

 

----------------------------------

p10    10

----------------------------------

 

----------------------------------

p2     2

p4     4

p6     6

p8     8

p10    10

----------------------------------

 

----------------------------------

p10    10

----------------------------------

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值