1. string类型的数组
简单说下string类型的数组吧。
就像之前说的,数组可以是任何类型。所以你当然可以创建string类型的数组。比如我们要写个程序,“从键盘输入10行字符串,然后倒序输出到屏幕”。代码大概长这样:
string[] array = new string[10]; // 创建一个10个元素的数组对象
for (int i = 0; i < 10; i++) // 循环从0到9
{
array[i] = Console.ReadLine(); // 从键盘读取一行字符串,存到数组里
}
for (int i = 9; i >= 0; i--) // 循环从9到0
{
Console.WriteLine(array[i]); // 输出数组的下一个元素
}
代码几乎没变!只是把数组类型从int换成了string。其它都一模一样!
2. 内存中的string数组
还有个有用的知识点。来看三张图:
图1. string对象在内存中的样子:
注意,字符串的文本内容不是直接存在变量里的:它会单独分配一块内存。而string类型的变量里存的是指向文本对象的地址(引用)。
图2. 整型数组在内存中的样子:
这张图你应该也见过。
图3. 字符串数组在内存中的样子:
左边是string[]类型的数组变量(存着数组对象的地址)。
中间是string类型的数组对象。
右边是存着文本的字符串对象。
string类型数组对象的每个格子里,存的不是字符串(文本)本身,而是它们的地址(引用)。就像string类型变量里存的是字符串(文本)的地址一样。
3. C#里的快速数组初始化
数组超有用,所以C#的开发者尽量让数组用起来更方便。首先,他们简化了数组初始化,也就是给数组填初始值。
很多时候,除了程序从外部读取的数据,还需要一些自己的内部数据。比如我们要存每个月的天数。代码可以这样写:
int[] months = new int[12];
months[0] = 31; // 一月
months[1] = 28; // 二月
months[2] = 31; // 三月
months[3] = 30; // 四月
months[4] = 31; // 五月
months[5] = 30; // 六月
months[6] = 31; // 七月
months[7] = 31; // 八月
months[8] = 30; // 九月
months[9] = 31; // 十月
months[10] = 30; // 十一月
months[11] = 31; // 十二月
不过其实有更短的写法——感谢C#的设计者:
// 一年每个月的天数
int[] months = new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
直接用逗号把所有数组值列出来就行!
是不是很方便?但还不止这些。
其实编译器可以根据数组变量的类型自动推断数组对象的类型。数组长度也能直接数大括号里的元素个数。
所以代码还能更短:
// 一年每个月的天数
int[] months = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
是不是很优雅?🙂
这种写法叫“快速数组初始化”。而且不仅仅是int类型才行哦……
// 一年每个月的名字
string[] months = { "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"};
4. foreach循环
遍历数组所有元素太常见了,所以专门有个循环!它叫foreach。写法如下:
foreach (int score in scores)
{
Console.WriteLine($"分数: {score}");
}
这里循环会自动遍历元素——不用手动写索引。不能直接修改元素(循环里只能读),但用来“走一遍”数组超方便。
编译器会把foreach循环的代码变成下面这样:
for (int i = 0; i < scores.Length; i++)
{
int score = scores[i];
Console.WriteLine($"分数: {score}");
}
所以其实没啥魔法。现在你也明白为啥不能通过score变量改数组元素了吧。
再来个例子:算所有分数的和。
int sum = 0;
foreach (int score in scores)
{
sum += score;
}
Console.WriteLine($"所有分数之和: {sum}");
什么时候用哪个循环?
- 如果需要索引或者要修改元素——用for循环。
- 如果只是遍历值——foreach更快更简洁。
5. 从末尾索引:^操作符
现在C#可以直接从数组末尾取元素了。以前要取最后一个元素,得写成这样:
int lastScore = scores[scores.Length - 1];
现在简单多了。只要在索引前加个^,就能从末尾数:
int lastScore = scores[^1]; // 最后一个元素
int penultimate = scores[^2]; // 倒数第二个元素
- ^1 —— 倒数第一个(最后一个)。
- ^2 —— 倒数第二个。
可视化
| 索引 | scores数组里的值 | 普通索引 | 从末尾索引 |
|---|---|---|---|
| 0 | 10 | 0 | ^5 |
| 1 | 15 | 1 | ^4 |
| 2 | 8 | 2 | ^3 |
| 3 | 22 | 3 | ^2 |
| 4 | 17 | 4 | ^1 |
score[^1] = 17, score[^2] = 22 —— 一目了然。
注意! 从末尾的索引是从1开始的,不是从0。
6. 区间:..操作符(range)
有时候你想从原数组里拿出子数组。C#里可以用专门的区间操作符..,超简洁超方便地拿到你想要的“片段”。
这种子数组有时叫切片(slice),由两个边界决定:起始和结束。
重点: 右边界不包含在结果里!
长啥样
int[] scores = { 10, 15, 8, 22, 17 };
int[] top3 = scores[0..3]; // 取0到2号元素: {10, 15, 8}
- scores[0..3] —— 取索引0(包含)到索引3(不包含)。
- 最终数组包含索引0、1、2的元素。
简写
如果没写左边界,就是从头开始:
int[] firstTwo = scores[..2]; // 等同于scores[0..2],也就是{10, 15}
如果没写右边界,就是到最后:
int[] fromThird = scores[2..]; // {8, 22, 17}
负索引
用^可以从末尾数索引:
int[] lastTwo = scores[^2..]; // {22, 17}
这里^2就是“倒数第二个元素”。
你也可以用负索引两边都写,取“从哪到哪”的区间:
int[] mid = scores[1..^1]; // {15, 8, 22}(从第2个到倒数第二个)
要记住啥
- 右边界永远不包含——写a..b,就是从a到b-1。
- 如果区间超出数组范围,会抛出IndexOutOfRangeException异常。
- 切片是新数组,原数组不会变。新数组怎么改都不会影响原来的。
实用例子
int[] arr = { 1, 2, 3, 4, 5, 6, 7 };
int[] arr = { 1, 2, 3, 4, 5, 6, 7 };
int[] start = arr[..3]; // {1, 2, 3}
int[] end = arr[^2..]; // {6, 7}
int[] middle = arr[2..5]; // {3, 4, 5}
int[] all = arr[..]; // {1, 2, 3, 4, 5, 6, 7}(完整拷贝)
GO TO FULL VERSION