C#语法笔记(二)


语言集成查询

语言集成查询(LINQ)提供一种一致的数据查询模型,使用相同的基本编码模式来查询和转换各种数据源,包括SQL server数据库、XML文档、ADO.NET数据集、支持IEumerable或泛型IEnumerable<T>接口的任意对象集合。

初始值设定项: 使用对象的初始值设定项可以在创建对象时,向对象的任何可访问的字段或属性分配值,而无需显式调用构造函数。

class Cat

{

public int Age{get; set; }

public  string name{get ; set; }

}

class Test

{

public static void Main()

{

Cat cat = new Cat{ name = "jason“, Age = 3};

}

}

使用集合初始值设定项可以在初始化一个实现了IEnumerable的集合类时,指定一个或多个元素初始值设定项,而无需在源代码中指定多个对该类的Add方法的调用。

List<int >  digits = new List<int> {0, 1, 2, 3};

初始值设定项特别适用于LINQ查询表达式:查询表达式经常使用匿名类型,而这些类型只能使用对象初始值设定项进行初始化。查询表达式可以将原始序列的对象转换为可能具有不同的值或形式的对象。

匿名类型 在LINQ查询表达式的select子句中,通常使用匿名类型以便返回源序列中每个对象的属性子集。匿名类型声明和使用基本形式如下:

var  匿名类型变量 =  new { 公共只读属性组}

使用关键字var 声明一个不指定类型变量,且立即使用初始值选项给该变量赋值。匿名类型是一组只读属性组成的类类型(不允许包含其他种类的类成员,如方法和事件)。匿名类型无需预先显式定义,其类型名有编译器生成,且不能再源代码级直接使用。匿名类型的有效范围为定义该匿名类型变量的方法。若跨越方法范围,建议使用普通的命名结构而不是匿名范围。

注意:匿名类型并不是无类型,只是无需预先显式定义,而是声明匿名类型变量时,通过初始值选项指定的类型,其类型名由编译器生成。

如:

var   n =  5 ;  //编译后为int 类型

n = "hello"; //编译错误,无法将string隐式转换为int

var  a;   //编译错误,不能没哟初值

var  s = null ; //编译错误,初值不能为null

Lambda表达式

lambda表达式是一个匿名函数,它可以包含表达式和语句,并且可用于创建委托或表达式目录树类型。

lambda表达式使用lambda运算符 "=>"(读作 goes  to)。“=>"左边是输入参数(可选),右边包含表达式或语句块。基本形式为(input parameters) =>  expression

如 x => x*x;  (x, y) => x == y;  () => Somemethod() //使用空括号指定0个输入参数

其中 (x, y) => x == y等价于

bool  函数名(x, y)

{

return x == y;

}

lambda表达式用于创建委托或表达式目录树类型,例如:

delegate  int  del(int  i);

del  myDelegate =  x => x*x;

int  j = myDelegate(5); //j = 25

Expression<del> =  x => x*x;

lambda常常用在基于方法的LINQ查询中,作为诸如where等标准查询运算符方法的参数。例如

int []  numbers = {5, 4, 2, 1, 9, 8 6, 7};

int  ddNumbers = numbers.Count(n => n % 2 == 1);  //返回所有奇数

扩展方法:

扩展方法向现有类型”添加“方法,而无需修改原类型的带吗并重新编译。集成语言查询使用扩展方法向现有的 System.Collections.IEnumerable 和 System.Collections.Generic.IEnumerable<T:>类型添加了LINQ 标准查询功能,只要通过using  System.Linq指令导入其命名空间,任何实现了IEnumerable<T>的类型就具有GrouopBy、OrderBy、Average等实例方法。

class Test

{

static void Main()

{

int [ ]  numbers = {10, 11, 2, 3, 90, 56};

var  result = numbers.OrderBy(g => g); // int型数组实现IEnumerable接口, 整数排序

foreach(int  i in  numbers)

Console.Write(i + "  ");

}

}

扩展方法定义为单独的命名空间中静态类的静态方法,它们的第一个参数指定该方法作用于哪个类型,并且该参数以this 修饰符为前缀。

只要使用using指令将包含扩展方法的命名空间显式导入源码中,就可以通过实例方法语法进行调用指定对象的扩展方法。

LINQ基本操作

LINQ查询操作由以下三个不同的操作组成:

class TestLinq

{

static void Main()

{

      //获取数据源
            int[] numbers = new int[] { 4, 2, 0, 9, 5, 7 };
            //创建查询,从整数数组中返回偶数
            //方法一:使用查询表达式声明查询变量
            var numQuery1 =             //查询变量(用于存储查询)
                from num in numbers     //必须以from子句开头:指定数据源和范围变量
                where (num % 2) == 0    //筛选子句(可选)  
                select num;             //必须以select子句(选择对象序列)或group子句(分组)结尾


            //方法二:使用查询方法声明查询变量
            var numQuery2 =
                numbers.Where(num => num % 2 == 0);
            
            //执行查询并显示查询结果
            Console.WriteLine("numQuery1 内容如下: ");
            foreach (int num in numQuery1)
            {
                Console.Write("{0, 1} ", num);
            }
            Console.WriteLine("\nnumQuery2内容如下: ");
            foreach (int num in numQuery2)
            {
                Console.Write("{0, 1} ", num);
            }

}

}

(1)获取数据源

支持IEnumerable或泛型IEnumerable<T>接口的类型为”可查询类型“,可查询类型可直接作为LINQ数据源。C#中数组隐式支持泛型IEnumerable<T>接口。对于其他形式的数据,如XML文档等,则需要通过相应的LINQ提供程序,把数据表示成内存中支持IEnumerable或泛型 IEnumerable<T>接口的类型,并作为LINQ数据源。

例如,LINQ to XML将XML文档加载到可查询的XElement类型中,并作为LINQ数据源:

XElement  contacts =  XElement.Load(@"c:\Sample.xml");

(2)创建查询

查询指定如何从数据源中检索信息,并对其进行排序、分组、结构化。

创建查询即声明一个匿名类型的查询变量,并使用查询表达式对其进行初始化, 也可以使用查询方法对其进行初始化。查询表达式包含3个子句: from , where , select。from子句指定数据源,where子句指定过滤器, select子句指定返回元素类型。

在LINQ中,查询变量本身只是存储查询命令,创建查询仅仅是声明查询变量,此时并不执行任何操作,也不返回任何数据。LINQ在随后执行查询后,才执行查询变量中声明的声明查询操作。并返回结果数据。

(3)执行查询

LINQ查询操作一般采用延迟执行模式,即只有当访问查询变量中的数据时,才会执行变量中声明的查询操作,并返回数据结果。

由于查询变量本身从不保存查询结果,因此可以根据需要随意执行查询。例如在应用程序中,可以创建一个检索最新数据的查询,并可以按某一时间间隔反复执行该查询以便每次检索不同的结果。 若要强制立即执行任意查询并缓存器结果,可以调用ToList()或 ToArray()方法,将所有数据缓存在单个集合对象中。

List<int> numQuery2 = (from  num  in numbers  where num % 2 == 0   select  num).ToList();

标准查询运算符

数据排序

排序操作按一个或多个属性对序列的元素进行排序,第一个排序条件对元素执行主要排序,第二个排序条件对主要排序结果执行排序。

方法查询表达式说明
OrderByorderby按升序对字符串长度进行排序。例如
string [] words = {"the", "quick", "brown",
 "fox", "jumps"};
IEnumerable<string> query =
from  w  in words
orderby  w.Length
select  w;
//结果:{"the", "fox", "quick",
"brown", "jumps"}
OrderByDescendingorderby...descending按降序对值进行排序
ThenByorderby..., ...按升序执行次要排序
ThenByDescendingorderby ....,... descending 
Reverse不适用颠倒集合中元素的顺序
筛选

