c#常用数据结构解析

C#语言的数据结构 除了使用内置的数据结构C#还允许开发者自定义数据结构,以便满足特定需求。自定义链表```csharpNext;head;else= null)```C#语言提供了丰富的数据结构以应对不同的数据管理需求。从基本的数组和链表到高级的栈、队列、哈希表等,每种数据结构都有其特定的使用场景和特点。了解这些数据结构的特性、优缺点以及适用场景,将有助于开发者在实际项目中选择合适的结构,以提高程序的性能和可维护性。 阅读详情
前言:
可能去过小匹夫博客的盆油们读过这篇对于数据结构的总结,但是小匹夫当时写那篇文章的时候略有匆忙,所以今天进行了一些增改,重新发表在蛮牛。作为程序猿,对于常见的数据结构的掌握是非常必要的,也许这篇文章略显朴实,没有那么花哨的东西,但是小匹夫也希望各位程序向的 U3D 从业者能喜欢。
前段时间小匹夫读过一份代码,对其中各种数据结构灵活的使用赞不绝口,同时也大大激发了小匹夫对各种数据结构进行梳理和总结的欲望。正好最近也拜读了若干大神的文章,觉得总结下常用的数据结构以供自己也能灵活的使用变得刻不容缓。那么还是从小匹夫的工作内容入手,就谈谈在平时使用U3D时经常用到的数据结构和各种数据结构的应用场景吧。
1.几种常见的数据结构 
这里主要总结下小匹夫在工作中常碰到的几种数据结构:Array,ArrayList,List<T>,LinkedList<T>,Queue<T>,Stack<T>,Dictionary<K,T>
数组Array:  
数组是最简单的数据结构。其具有如下特点:
  • 数组存储在连续的内存上。
  • 数组的内容都是相同类型。
  • 数组可以直接通过下标访问。
  数组Array的创建:
