作为一名专注于 .NET 技术的开发者,我们经常需要处理数据的动态集合。你可能遇到过这样的场景:最初定义了一个固定大小的数组,但在程序运行过程中,发现需要存储更多的数据,或者为了节省内存需要丢弃一部分数据。由于 C# 中的数组在底层是固定大小的,这就引出了一个常见的问题——我们该如何安全、高效地改变一维数组的大小?
在这篇文章中,我们将深入探讨 C# 中调整数组大小的核心方法,剖析其背后的工作原理,并通过多个实际的代码示例展示不同场景下的最佳实践。无论你是初学者还是有一定经验的开发者,这篇文章都将帮助你更好地理解内存管理机制,编写出更健壮的代码。
为什么我们需要调整数组大小?
在 C# 中,INLINECODE92812285 是所有数组的基础类型。当你声明 INLINECODEeb23b3e3 时,CLR 会在内存中分配一块连续的固定空间来存储 5 个整数。这种设计带来了极高的访问速度,但也付出了灵活性降低的代价。
与 INLINECODEe5bea0e5 等动态集合不同,数组一旦创建,其长度(Length)就是不可变的。如果你尝试直接修改 INLINECODE3ec2e1e8 属性,编译器会直接报错。那么,当我们确实需要“扩容”或“缩容”时,该怎么办呢?
核心解决方案:Array.Resize 方法
为了解决这个问题,C# 提供了一个非常实用的静态方法:INLINECODEceec3ed0。这是我们在不切换到 INLINECODE7ecdc526 的情况下,处理数组大小调整的首选方案。
方法签名与语法解析
让我们先来看一下它的定义。这是一个泛型方法,能够适用于任何类型的数组(无论是值类型还是引用类型)。
public static void Resize(ref T[] array, int newSize);
这里有几个关键的细节值得我们注意:
- ref 关键字:这是最关键的一点。请注意,参数 INLINECODE6c2045bf 前面加上了 INLINECODE8b7e9f32 关键字。这意味着该方法可以改变传入变量引用的内存地址。换句话说,
Resize可能会返回一个全新的数组对象,并用这个新对象替换你传入的那个旧变量。
- T (泛型):表示数组的元素类型。这保证了我们在调整大小时,类型是安全的。
- newSize:这是你希望数组达到的新容量。
参数说明
- array: 这是一个基于零索引的一维数组,我们需要调整它的大小。如果你传递
null,该方法会创建一个新的数组。如果你传递一个现有的数组,它的内容将会被保留(根据新大小进行截断或填充)。 - newSize: 新数组所需的元素总数。
异常处理
在进行开发时,我们必须考虑到错误处理。如果我们将 INLINECODEd1987bcd 设置为负数,例如 INLINECODEe3fc10be,该方法将立即抛出 ArgumentOutOfRangeException。因此,在实际业务代码中,如果你对输入数据来源不确定,最好在调用前检查其是否大于等于 0。
性能考量
作为一个负责任的开发者,我们需要了解代码的性能影响。INLINECODEa78c3446 是一个 O(n) 操作,其中 n 是 INLINECODE14f2585c。
为什么是 O(n)?
- 内存分配:系统需要在托管堆上寻找一块新的、连续的内存空间来容纳新数组。
- 数据复制:元素从旧数组复制到新数组需要时间。
这告诉我们要避免在性能极其敏感的循环中频繁调用 INLINECODE3d739567。如果你需要频繁添加元素,INLINECODEddd7676e(内部也是通过数组实现,但有智能的扩容策略)通常是更好的选择。
深入示例:从实践中学习
为了彻底掌握这个方法,让我们通过几个实际的场景来演练。我们将覆盖“扩容”(增大数组)和“缩容”(减小数组)的情况,并观察数据的走向。
示例 1:缩小数组(数据截断)
想象一下,你正在处理一个任务列表,最初申请了 7 个名额,但最后只完成了前 4 个。为了节省内存,我们可能希望将数组缩小到实际使用的大小。
在下面的代码中,我们将把一个长度为 7 的字符串数组缩小到 4。让我们看看多余的元素会发生什么。
using System;
public class ArrayResizeExample
{
public static void Main(string[] args)
{
// 1. 初始化原始数组:包含 7 种编程语言
string[] techStack = { "C#", "Java", "C++", "Python", "HTML", "CSS", "JavaScript" };
Console.WriteLine("--- 原始数组 ---");
Console.WriteLine($"当前长度: {techStack.Length}");
foreach (var item in techStack)
{
Console.WriteLine(item);
}
// 2. 计算新的大小:我们只保留前 4 个
int newSize = 4;
// 3. 调用 Resize
// 注意:ref 关键字在这里起作用,techStack 变量将指向新的内存地址
Array.Resize(ref techStack, newSize);
Console.WriteLine($"
--- 调整大小后 (长度: {techStack.Length}) ---");
foreach (var item in techStack)
{
Console.WriteLine(item);
}
Console.WriteLine("
注意: 原数组末尾的 ‘HTML‘, ‘CSS‘, ‘JavaScript‘ 已经丢失了。");
}
}
输出结果:
--- 原始数组 ---
当前长度: 7
C#
Java
C++
Python
HTML
CSS
JavaScript
--- 调整大小后 (长度: 4) ---
C#
Java
C++
Python
注意: 原数组末尾的 ‘HTML‘, ‘CSS‘, ‘JavaScript‘ 已经丢失了。
关键见解: 当新大小小于原始大小时,INLINECODE6d008812 会保留从索引 0 开始到 INLINECODEc327f36c 的元素。超出这个范围的数据将被永久丢弃,无法恢复。
—
示例 2:扩大数组(默认值填充)
现在让我们看看相反的情况。假设我们的团队扩大了,需要在一个包含 5 个人的数组中增加更多的空位。
在下面的示例中,我们将把数组从 5 扩大到 8。我们将重点观察新增加的空间里填的是什么。
using System;
public class ArrayUpsizeExample
{
public static void Main(string[] args)
{
// 1. 初始化一个较小的数组
// 对于引用类型(如 string),初始值为 null
string[] names = new string[5];
names[0] = "Alice";
names[1] = "Bob";
names[2] = "Charlie";
Console.WriteLine("--- 原始数组 ---");
Console.WriteLine($"长度: {names.Length}");
PrintArray(names);
// 2. 扩大数组到 10
// 此时原数组只有 5 个元素,我们需要判断数据会发生什么
Array.Resize(ref names, 10);
Console.WriteLine("
--- 扩大后数组 (Target Size: 10) ---");
Console.WriteLine($"长度: {names.Length}");
PrintArray(names);
Console.WriteLine("
观察:原有数据被保留,新增的索引位置被填充为 null (对于字符串) 或 0 (对于整数)。");
}
// 辅助方法:安全打印数组内容,处理 null
static void PrintArray(string[] arr)
{
for (int i = 0; i < arr.Length; i++)
{
// 如果是 null,打印 字样以便观察
string value = arr[i] ?? "";
Console.WriteLine($"Index {i}: {value}");
}
}
}
输出结果:
--- 原始数组 ---
长度: 5
Index 0: Alice
Index 1: Bob
Index 2: Charlie
Index 3:
Index 4:
--- 扩大后数组 (Target Size: 10) ---
长度: 10
Index 0: Alice
Index 1: Bob
Index 2: Charlie
Index 3:
Index 4:
Index 5:
Index 6:
Index 7:
Index 8:
Index 9:
观察:原有数据被保留,新增的索引位置被填充为 null (对于字符串) 或 0 (对于整数)。
重要概念: 请注意索引 5 到 9 的位置。对于 INLINECODEc2366e1f 这样的引用类型,它们被初始化为 INLINECODEfb8817e1。如果是 INLINECODEa2d0aa2c,它们会被初始化为 INLINECODEa9573bb0。这一点在后续逻辑中非常重要,否则你可能会遇到 NullReferenceException。
—
示例 3:处理值类型(整数数组)
为了进一步巩固我们的理解,让我们来看看值类型(Struct)的行为是否有所不同。在 C# 中,值类型的默认值与引用类型不同。
using System;
public class ValueTypeResizeExample
{
public static void Main(string[] args)
{
// 定义一个包含偶数的数组
int[] evenNumbers = { 2, 4, 6, 8, 10 }; // 长度 5
Console.WriteLine("初始整数数组:");
Console.WriteLine(string.Join(", ", evenNumbers));
// 我们将数组缩小到 2
Array.Resize(ref evenNumbers, 2);
Console.WriteLine("
缩小到 2 后:");
Console.WriteLine(string.Join(", ", evenNumbers)); // 输出: 2, 4
// 现在再将其扩大到 5
Array.Resize(ref evenNumbers, 5);
Console.WriteLine("
重新扩大到 5 后:");
// 注意:后三个元素现在被重置为 int 的默认值,即 0
Console.WriteLine(string.Join(", ", evenNumbers)); // 输出: 2, 4, 0, 0, 0
}
}
输出结果:
初始整数数组:
2, 4, 6, 8, 10
缩小到 2 后:
2, 4
重新扩大到 5 后:
2, 4, 0, 0, 0
实战提示: 很多新手容易犯的错误是认为 INLINECODEd2da4931 会“记住”之前的数据。在这个例子中,当我们把数组从 2 扩回 5 时,原本的 INLINECODE663c12e4 已经丢失了,取而代之的是默认值 INLINECODE54153454。这再次印证了 INLINECODE5cd63f2d 涉及底层的内存重新分配。
—
示例 4:处理 Null 数组
如果我们在调用 INLINECODE3055694e 之前,数组变量是 INLINECODEb7903ce0,会发生什么?其实,这是一个非常实用的“快速创建数组”的技巧。
using System;
class NullArrayTest
{
static void Main()
{
string[] items = null;
// 试图输出 items.Length 会在这里抛出 NullReferenceException
// Console.WriteLine(items.Length);
Console.WriteLine("调用 Resize 之前,items 是 null。");
// 将 null 数组调整为大小 3
// Array.Resize 内部会自动处理 null 引用,分配一个新数组
Array.Resize(ref items, 3);
Console.WriteLine($"调用 Resize 后,items 长度为: {items.Length}");
// 现在我们可以安全地赋值了
items[0] = "New Item 1";
items[1] = "New Item 2";
items[2] = "New Item 3";
foreach (var item in items)
{
Console.WriteLine(item);
}
}
}
这个特性表明,INLINECODEe9e08870 实际上等同于“如果没有数组就创建一个,如果有了就调整大小”,这在某些初始化逻辑中可以简化 INLINECODE472e29ec 的判断代码。
最佳实践与常见陷阱
通过上面的讲解,我们已经掌握了基础知识。在实际的企业级开发中,我们还需要注意以下几点。
1. 性能敏感场景下的建议
虽然 INLINECODE4d3a5cd9 很方便,但它涉及到内存复制。如果你在一个 INLINECODEdd45ae25 循环中不断地给数组添加元素(例如 Resize 10000 次),这将导致严重的性能瓶颈。
错误的写法:
int[] numbers = new int[0];
for (int i = 0; i < 10000; i++)
{
// 每次循环都重新分配内存并复制旧数据,极度低效!
Array.Resize(ref numbers, numbers.Length + 1);
numbers[i] = i;
}
推荐的替代方案:
如果你不知道数组最终会有多大,或者它是动态增长的,请直接使用 List。
// List 内部通过倍增策略来优化扩容,比反复 Resize 快得多
List numberList = new List();
for (int i = 0; i < 10000; i++)
{
numberList.Add(i);
}
// 如果最后确实需要数组,可以调用 .ToArray()
int[] finalArray = numberList.ToArray();
2. 多维数组的限制
务必牢记,INLINECODE57819bb3 仅支持一维数组(也称为向量)。如果你尝试传递一个二维数组 INLINECODE93265d55,代码将无法编译。对于多维数组,你通常需要手动创建一个新的多维数组并使用 Array.Copy 循环复制每一维的数据,这是一个相对复杂的过程。
3. 引用类型与浅拷贝
由于 INLINECODE78ad2074 执行的是内存复制(INLINECODE1d1a11df),对于引用类型(如自定义类的对象数组),它执行的是浅拷贝。这意味着新旧数组(在调整大小过程中的瞬间)将指向内存中相同的对象实例。如果你修改了数组中某个对象的属性,这个修改是全局的,无论该数组引用存在于哪个数组变量中。这与对象的深拷贝是有区别的。
总结与后续步骤
在本文中,我们全面探讨了如何使用 Array.Resize 方法来处理 C# 中的一维数组。
我们回顾了以下几点核心内容:
- 基本用法:利用
ref关键字将数组调整为指定大小。 - 数据保留机制:扩容时填充默认值(null 或 0),缩容时截断数据。
- 性能特性:这是一个 O(n) 操作,不应在紧密循环中滥用。
- 实战场景:了解了它如何处理 null 数组、整数数组以及字符串数组。
掌握这个方法后,当你需要在处理固定配置、日志缓冲区或者与旧版 API 交互时,将更加游刃有余。虽然现代 C# 开发中更多推荐使用 List 或更现代的集合,但理解数组这一基石的操作原理,将帮助你写出性能更优、bug 更少的代码。
希望这篇文章对你有所帮助。接下来,你可以尝试在自己的项目中重构一些旧的数组处理逻辑,看看是否可以通过这个方法让代码变得更简洁。祝编码愉快!