将结果集限制为只包含那些指定条件的元素,又成为选择。

方法查询表达式说明
OfType不适用根据值强制转换为指定类型的能力选择值。如
ArrayList  fruits = new ArrayList{"mango", "orange", 3.0, 5, "banana"};
IEnumerable<string> query = fruits.OfType<string>();
Wherewhere选择基于谓语函数的值。在单一where子句中,可以使用&&和||运算符根据需
要指定任意多个谓语。
where   条件1 && 条件2
数据投影

将对象转化为一中新形式的操作,通过映射属性(直接映射,或对属性执行数学函数)以构建仅包含必需属性的新类型。

方法查询表达式说明
Selectselect映射基于转换函数的值。例如
List<string>words = new List<string>() { "a", "apple", "an", "day"};
var query  =  from word  in  words
select  word.Substring(0, 1);
//结果{"a", "a", "a", "d"}
SelectMany使用多个from子句映射基于转换函数的值序列,然后将他们展平为一个序列。例如:
var  query =   from  B  in  A
  from  C  in B
select xxx

数据分组

分组操作将数据按共享公共属性进行分组,以便对每个组中的元素进行处理。

方法查询表达式说明
GroupBygroup ... by 或group ... by ... into...对共享公共属性的元素进行分组。例如,根据是奇数还是偶数进行分组。
List<int>  numbers = new List<int>() { 35, 44, 2, 1, 9, 78};
IEnumerable<IGrouping<int, int>> query = from num in numbers
group num by num % 2
联接运算