[C#]  纯文本查看  复制代码
?
1
2
int size = 5;
int [] test = new int [size];

  创建一个新的数组时将在 CLR 托管堆中分配一块连续的内存空间,来盛放数量为size,类型为所声明类型的数组元素。如果类型为 值类型,则将会有size个未装箱的该类型的值被创建。如果类型为引用类型,则将会有size个相应类型的引用被创建。
  由于是在连续内存上存储的,所以它的索引速度非常快,访问一个元素的时间是恒定的也就是说与数组的元素数量无关,而且赋值与修改元素也很简单。
[C#]  纯文本查看  复制代码
?
1
2
3
4
5
6
7
string [] test2 = new string [3];
//赋值
test2[0] = "chen" ;
test2[1] = "j" ;
test2[2] = "d" ;
//修改
test2[0] = "chenjd" ;


  但是有优点,那么就一定会伴随着缺点。由于是连续存储,所以在两个元素之间插入新的元素就变得不方便。而且就像上面的代码所显示的那样,声明一个新的数组时, 必须 指定其长度,这就会存在一个潜在的 问题 ,那就是当我们声明的长度过长时,显然会浪费内存,当我们声明长度过短的时候,则面临这溢出的风险。这就使得写代码像是投机,小匹夫很厌恶这样的行为!针对这种缺点,下面隆重推出ArrayList。
ArrayList:   
为了解决数组创建时必须指定长度以及只能存放相同类型的缺点而推出的数据结构。ArrayList是System.Collections命名空间下的一部分,所以若要使用则必须引入System.Collections。正如上文所说,ArrayList解决了数组的一些缺点。
  • 不必在声明ArrayList时指定它的长度,这是由于ArrayList对象的长度是按照其中存储的数据来动态增长与缩减的。
  • ArrayList可以存储不同类型的元素。这是由于ArrayList会把它的元素都当做Object来处理。因而,加入不同类型的元素是允许的。
  ArrayList的操作:
[C#]  纯文本查看  复制代码
?
01
02
03
04
05
06
07
08
09
10
11
ArrayList test3 = new ArrayList();
//新增数据
test3.Add( "chen" );
test3.Add( "j" );
test3.Add( "d" );
test3.Add( "is" );
test3.Add(25);
//修改数据
test3[4] = 26;
//删除数据
test3.RemoveAt(4);


  说了那么一堆”优点“,也该说说缺点了吧。为什么要给”优点”打上引号呢?那是因为ArrayList可以存储不同类型数据的原因是由于把所有的类型都当做Object来做处理,也就是说ArrayList的元素其实都是Object类型的,辣么问题就来了。
  • ArrayList不是类型安全的。因为把不同的类型都当做Object来做处理,很有可能会在使用ArrayList时发生类型不匹配的情况。
  • 如上文所诉,数组存储值类型时并未发生装箱,但是ArrayList由于把所有类型都当做了Object,所以不可避免的当插入值类型会发生装箱操作,在索引取值时会发生拆箱操作。这能忍吗?
注:为何说频繁的没有必要的装箱和拆箱不能忍呢?且听小匹夫慢慢道来:所谓 装箱 (boxing):就是值类型实例到对象的转换(百度百科)。那么拆箱:就是将引用类型转换为值类型咯(还是来自百度百科)。下面举个栗子~
[C#]  纯文本查看  复制代码
?
1
2
3
4
5
6
7
//装箱,将String类型的值FanyoyChenjd赋值给对象。
String  info = ”FanyoyChenjd”; 
object obj=( object )info; 
 
//拆箱,从Obj中提取值给info
object obj = "FanyoyChenjd" ;
String info = (String)obj;

那么结论呢?好吧,请允许小匹夫很low再次引用百度百科。 显然,从原理上可以看出,装箱时,生成的是全新的引用对象,这会有时间损耗,也就是造成效率降低。

List<T>泛型List  
为了解决ArrayList不安全类型与装箱拆箱的缺点,所以出现了泛型的概念,作为一种新的数组类型引入。也是工作中经常用到的数组类型。和ArrayList很相似,长度都可以灵活的改变,最大的不同在于在声明List集合时,我们同时需要为其声明List集合内数据的对象类型,这点又和Array很相似,其实List<T>内部使用了Array来实现。
[C#]  纯文本查看  复制代码
?
01
02
03
04
05
06
07
08
09
10
11
List< string > test4 = new List< string >(); 
   
//新增数据 
test4.Add(“Fanyoy”); 
test4.Add(“Chenjd”); 
 
//修改数据 
test4[1] = “murongxiaopifu”; 
   
//移除数据 
test4.RemoveAt(0);


 这么做最大的好处就是
  • 即确保了类型安全
  • 取消了装箱和拆箱的操作。
  • 它融合了Array可以快速访问的优点以及ArrayList长度可以灵活变化的优点。
假设各位和小匹夫一样,在工作中最常使用的一种数据结构就是它。那么我们是否能再多一点好奇心呢?那就是探究一下,如果我们自己实现一个类似的数据结构,该从何处下手呢?
下面小匹夫就抛砖引玉了。
刚才说过了,List<T>的内部其实也是一个Array,且是强类型的,所以我们的简单实现(暂且称之为EggArray<T>)也秉承这个特点,内部通过一个Array来实现,且需要声明类型。但是同时我们也看到List<T>继承和实现了很多接口,比如IEnumerable接口等,而且值类型和引用类型通吃。这里为了EggArray<T>实现起来轻装简行,我们不继承List<T>继承的各种接口,同时我们的EggArray只服务于引用类型。
那么首先明确了,它是一个处理引用类型,且实现了泛型的。那么定义就出来了:
[C#]  纯文本查看  复制代码
?
1
2
3
4
5
//EggArray类
//定义
public class EggArray<T> where T : class
{
}

那么下一步呢?该确定它的内部成员了,就先从字段和属性开始吧。
属性&变量
属性
说明
Capacity EggArray的容量
Count EggArray中的元素个数
items T[],一个Array,因为上一篇文章说过List<T>的内部其实还是Array,所以内部我们也使用Array
[C#]  纯文本查看  复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
//EggArray<T>的属性&&变量
private int capacity;
private int count;
private T[] items;
 
public int Count
{
     get
     {
         return this .count;
     }
}
 
public int Capacity
{
     get
     {
         return this .capacity;
     }
}

之后呢?好像是需要一个构造函数了。上文也说了,貌似new的时候不需要指定容量呀。那么我们就把构造函数做成这样吧。
构造函数:
构造函数 说明
EggArray() 初始化 EggArray<T> 类的新实例,该实例为空并且具有默认初始容量。
EggArray(int32) 初始化 EggArray<T> 类的新实例,该实例为空并且具有指定的初始容量。
[C#]  纯文本查看  复制代码
?
01
02
03
04
05
06
07
08
09
10
//EggArray的构造函数,默认容量为8
public EggArray() : this (8)
{
}
 
public EggArray( int capacity)
{
     this .capacity = capacity;
     this .items = new T[capacity];
}

好了,构造函数也说完了,那么就介绍一下私有方法,因为运行机制全部是有私有方法来运筹的,公共方法只不过是开放给我们的使用的罢了。小匹夫对公共方法的实现没有兴趣,这里就不做演示了。
刚刚也说了,List<T>是无所谓初始长度的,可以用Add()方法往里面添加元素,同时也不可能是有一个无限大的空间让它来存储,那么究竟它究竟为何能做到这一点呢?因为有一个能动态调整内部数组大小的方法存在,且调整大小是按照原有长度成倍增长的。我们姑且称之为Resize。
那么在进行下面的内容之前,小匹夫还想先问各位一个问题:
[C#]  纯文本查看  复制代码
?
1
2
3
4
5
6
7
8
9
List< int > test = new List< int >(){0,1,2,3,4,5,6,7,8,9};
                 int count = 0;
                 for ( int i = 0; i < test.Count; i++)
                 {
                         if (i == 1)
                                 test.Remove(test[i]);
                         count++;
                 }
                 Debug.Log (count);

上面这段代码会输出什么呢?答案是9。可能有的盆油会感到奇怪,test进去时长度明明是10啊。就算你中间Remove了一个元素,可为什么会影响后面的元素呢?(比如把index为1的元素remove掉,原来index为2的元素现在的index就成1了。)感觉乱套有木有?其实这里List<T>在执行remove的同时,也把内部的数组压缩了。所以也肯定有一个方法用来压缩咯。我们姑且称为Compact。
私有方法
私有方法
说明
Resize 当数组元素个数大于或等于数组的容量时,调用该方法进行扩容,会创建一个新的Array存放数据,“增长因子”为2
Compact 压缩数组,在Remove时候默认调用
[C#]  纯文本查看  复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//当数组元素个[/size][/backcolor][/color][i][color=White][backcolor=DarkGreen][size=2]数不小于数组容量时,需要扩容,增长因子growthFactor为2
private void Resize()
{
     int capacity = this .capacity * growthFactor;
     if ( this .count > capacity)
     {
         this .count = capacity;
     }
     T[] destinationArray = new T[capacity];
     Array.Copy( this .items, destinationArray, this .count);
     this .items = destinationArray;
     this .capacity = capacity;
}
 
  private void Compact()
         {
             int num = 0;
             for ( int i = 0; i < this .count; i++)
             {
                 if ( this .items[i] == null )
                 {
                     num++;
                 }
                 else if (num > 0)
                 {
                     this .items[i - num] = this .items[i];
                     this .items[i] = null ;
                 }
             }
             this .count -= num;
         }[i][i][i]


LinkedList<T>  
也就是链表了。和上述的数组最大的不同之处就是在于链表在内存存储的排序上可能是不连续的。这是由于链表是通过上一个元素指向下一个元素来排列的,所以可能不能通过下标来访问。如图

  既然链表最大的特点就是存储在内存的空间不一定连续,那么链表相对于数组最大优势和劣势就显而易见了。
  • 向链表中插入或删除节点无需调整结构的容量。因为本身不是连续存储而是靠各对象的指针所决定,所以添加元素和删除元素都要比数组要有优势。
  • 链表适合在需要有序的排序的情境下增加新的元素,这里还拿数组做对比,例如要在数组中间某个位置增加新的元素,则可能需要移动移动很多元素,而对于链表而言可能只是若干元素的指向发生变化而已。
  • 有优点就有缺点,由于其在内存空间中不一定是连续排列,所以访问时候无法利用下标,而是必须从头结点开始,逐次遍历下一个节点直到寻找到目标。所以当需要快速访问对象时,数组无疑更有优势。
  综上,链表适合 元素数量不固定 ,需要 两端存取且经常增减节点 的情况。
  关于链表的使用,MSDN上有详细的例子。
Queue<T>  
在Queue<T>这种数据结构中,最先插入在元素将是最先被删除;反之最后插入的元素将最后被删除,因此队列又称为“先进先出”(FIFO—first in first out)的线性表。通过使用Enqueue和Dequeue这两个方法来实现对 Queue<T> 的存取。

  一些需要注意的地方:
  • 先进先出的情景。
  • 默认情况下,Queue<T>的初始容量为32, 增长因子为2.0。
  • 当使用Enqueue时,会判断队列的长度是否足够,若不足,则依据增长因子来增加容量,例如当为初始的2.0时,则队列容量增长2倍。
  • 乏善可陈。
  关于Queue<T>的使用方法,MSDN上也有相应的例子。
Stack<T>
  
  与Queue<T>相对,当需要使用后进先出顺序(LIFO)的数据结构时,我们就需要用到Stack<T>了。
  一些需要注意的地方:
  • 后进先出的情景。
  • 默认容量为10。
  • 使用pop和push来操作。
  • 乏善可陈。
  同样,在MSDN你也可以看到大量Stack<T>的例子。
Dictionary<K,T>  
字典这东西,小匹夫可是喜欢的不得了。看官们自己也可以想想字典是不是很招人喜欢,创建一个字典之后就可以往里面扔东西,增加、删除、访问那叫一个快字了得。但是直到小匹夫日前看了一个大神的文章,才又想起了那句话“啥好事咋能让你都占了呢”。那么字典背后到底隐藏着什么迷雾,拨开重重迷雾之后,是否才是真相?且听下回分。。。等等,应该是下面就让我们来分析一下字典吧。
  提到字典就不得不说Hashtable哈希表以及Hashing(哈希,也有叫散列的),因为字典的实现方式就是哈希表的实现方式,只不过 字典是类型安全的 ,也就是说当创建字典时,必须声明key和item的类型,这是 第一条 字典与哈希表的 区别 。关于哈希表的内容推荐看下这篇博客 哈希表 。关于 哈希 ,简单的说就是一种将任意长度的消息压缩到某一固定长度,比如某学校的学生学号范围从00000~99999,总共5位数字,若每个数字都对应一个索引的话,那么就是100000个索引,但是如果我们使用后3位作为索引,那么索引的范围就变成了000~999了,当然会冲突的情况,这种情况就是 哈希冲突(Hash Collisions) 了。扯远了,关于具体的实现原理还是去看小匹夫推荐的那篇博客吧,当然那篇博客上面那个大大的转字也是蛮刺眼的。。。
  回到Dictionary<K,T>,我们在对字典的操作中各种时间上的优势都享受到了,那么它的劣势到底在哪呢?对嘞,就是空间。以空间换时间,通过更多的内存开销来满足我们对速度的追求。在创建字典时,我们可以传入一个容量值,但实际使用的容量并非该值。而是使用 “不小于该值的最小质数来作为它使用的实际容量,最小是3。”(老赵), 当有了实际容量之后,并非直接实现索引,而是通过创建额外的2个数组来实现间接的索引,即int[] buckets和Entry[] entries两个数组(即buckets中保存的其实是entries数组的下标),这里就是 第二条 字典与哈希表的 区别,还记得哈希冲突吗?对,第二个区别就是处理哈希冲突的策略是不同的!字典会采用额外的数据结构来处理哈希冲突,这就是刚才提到的数组之一buckets桶了,buckets的长度就是字典的真实长度,因为buckets就是字典每个位置的映射,然后buckets中的每个元素都是一个链表,用来存储相同哈希的元素,然后再分配存储空间。

因此,我们面临的情况就是,即便我们新建了一个空的字典,那么伴随而来的是2个长度为3的数组。所以当处理的数据不多时,还是慎重使用字典为好,很多情况下使用数组也是可以接受的。

2.几种常见数据结构的使用情景
Array 需要处理的元素数量确定并且需要使用下标时可以考虑,不过建议使用List<T>
ArrayList 不推荐使用,建议用List<T>
List<T>泛型List 需要处理的元素数量不确定时 通常建议使用
LinkedList<T> 链表适合元素数量不固定,需要经常增减节点的情况,2端都可以增减
Queue<T> 先进先出的情况
Stack<T> 后进先出的情况
Dictionary<K,T> 需要键值对,快速操作

文章转载自匹夫细说Unity3D(四)——常用数据结构大起底,感谢[慕容小匹夫]提供好文章
C#数据结构与算法:从基础到高阶 数据结构是计算机中存储和组织数据的方式。不同的数据结构适用于不同类型的问题,选择合适的数据结构可以提高程序的运行效率。 阅读详情

相关推荐

深入探讨 C# 集合与数据结构:从基础到实战

C#集合类(Collections)是指实现特定数据结构的类,能够高效地存储、管理和操作数据。C# 提供了丰富的集合类型,主要包括数组、列表、字典、队列和栈等。线性集合:如数组、列表、队列、栈等,主要用于存储一组顺序排列的元素。键值对集合:如字典和哈希表,通过键来快速查找对应的值。无序集合:如HashSet和SortedSet,无序集合能够有效地去重和执行集合运算。C# 提供了丰富的集合类,每种集合类型都有其独特的应用场景和优势。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1131

C#数据分析

此软件用到chart控件,主要用于人的基础数据分析,对人分成各个阶段进行分析

【愚公系列】数据结构(十二)-红黑树

数据结构是计算机科学中的一个重要概念,它描述了数据之间的组织方式和关系,以及对这些数据的访问和操作。常见的数据结构有:数组、链表、栈、队列、哈希表、树、堆和图。数组(Array):是一种线性数据结构,它将一组具有相同类型的数据元素存储在一起,并为每个元素分配一个唯一的索引。数组的特点是具有随机访问的能力。链表(Linked List):也是一种线性数据结构,它由一系列的节点组成,每个节点包含数据和指向下一个节点的引用。链表的特点是可以动态地插入或删除节点,但访问某个节点时需要从头开始遍历。

愚公智库 48万+

基于C#的多曲线数据分析实例

多曲线分析数据,基于C#的多曲线数据分析实例源代码,运行EXE需要用到数据库文件,已放在Database文件夹中,源码测试截图并没有看到多曲线效果,是因为数据库没有附加,有闲暇的亲们可以自己附加上数据库测试一下。 运行环境:Visual Studio2010

C#高级--常用数据结构

C#高级–常用数据结构 零、文章目录 一、常见的数据结构 1、集合[Set] 2、线性结构 3、树形结构 4、图形结构 二、Array/ArrayList/List 内存上连续存储,节约空间,可以索引访问,读取快,增删慢 1、Array 元素类型是一样的,定长 int[] list = new int[3]; list[0] = 123; string[] stringArray = new string[] { "123", "234" }; for (int i = 0; i < list.Le

李宥的博客 9876

C#常用数据结构

C#常用数据结构

微软Dynamics 365 CRM,Power Apps,Power Platform技术和技能培训 4278

深入解析 C# 常用数据结构:特点、区别与优缺点分析

数据结构优点缺点适用场景List动态大小,支持按索引访问,操作灵活插入/删除操作可能较慢,内存重新分配开销高需要灵活的存储结构,并且频繁按索引访问元素Array快速的索引访问,低内存开销大小固定,插入和删除不方便元素数量固定且已知时,操作简单且高效高效的键值对查找,支持快速插入和删除无序,内存开销较大需要根据键快速查找值的场景HashSet唯一元素,查找和插入操作非常高效无序,不支持索引访问需要去重操作,且要求快速查找元素的场景Queue先进先出(FIFO),高效的入队和出队操作。

net core ,前端 ,python 全栈开发 1193

Unity(二) c#常用数据结构解析回顾

最近做完项目后,对数组的用法的不同产生了兴趣,深挖了一下,顺便对其他的常用数据结构也了解一下。 一、数组 数组当仁不让是最基础的结构了。C#里按时间线是Array——ArrayList——List。但ArrayList应该是里面使用率最低的了,原因往下说。 1)Array 1)Array在内存上的存储是连续的。 2)Array中的元素必须是同类型的。 3)Array直接通过下标访问(后面会和链表比...

qq_36648510的博客 2389

c#数据结构

c#常用数据结构解析 http://blog.csdn.net/suifcd/article/details/42869341谈谈在平时使用U3D时经常用到的数据结构和各种数据结构的应用场景吧。1.几种常见的数据结构 这里主要总结下小匹夫在工作中常碰到的几种数据结构:Array,ArrayList,List<T>,LinkedList<T>,Queue<T>,...

weixin_30624825的博客 128

C#数据结构与算法:从理论到面试题的全面解析

本文系统介绍了C#中的数据结构与算法核心知识,包含数组、链表、栈、队列和哈希表等基础数据结构,并详细讲解了冒泡排序、快速排序等常用算法及其实现。通过二分查找等示例展示了高效算法设计,最后以"两数之和"面试题演示实际应用。文章兼顾理论分析与代码实现,为开发者提供了全面的数据结构与算法学习指南。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 894

C#集合 Stack 中常用的方法和属性

C# Stack (栈)是常见的数据结构之一,栈是一种先进后出的结构,即元素从栈的尾部插入,从栈的尾部移除,类似于日常生活中搬家的时候装车,先装上车的东西要后拿下来。 Stack 类提供了 3 种构造方法,如下表所示。 构造方法 作用 Stack() 使用初始容量创建 Stack 的对象 Stack(ICollection) 创建 Stack 的实例,该实例包含从指定实例中...

星期一兴起 1641

C#数据结构Dictionary简单解析

C#的字典Dictionary也是比较常用数据结构,简单解析一下字典的源码结构

w70289728的博客 647

C#数据结构List简单解析

List是常用数据结构,大部分人都知道它是用数组实现的

w70289728的博客 313

C#编程核心技术与应用:20个常用技术详解

C#语言的设计初衷是为了提供一种简洁、类型安全且与.NET平台紧密集成的编程方式。C#语言融合了C、C++和Java语言的特点,是一个现代、强类型、面向对象的编程语言。它支持垃圾回收,统一的类型系统以及多线程等特性。在C#中,类是一组数据成员(字段)和函数成员(方法、属性等)的集合。它们定义了对象的蓝图或模板。set;set;set;在这个例子中,我们定义了一个名为Car的类,它有几个字段(Brand, Model, Year)和一个方法(DisplayCarInfo),用于显示汽车的信息。

weixin_36213081的博客 833

C#

C#栈 一、基本概念 栈和队列非常相似,只是操作的规律不同而已。队列是先进先出的逻辑,而栈是先进后出,后进先出的逻辑。即(LIFO) 二、命名空间 栈在System.Collections.Generic命名空间中可以找到。其名为Stack,泛型的为Stack。它也实现了IEnumerable和ICollection接口。 三、Stack中常用的方法 // 摘要: // 表示相同任意类型...

weixin_40786497的博客 1159

C#开发之——Stack(8.4)

一 概述 C#中Stack(栈)是最常见的数据结构之一,栈是一种先进后出的结构,即元素从栈的尾部插入,从栈的尾部移除,类似于日常生活中搬家的时候撞车,先装上车的东西要后拿下来 <!--more--> 二 构造方法 2.1 构造方法 构造方法 作用 Stack() 使用初始容量创建 Stack 的对象 Stack(ICollection col) 创建 Stack 的实例,该实例包含从指定实例中复制的元素,并且初始容量与复制的元素个数、增长因子相同 Stac

我的博客 269
上一篇: C#委托 和代理模式 观察者模式的关系
下一篇: eclipse中文字体大小修改,让中英文字体协调
随风吹笛
博客等级 码龄15年 167粉丝 54原创
评论 11
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值