C#基础学习(二)C#数组生存手册:从入门到“血压拉满“的奇妙旅程
作为一只C#萌新,当你试图用数组装下整个世界时,系统可能会温柔地弹出一句**"Index was outside the bounds of the array."**。别慌!这份求生指南将用段子教你玩转数组
一、数组是什么
数组简单来说就是由相同元素组成的一个集合,数组里面不一定是数,还可能是bool,string等类型组成的集合。那么他有些什么特点呢:
- 本质:装着相同类型元素的集装箱(比如一箱肥宅快乐水)
- 特性:长度固定(集装箱容量不可变)、有编号(每个可乐罐都有座位号)
- 致命诱惑:访问速度极快(毕竟不用像ArrayList那样扭扭捏捏扩容)
//数组是存储一组相同类型数据的集合
//数组分为 一维 多维 交错数组
//一般情况下 一维数组简称数组
二、C#数组三大门派
1.一维数组:直男式集装箱
怎么申明呢,
//变量类型[] 数组名;//只是申明 并没有初始化
//变量类型 可以是 学过的和没学过的所有变量类型
示例1:
int[] arr1;
//变量类型[] 数组名 = new 变量类型[数组的长度];
int[] arr2 = new int[5];//这种方式 相当于开了5个房间 但是默认的int值是0
//变量类型[] 数组名 = new 变量类型[数组的长度]{内容1,内容2,内容3,....};
int[] arr3 = new int[5] {1,2,3,4,5};
//变量类型[] 数组名 = new 变量类型[]{内容1,内容2,。。。。};
int[] arr4 = new int[] {2,3,4,5};//后面的内容决定数组的长度
//变量类型[] 数组名 = {内容1,内容2,内容3,....};
int[] arr5 = { };
示例2:
// 声明方式1:班主任点名式
int[] scores = new int[5]; // 5个空座位,默认值0
// 声明方式2:暴力填鸭式
int[] scores = {90, 80, 85, 0, 0}; // 第一个座位90分,第二个80分...
// 使用姿势:
scores[0] = 100; // 第一个座位改成100分
Console.WriteLine(scores[2]); // 访问第三个座位
⚠️危险行为:scores[5] = 60;
(试图打开不存在的第6个座位,系统将对你发动"越界异常攻击"),所以务必注意在使用数组是,请处理好下标的各种情况。
示例3:数组的实际应用
int[] array = new int[5] { 1, 2, 3, 4, 5 };
//1.数组的长度
//数组变量名.Length
Console.WriteLine(array.Length);
//2.获取数组中的元素
//数组中的下标和索引 他们都是从0开始的
//数组的范围 0~Length - 1
Console.WriteLine(array[0]);
//3.修改数组中的元素
array[0] = 99;
//4.遍历数组 通过循环 快速获取数组中的每一个元素
for (int i = 0; i < array.Length; i++)
{
Console.WriteLine(array[i]);
}
//5.增加数组的元素
//数组初始化以后 是不能 直接添加新的元素
int[] array2 = new int[6];
for (int i = 0; i < array.Length; i++)
{
array2[i] = array[i];
}
array = array2;
//先搬家 先把原有数据储存 等新家建好后 在搬回来 自动把房子变大了
//6.删除数组的元素
//数组初始化以后 是不能 直接删除新的元素
//搬家的原理
int[] array3 = new int[5];
for (int i = 0; i < array3.Length; i++)
{
array3[i] = array[i];
}
array = array3;
//7.查找数组中的元素
//遍历即可
2. 多维数组:强迫症の矩形方阵(比如Excel表格)
既然数组是排成一排的一组元素,你有没想过,将他拼成两排,三排或者更多排呢
下面先来看看简单二维数组是怎么定义的:
//二维数组的申明
int[,] arr3 = new int[,] {
{ 1, 2 },
{ 3, 4 },
{ 5, 6 },
{ 7, 8 } };
//PLUS
int[,] arr4 = {
{ 1, 2 },
{ 3, 4 },
{ 5, 6 },
{ 7, 8 } };
好的,现在你已经知道怎么定义二维数组了,是时候该传授你该如何使用这个东西:
二维数组的使用方法(简单版):
//二维数组的使用
//1.二维数组的长度
int[,] array = new int[,] { {8,9,55 },
{ 48,85,95} };
Console.WriteLine(array.GetLength(0));//得到多少行
Console.WriteLine(array.GetLength(1));//得到多少列
//2.获取二维数组中的元素
Console.WriteLine(array[0,1]);
//3.修改二维数组中的元素
array[0, 0] = 55;
Console.WriteLine(array[0,0]);
//4.遍历二维数组
for (int i = 0; i < array.GetLength(0); i++) //行
{
for (int j = 0; j < array.GetLength(1); j++) //列
{
Console.WriteLine(array[i, j]);
}
}
//5.增加数组的元素
int[,] array2 = new int[3, 3];
值得关注的主要是一个函数,两个输入值:GetLength(0),当是0的时候表示获取到的是二维数组的行数,当然咱们就一个二维数组。自然而然行数就为2哈。好像没啥用,嘿嘿。不过,我们下面即将介绍的交错数组,那个时候就可以发挥巨大作用了!里面的参数为1时获取的为列数,这个显而易见。
示例:
// 二维数组声明:
int[,] matrix = new int[3,4]; // 3行4列的数学课座位表
// 三维数组声明(慎用,容易晕车):
int[,,] cube = new int[2,3,4]; // 2层楼,每层3行4列
// 使用姿势:
matrix[0,1] = 42; // 第一行第二列塞入42
Console.WriteLine(matrix.GetLength(0)); // 获取行数:3
⚠️雷区警示:所有行必须一样长!就像军训队列,不允许有人偷懒少站一列。
3. 交错数组(锯齿数组):俄罗斯套娃の千层套路
除了前面规规整整的数组,每一行的元素数目都是一样的以外。其实C#还给我们提供了一个不用像这样必须每行元素都要一样长,每行元素任意coder操作的数组,那就是交错数组!你可以自由决定你每一行的元素个数,就相当于你将很多个不同长度的一维数组从上往下依次放好。下面咱们来看看其怎么声明的:
//数组的申明
//变量类型[][] 交错数组名;
int[][] arr1;
//变量类型[][] 交错数组名 = new 变量类型[行数][];
int[][] arr2 = new int[3][];
//变量类型[][] 交错数组名 = new 变量类型[行数][]{一维数组1,一维数组2,一维数组3......};
int[][] arr3 = new int[3][] { new int[] { 1,2,3},
new int[] { 1,2},
new int[] { 1} };
int[][] arr4 = new int[][] { new int[] { 1,2,3},
new int[] { 1,2},
new int[] { 1} };
int[][] arr5 = { new int[] { 1,2,3},
new int[] { 1,2},
new int[] { 1} };
✨优势:每行长度可随意变化,像乐高积木一样自由(适合处理不规则数据,比如CSV文件)
// 声明方式:先造外层箱子,再给每个箱子塞内层箱子
int[][] jagged = new int[3][];
jagged[0] = new int[] {1,3,5}; // 第一个箱子装3个元素
jagged[1] = new int[] {2,4}; // 第二个箱子装2个元素(参差不齐才是美)
jagged[2] = new int[] {666}; // 第三个箱子只装一个卷王
// 使用姿势:
Console.WriteLine(jagged[1][0]); // 输出2(第二箱子的第一个元素)
实际使用:
int[][] arr6 = { new int[] {1,2,3,5},
new int[] { 4,5}};
//1.数组的长度
Console.WriteLine(arr6.GetLength(0));//行
Console.WriteLine(arr6[0].Length);//得到一行的列数
2.获取数组中的元素
Console.WriteLine(arr6[0][3]);
//Console.ReadLine();
//3.修改元素
//4.遍历元素
for (int i = 0; i < arr6.GetLength(0); i++)
{
for (int j = 0; j < arr6[i].Length; j++)
{
Console.Write(arr6[i][j] + "66666666 ");
}
}
看到没,这里又出现了GetLength()这个函数,所以这个函数很重要啊,请快速在心里默念几遍,加深自己的记忆, GetLength(),GetLength(),GetLength,0是行啊,1是列。
好的,我们已经介绍了C#中数组都长了些什么样子,接下来,我将传授各位一个秘密武器,那就是Array类里面提供的方便函数,接下来,和我一起来看看吧,Look in my eyes!,最后读者是MVP,笔者是躺赢狗。
三、 Array类——数组的瑞士军刀
在C#中,Array
是所有数组的隐形爸爸(所有数组都继承自它)。它自带一套让数组"旋转跳跃"的魔法方法,但稍有不慎就会踩雷💣
🎩 核心魔法技能(静态方法)
1、Sort:一键整理混乱现场,将数组中元素进行排序。
注意:会修改原数组!就像老妈帮你收拾房间,东西可能再也找不到了,是直接修改原数组
当你的数组里面存的都是一些整数时,那排序简直就是简简单单,直接排序,那如果是字符呢,字符串呢?
int[] chaos = {5,3,9,1};
Array.Sort(chaos); // chaos变成[1,3,5,9]
当你的数组是char[]
时,Array.Sort()
会化身无情的ASCII码判官,严格按照每个字符的Unicode值排序:
char[] letters = { 'd', 'A', '3', '!', '中' };
Array.Sort(letters);
Console.WriteLine(string.Join(" ", letters));
// 输出:! 3 A d 中
排序规则详解:
1、特殊符号 < 数字 < 大写字母 < 小写字母 < 汉字
(因为!
的Unicode是33,3
是51,A
是65,d
是100,中
是20013)
2、大小写敏感:大写字母永远排在小写前面
char[] caseTest = { 'a', 'Z', 'B' };
Array.Sort(caseTest);
// 结果:B Z a (B(66) < Z(90) < a(97))
字符串数组(string[])排序:文化差异的修罗场排序规则解密:字符串排序是字典序比较,但有个隐藏雷区——文化敏感性(Culture)。默认情况下:
string[] words = { "apple", "Banana", "cherry", "1", "_" };
Array.Sort(words);
// 结果:_ 1 apple Banana cherry
排序规则详解:
1、优先级阶梯:
符号/数字 → 大写字母 → 小写字母,_
的ASCII是95,1
是49,但字符串比较时数字被视为整体)
2、逐字符比较:
string[] test = { "a", "ab", "abc", "b" };
Array.Sort(test);
// 结果:a, ab, abc, b (像字典一样逐个字母比较长度)
3、文化差异炸弹陷阱:
在土耳其等地区,字母i
的大写是İ
(带点),而I
的小写是ı
(无点)。若未指定文化:
这个不是很重要,用到了再来了解也是一样的。
Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
string[] turkishTest = { "ı", "i", "İ" };
Array.Sort(turkishTest);
// 默认排序可能与英语环境不同!
常见小坑:
混合类型数组的迷惑行为
object[] mixedArray = { 5, "3", 'A' };
Array.Sort(mixedArray); // 运行时直接炸裂!
// 错误信息:至少一个对象必须实现IComparable
自以为是的数字字符串排序
string[] numbers = { "100", "20", "3" };
Array.Sort(numbers);
// 你以为会排成3,20,100?实际结果是"100","20","3"(按字符逐个比较)
2、Reverse:倒放人生
核心原则:只翻转元素顺序,绝不碰元素内容!无论数组里装的是数字、字符串还是对象,它都像搬家师傅一样,只管把箱子位置调换,绝不拆箱!
Array.Reverse(chaos); // chaos变成[9,5,3,1]
如果用在二维数组,只会反转第一层容器,内层纹丝不动
双层嵌套的俄罗斯套娃
string[][] jagged = {
new[] { "A", "B" },
new[] { "C", "D", "E" },
new[] { "F" }
};
Array.Reverse(jagged); // 只反转外层数组
/*
结果变成:
[ ["F"], ["C","D","E"], ["A","B"] ]
*/
// 如果想连内层一起反转
foreach(var arr in jagged){
Array.Reverse(arr);
}
/*
最终结果:
[ ["F"], ["E","D","C"], ["B","A"] ]
*/
Reverse()
默认只操作最外层,像剥洋葱一样需要手动层层深入- 深度反转需要递归操作(小心栈溢出!)
一些常见坑:
以为字符串内容会被反转:
string[] words = { "hello", "world" };
Array.Reverse(words);
// 数组变成 ["world", "hello"],但每个字符串还是原样
// 想反转字符串本身需要额外操作:
words[0] = new string(words[0].Reverse().ToArray()); // "hello" → "olleh"
- 它很单纯:只负责调换元素位置,不管元素是啥
- 它很专一:对多维数组永远只动最高维度
- 它很快:时间复杂度是 O(n/2),比你自己写循环高效
- 它很危险:遇到
null
或越界区域会瞬间翻脸
3、Copy:克隆人战争
int[] target = new int[4];
Array.Copy(chaos, target, 2); // 复制前两个元素到目标数组
// target变成[9,5,0,0]
🔍 细节:第三个参数是复制数量,不是索引!
4、IndexOf:寻人启事
int pos = Array.IndexOf(chaos, 5); // 返回1(下标位置)
int notFound = Array.IndexOf(chaos, 666); // 返回-1
💡 技巧:找不到时返回-1,记得判断!否则可能引发连锁反应
5、Resize:整容手术(其实是换头术)
Array.Resize(ref chaos, 6);
// chaos变成[9,5,3,1,0,0](原数组已被替换!)
惊天大坑:你以为是在原数组上扩容?实际是创建新数组,杀死旧数组!
当你在代码中写下 Array.Resize(ref arr, newSize)
,你以为数组像孙悟空的金箍棒一样自由伸缩?
int[] original = { 1, 3, 5 };
Array.Resize(ref original, 5);
// 发生了什么?
// 1. 偷偷new一个新数组:[0,0,0,0,0]
// 2. 把旧数组元素复制过去:[1,3,5,0,0]
// 3. 把original变量指向新数组
// 4. 旧数组被丢进垃圾回收站
灵魂拷问:原数组真的被"扩容"了吗?不!它已经死了,现在你看到的只是戴着原数组面具的新数组!阿伟已经死了!
Resize()的注意事项:
1、ref参数,不加相当于你没有将修改的东西传出来,至于这个ref是啥,好像听说他还有个兄弟叫out,我们后面再说。
int[] arr = { 1, 2 };
Array.Resize(ref arr, 3); // 必须加ref!否则白忙活
// 忘记ref的惨案:
Array.Resize(arr, 3); // 原数组纹丝不动!
2、值类型和引用类型区别
// 值类型数组:新空间填默认值
int[] nums = { 1 };
Array.Resize(ref nums, 3);
// 变成 [1, 0, 0]
// 引用类型数组:新空间都是null
Student[] students = { new Student() };
Array.Resize(ref students, 3);
// 变成 [student实例, null, null]
3、缩容时的断舍离,当断则断就像杨过断手臂一样。
string[] words = { "A", "B", "C", "D" };
Array.Resize(ref words, 2);
// 直接腰斩!变成 ["A", "B"]
// 被抛弃的"C","D":终究是错付了😭
场景 | 推荐方案 | 优势 |
---|---|---|
频繁增删 | List | 自动扩容、性能优化 |
超大数组 | MemoryPool | 内存池管理、减少GC压力 |
需要保留旧引用 | 手动创建新数组复制 | 完全掌控内存生命周期 |
记住:Array.Resize() 就像给数组做换头手术,手术成功后,你的数组将拥有新长度、新身体,但——不再是最初的那个TA!
🧙 实例方法(通过数组对象调用)
1、GetLength:多维数组の测谎仪
int[,] matrix = new int[3,4];
Console.WriteLine(matrix.GetLength(0)); // 3(第一维长度)
Console.WriteLine(matrix.GetLength(1)); // 4(第二维长度)
错误示范:用在交错数组会报错!交错数组请用array.Length
2、
Clone:深度复制?天真!
int[] source = {1,2,3};
int[] clone = (int[])source.Clone();
clone[0] = 666; // source不受影响(值类型安全)
// 但如果是对象数组...
Student[] students = { new Student() };
Student[] evilClone = (Student[])students.Clone();
evilClone[0].Name = "张三"; // 原数组里的对象也被修改了!
深水炸弹:Clone()是浅拷贝!引用类型数组会共享对象
好的 方法已经传授给你了,该你好好使用了,接下来我将为你展示一些常见的错误,希望你不要再犯了哦
坑1: 交错数组必须先初始化每一行(这个在上面也说明了,在这里强调是为了加深印象!!!)
int[][] jagged = new int[3][]; // 只创建了外层箱子
jagged[0][1] = 5; // 运行时NullReferenceException!内层箱子还没造
// 正确姿势:逐行初始化
jagged[0] = new int[2]; // 造第一个箱子
jagged[1] = new int[3]; // 造第二个箱子(尺寸不同)
jagged[0][1] = 5; // 安全操作
坑2:多维数组 vs 交错数组的GetLength
int[,] matrix = new int[3,4];
Console.WriteLine(matrix.GetLength(0)); // 正确:3
int[][] jagged = new int[3][];
Console.WriteLine(jagged.GetLength(0)); // 正确:3
Console.WriteLine(jagged[0].GetLength(0)); // 错误!如果jagged[0]未初始化
坑3:Clear方法的温柔陷阱
int[] nums = {1,2,3,4,5};
Array.Clear(nums, 0, nums.Length);
// 值类型数组:全变成0
// 引用类型数组:全变成null(可能引发后续空指针异常)
坑4:自定义对象的Sort需实现IComparable
class Cat{
public int Age;
}
Cat[] cats = new Cat[3];
Array.Sort(cats); // 运行时炸裂!因为Cat类没有定义比较规则
总结:
-
交错数组初始化口诀:
先造外箱,再造内箱,逐行填装,小心空箱 -
Array方法使用三连问:
- 是否会修改原数组?(如Sort、Reverse会)
- 是否处理引用类型?(Clone浅拷贝需警惕)
- 返回值是否需要接收?(如Resize要配合ref)
-
多维数组 vs 交错数组选择指南:
- 需要数学计算 → 多维数组(内存连续效率高)
- 需要灵活行长度 → 交错数组(但初始化更繁琐)
好的 现在你已经掌握了极大多数的数组的知识,好好去代码世界闯荡一番吧!芜湖起飞!
补充知识点:自定义比较器:
为什么要自定义比较器?
- 想把 ["Apple", "Banana", "cherry"] 按字母无视大小写排序
- 想让人物对象数组按年龄倒序+身高升序排列
- 需要把 ["100", "20", "3"] 识别为数字排序(而非字典序)
这时候就该祭出IComparer接口,让数组按你的节奏摇摆
场景1:字符串长度优先排序(短的在前)
class 长度比较器 : IComparer<string>
{
public int Compare(string x, string y)
{
// 处理null值(null视为无限小)
if (x == null) return -1;
if (y == null) return 1;
int lenCompare = x.Length.CompareTo(y.Length);
return lenCompare != 0 ? lenCompare : string.Compare(x, y);
}
}
// 使用示例:
string[] words = { "C#", "is", "awesome", null, "!" };
Array.Sort(words, new 长度比较器());
// 结果:null, "!", "C#", "is", "awesome"
最后两句代码的解释:
// 第一战:比较字符串长度
// CompareTo()返回值:
// - 负数表示x.Length < y.Length(x应该排在前面)
// - 0表示长度相同
// - 正数表示x.Length > y.Length(x应该排后面)
int lenCompare = x.Length.CompareTo(y.Length);
// 第二战:如果长度不同,直接返回长度比较结果
// 如果长度相同,进入字典序加时赛(调用默认字符串比较)
return (lenCompare != 0)
? lenCompare // 长度不同,按长度排序
: string.Compare(x, y); // 长度相同,按字母表顺序排
场景2:对象数组多条件排序
假设有Student
类:
class Student
{
public string Name { get; set; }
public int Age { get; set; }
public double Score { get; set; }
}
// 先按年龄降序,年龄相同按分数升序
class 学霸比较器 : IComparer<Student>
{
public int Compare(Student x, Student y)
{
// 处理null(假设null学生排最后)
if (x == null) return 1;
if (y == null) return -1;
int ageCompare = y.Age.CompareTo(x.Age); // 降序
if (ageCompare != 0) return ageCompare;
return x.Score.CompareTo(y.Score); // 升序
}
}
// 使用:
Student[] students = { /* 学生数据 */ };
Array.Sort(students, new 学霸比较器());
解释语句
// 第一步:比较年龄(降序规则)
// 注意这里故意用 y.Age.CompareTo(x.Age) 而不是 x.Age.CompareTo(y.Age)
// 假设:
// x.Age = 18,y.Age = 20 → y.CompareTo(x) = 20.CompareTo(18) = 1(正数)
// 返回正数表示x应该排在y后面 → 年龄大的排前面
int ageCompare = y.Age.CompareTo(x.Age);
// 如果年龄不同,直接返回比较结果(年龄大的胜出)
if (ageCompare != 0)
return ageCompare;
// 第二步:年龄相同时,比较分数(升序规则)
// 此时用x.Score.CompareTo(y.Score)正常比较
// 假设:
// x.Score = 85,y.Score = 90 → 85.CompareTo(90) = -1(负数)
// 返回负数表示x应该排在y前面 → 分数低的排前面
return x.Score.CompareTo(y.Score);