联接运算将一个数据源中的对象与另一个数据源中共享某个公共属性的对象关联起来。

方法查询表达式说明
Joinjoin ... in
on....equals ...
内部联接。根据键值选择器函数联接两个数据集,只返回那些在另一个
数据集中具有匹配项的对象并提取值对。
var innerJoinQuery =  from category in categories
join prod  in products  on category.ID equals prod.CategoryID
select new {ProductName = prod.Name, Category = category.Name};
//查询结果:”产品名称/类别“系列。如果categorires中的某个元素不具有匹配的products,则该类别不会出现在结果中。
GroupJoin  
   

方法查询表达式说明
Joinjoin...in...on...equals...内部联接。根据键值选择器函数联接两个数据集,只返回那些在另一个数据集中具
有匹配项的对象并提取值对。
var innerJoinQuery =  from category in categories
join prod  in products  on category.ID equals prod.CategoryID
select new {ProductName = prod.Name, Category = category.Name};
//查询结果:”产品名称/类别“系列。如果categorires中的某个元素不具有匹配的products,
则该类别不会出现在结果中。
GroupJoinjoin...in...on...equals...into分组联接,根据键值选择器函数联接两个数据集,返回一个分层的结果序列:将左侧数据集中
的元素与右侧数据集中的一个或多个元素匹配相关联,如果在右侧数据集中找不到与左侧数据集中
的元素相匹配的元素,则join子句将为该项产生一个空数组。
var innerGroupJoinQuery = 
from  category  in  categories
join  prod  in  products  on  category.ID  equals  prod.CategoryID   into  prodGroup
select new{CategoryName = category.Name, Products = prodGroup}
GroupJoinjoin...in...on...equals...into....
DefaultEmpty
左外部联接。将返回左侧源序列中的所有元素,即使他们在右侧序列中没有匹配的元素也是如此。若
要在LINQ中执行左外部联接,则需要将DefaultEmpty方法与分组联接结合起来,以指定要在某个左侧
元素不具有匹配元素时产生的默认右侧元素。
LINQ还有一些方法,如进行数据分区、限定符运算、聚合运算、集合运算、生成运算、元素操作、串联运算、相等运算、数据类型转换等操作。


多线程编程技术

线程是操作系统分配处理器时间的基本单元,每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。每个应用程序都是由单个线程启动的(应用程序的入口点Main方法),应用程序域中的代码可以创建附加应用程序域和附加线程。

C#应用程序主线程: 应用程序运行时将创建新的应用程序域,当运行环境调用应用程序的入口点(Main方法)时,将创建应用程序主线程。

创建和启动新线程:     System.Threading命名空间提供支持多线程编程的类和接口,用于执行诸如创建和启动新线程、同步多个线程、挂起线程以及终止线程等任务。

主线程以外的线程一般称为工作者线程。创建新线程:

(1)创建一个将在主线程外执行的函数,即类的方法,用于执行新线程要执行的逻辑操作。

(2)在主线程(Main方法)中创建一个Thread实例,指向(1)中的函数。

Thread  newThread = new Thread(anObject.AMethod);

也可以使用 ThreadStart 委托, ThreadStart  threadDelegate = new ThreadStart(anObject.AMethod);

         Thread  newThread  = new Thread(threadDelegate);

(3)调用(2)中的Thread实例的Start方法,启动新线程

任何线程,包括主线程和工作者线程,在其线程体内部都用Thread 表示自身,对自身线程进行操作。Thread.Sleep()表示自身睡眠

暂停线程:

调用Thread.Sleep()方法会导致当前线程立即阻止,阻止时间长度等于传递给Thread.Sleep()的参数毫秒。注意:一个线程不能对另一个线程调用 Thread.Sleep()

中断线程:

通过对被阻止的线程调用Thread.Interrupt, 可以中断正在等待的线程并引发ThreadInterruptedException,从而使该线程脱离造成阻止的调用。

在一个线程中对另外的线程调用该方法,结束它的工作。


销毁线程:

Abort方法用于永久的停止,即销毁托管线程。调用Abort方法时,公共语言运行时库在目标线程中引发ThreadAbortException,目标线程可捕捉此异常。


等待线程结束:

Join方法,在线程A的函数体中使用  线程 B.Join(),  即A线程在此处停止,直到B线程结束为止。

线程优先级和线程调度:

每个线程都有一个分配的优先级,在运行库内创建的线程最初被分配Normal优先级。通过线程的Priority属性可以获取和设置优先级。

Lowest优先级: 可以将Thread安排在具有任何其他优先级的线程之后

BelowNormal优先级: 可以将Thread安排在具有Normal优先级的线程之后,在具有Lowest优先级的线程之前

Normal优先级: 可以将Thread安排在具有AboveNormal优先级的线程之后,在具有BelowNormal优先级的线程之前。默认情况下,线程具有Normal优先级

AboveNormal优先级: 可以将Thread安排在具有Highest优先级的线程之后,在具有Normal优先级的线程之前

Highest优先级: 可以将Thread安排在具有任何其他优先级的线层之前

线程是根据其优先级而被调度执行的。操作系统为每个优先级分别创建一个线程调度队列,只有当高优先级队列的线程执行完毕后,操作系统才会带哦哦那个执行较低优先级的线程调度队列。


线程状态和生命周期

线程的生命周期中包含各种执行状态:

Running         线程已经启动,它未被阻塞,并且没有挂起的ThreadAbortException

StopRequested             正在请求线程停止,这仅用于内部

SuspendRequested        正在请求线程挂起

Background         线程正在作为后台线程执行(相对于前台线程而言),此状态可以通过设置Thread.IsBackground 属性来控制。

Unstarted           尚未对线程调用Start方法

Stopped         线程已停止

WaitSleepJoin         线程已被阻止。这可能是因为调用Thread.Sleep() 或  Thread.Join、请求锁定(例如通过Monitor.Enter或Monitor.Wait)或等待线程同步对象(例如ManualResetEvent)

Suspended       线程已被挂起

AbortRequested        已对线程调用了Thread.Abort()方法,但线程尚未收到视图终止它的挂起的System.Threading.ThreadAbortException

Aborted        线程状态包含AbortRequested 并且该线程现在已死,但其状态尚未更改为Stopped

通过执行相应的操作,线程可以转换为对应的状态:

操作线程状态操作线程状态
在公共语言运行库中创建线程Unstarted另一个线程调用SuspendSuspendRequested
线程调用 StartUnstarted线程响应Suspend请求Suspended
线程开始运行Running另一个线程调用ResumeRunning
线程调用SleepWaitSleepJoin另一个线程调用AbortAbortRequested
线程对其他对象调用WaitWaitSleepJoin线程响应Abort请求Stopped
线程对其他对象调用JoinWaitSleepJoin线程被终止Stopped
另一个线程调用Interrupt{对该线程}Running  
(1)一旦线程被创建,它就至少处于其中一个状态中,直到终止

(2)在公共语言运行库中创建的线程最初都处于Unstarted状态,而进入运行库的外部线程则已经处于Running状态

(3)通过调用Start可以将 Unstarted线程转换为Running状态

(4)并非所有的ThreadState值的组合都是有效的。例如一个线程不能同时处于Aborted和Unstarted状态中。


线程同步

当多个线程调用单个对象的属性或方法时,一个线程可能会中断另一个线程正在执行的任务,使该对象处于一种无效状态。因此必须针对这些调用进行同步处理。如果一个类的设计使得其成员不受这类中断的影响,则该类成为线程安全类。

使用lock语句同步代码块

lock语句使用lock关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。lock关键字可以确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。lock语句以关键字lock开头,并以一个对象作为参数,在该参数的后面为线程互斥的代码块。{代码块必须用{ } 包起来}

使用监视器同步代码块

与lock类似,使用Monitor也能防止多个线程同时执行代码块。调用Monitor.Enter方法,允许一个且仅一个线程继续执行后面的语句;其他所有线程都将被阻止,直到执行语句的线程调用Exit。

System.Object   obj = (System.Object)x;

System.Threading.Monitor.Enter(obj);

try

{ Dosomething();

}

finally

{ System.Threading.Monitor.Exit(obj);

}

等价于

System.Object  obj = (System.Object) x;

lock ( x )

DoSomething();

}

同步事件和等待句柄

同步事件允许线程通过发信号相互通信,从而实现线程需要独占访问资源的同步处理控制。

同步事件有两种:AutoResetEvent(自动重置的本地事件) 和 ManualResetEvent(手动重置的本地事件)。每种事件都包括两种状态:收到信号状态(signaled) 和 未收到信号状态 (unsignaled)。 可以传递构造函数参数(布尔值 ture或false),还可以设置同步事件的状态。通过调用同步事件的Set方法,可以设置器状态为收到信号状态。

线程通过调用同步事件上的WaitOne(阻止线程直到单个事件变为收到信号状态)/ WaitAny(阻止线程直到所有指示的事件都变为收到信号状态)来等待信号,如果同步事件处于未收到信号状态吗,则该线程阻塞,并等待当前控制资源的线程通过调用Set方法把同步事件设置为 收到信号状态。

当AutoResetEvent为收到信号状态时,直到一个正在等待的线程被释放,然后自动返回未收到信号状态。如果没有任何线程在等待,则状态将无限期保持为收到信号。

当ManualResetEvent为收到信号状态时,则需要通过调用Reset方法,以设置其状态为未收到信号状态。

//WaitOne

            //线程A中操作
            while (true)
            {
                //等待autoEvent的信号 
                autoEvent.WaitOne();
                //autoEvent在信号被接收之后,自动设置为无信号状态
                //Dosomething();
                //将manualEvent设为信号状态,给线程B继续
                manualEvent.Set();  //设置为接收到信号状态
            }
            //在线程B中
            while (true)
            {
                //等待manualEvent事件信号
                manualEvent.WaitOne();
                //等待后,执行操作
                //Dosomething();
                manualEvent.Reset();    //重新将manualEvent设置为无信号状态,防止循环
                autoEvent.Set();    //将autoEvent设置为信号状态,从而让线程A可以继续
            }

//多个事件,waitAll

        AutoResetEvent[] autoEvents;
        void work1(object stateInfo)
        {
            //DoSomething
            autoEvents[0].Set();
        }
        void work2(object stateInfo)
        {
            //DoSomething
            autoEvents[1].Set();
        }
        void work3(object stateInfo)
        {
            //DoSomething
            autoEvents[2].Set();
        }


        public void Test()
        {
            autoEvents = new AutoResetEvent[3]
            {
                new AutoResetEvent(false),
                new AutoResetEvent(false),
                new AutoResetEvent(false)
            };
            ThreadPool.QueueUserWorkItem(new WaitCallback(work1));
            ThreadPool.QueueUserWorkItem(new WaitCallback(work2));
            ThreadPool.QueueUserWorkItem(new WaitCallback(work3));
            //等待work1, work2, work3 均完成
            WaitHandle.WaitAll(autoEvents);
            //Continue to do something
        }


使用Mutex同步代码块

mutex不仅可以实现线程同步,还可以实现不同进程的线程之间同步。Mutex会消耗更多的资源,在同一进程内的线程同步,用Monitor实现;不同进程内的线程同步,用Mutex.

Mutex分为两种:未命名的局部mutex和已命名的系统mutex。

未命名的局部mutex仅存在于当前进程中。当前进程中任何引用表示mutex的Mutex对象的线程都可以使用它。每个未命名的Mutex对象都表示一个单独的局部mutex。

已命名的系统mutex在整个操作系统都可见,可用于同步进程活动。可以使用接受名称的构造函数创建表示已命名系统mutex的Mutex对象。同时也可以创建操作系统对象,或者他在创建Mutex对象前就已存在。可以创建多个Mutex对象来表示同一个已命名的系统mutex,也可以使用OpenExisting方法打开现有的已命名的系统mutex。

Mutex是同步基元,它只向一个线程授予对共享资源的独占访问权。可以使用WaitOne方法请求mutex所属权,如果一个线程获得了mutex,则要获取该mutex的第二个线程会被挂起,直到第一个线程使用ReleaseMutex方法释放该mutex。

如果线程在拥有mutex时终止,则成此mutex被放弃(通常表示代码中有严重错误,或程序非正常终止)。在此情况下,系统将此mutex的状态设置为收到信号状态,下一个等待线程获得所有权,并在获取被放弃的mutex 的下一个线程中引发异常 AbandonedMutexException以便程序可以采取适当的处理。

//创建命名mutex,只能存在一个名为MyMutex的系统对象

Mutex  m = new Mutex(false, "MyMutex");

//等待获得MyMutex的控制权

m.WaitOne();

DoSomething();

//释放对MyMutex的控制

m.ReleaseMutex();


线程池

线程池是可以用来在后台执行多个任务的线程集合,这使主线程可以自由地异步执行其他任务。线程池通常用于服务器应用程序,每个传入请求都将被分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理。

一旦线程池中的某个线程完成任务,它将返回到等待线程队列中,等待被再次使用。这种重用使应用程序可以避免为每个任务创建新线程的开销。

线程池通常具有最大线程数限制。如果线程都繁忙,则额外的任务呗放入队列中,直到有线程可用时才能够得以处理。

namespace ConsoleApplication2
{
    public class Fibonacci
    {
        private int _n;
        private int  _fibOfN;
        private ManualResetEvent _doneEvent;
        public int N{get{return _n;}}
        public int FibOfN{get{return _fibOfN;}}       
        public Fibonacci(int n, ManualResetEvent doneEvent)
        {
            _n = n;
            _doneEvent = doneEvent;
        }
        public int Calculate(int n)
        {
            if (n <= 1)
                return n;
            return Calculate(n - 1) + Calculate(n - 2);
        }
        public void ThreadPoolCallback(Object threadContext)
        {
            int threadIndex = (int)threadContext;
            Console.WriteLine("线程{0}开始....", threadIndex);
            _fibOfN = Calculate(_n);
            Console.WriteLine("线程{0}结果计算...", threadIndex);
            _doneEvent.Set();
        }
    }
    class Program
    {
        static void Main()
        {
            const int FibonacciCalculations = 5;
            //每个事件用于每个Fibonacci对象
            ManualResetEvent[] doneEvents = new ManualResetEvent[FibonacciCalculations];
            Fibonacci[] fibArray = new Fibonacci[FibonacciCalculations];
            Random r = new Random();
            //使用ThreadPool配置和启动线程
            Console.WriteLine("启动{0}个任务...", FibonacciCalculations);
            for (int i = 0; i < FibonacciCalculations; i++)
            {
                doneEvents[i] = new ManualResetEvent(false);
                Fibonacci f = new Fibonacci(r.Next(1, 10), doneEvents[i]);
                fibArray[i] = f;
                ThreadPool.QueueUserWorkItem(f.ThreadPoolCallback, i);
            }
            //等待线程池中所有线程的计算....
            WaitHandle.WaitAll(doneEvents);
            Console.WriteLine("完成所有计算!");
            Console.WriteLine("第1项[F0]到第10项[F10]之间的任意5个Fibonacci数为...");
            for (int i = 0; i < FibonacciCalculations; i++)
            {
                Fibonacci f = fibArray[i];
                Console.WriteLine("Fibonacci({0}) = {1}", f.N, f.FibOfN);
            }
        }
    }
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值