为什么数组需要在定义时设置维度?

4
我在想,为什么我们不能像这样定义一些东西呢:

我只是想知道,为什么我们不能像这样定义一些东西:

int[] arr = new int[];
arr[0] = 1;

就像列表一样,自动调整大小。 我想知道的是为什么这是不可能的,我们需要每次以以下方式设置大小:

int[] arr = new int[1];
arr[0] = 1;

2
因为这就是数组的工作方式。它们被语言定义为固定大小的容器。 - Frédéric Hamidi
3
我觉得这是一个非常有趣的问题。正如你自己所说,回答“因为C#/CLR规范规定如此”并不能完全解释。尝试这样做,特别是将其与C++的数组和向量进行比较,让我有了新的见解。 - Peter - Reinstate Monica
@PeterSchneider 我部分同意这个问题本身的概念是有趣的,但是所写的问题很差,对于 OP 的技能并没有给我们任何线索。在我看来,它太过广泛,无法回答。我们不知道 OP 的技能水平,因此从头开始完全解释为什么会这样很难。一个恰当的答案可能像一本简短的书那样长,因此我认为这个问题以其当前的格式无法回答。像“因为规格如此”之类的答案表明我正在应对不能编程的程序员 - user2140173
@Peter Schneider 我同意。这是一个“愚蠢的问题”,许多人不知道答案,因此无论有意还是无意,都是非常出色的。即使是我思考只能基于有限的信息。 - NPSF3000
1
@mehow,如果你能回答这个问题并提供比“规格说明”更好的答案,我将非常高兴学习。 - NPSF3000
如果你还没有阅读过的话,我建议看一下Eric Lippert的文章《Arrays Considered Somewhat Harmful》。 - Daniel Pryden
5个回答

12

List<T>的调整大小基于在需要时创建一个新数组来完成。

想一想这里的底层实现是什么样子的。当你分配一个数组时,它会保留一块内存空间,并且引用实际上指向该内存空间。如果你需要存储更多值而没有预留足够的空间,就需要在其他地方分配更多的内存,但是你不能改变引用以指向新的内存,因为这些引用散布在各处。(数组不知道有哪些引用指向它。)

显然可以通过增加一层间接性来解决这个问题--使得最初的引用是指向一个对象,该对象跟踪存储真实数据的位置,因此可以在需要时重新分配内存。这正是 List<T> 做的... 但这意味着有额外的间接层次。它的效率成本较高,部分原因是实际数据可能与 List 对象相隔很远,对缓存不利……并且仅仅通过额外的间接层次也会带来一定的成本。

基本上,如果你需要一个动态大小的集合,请使用 List<T>--那就是它存在的原因。如果你从开始就知道最终大小并且想要受益于数组“更接近底层”的方面,请使用数组。

数组是一个相对较低级别的概念--如果你需要一个高级抽象,请使用其中之一……


2
也许我们应该强调你隐含的话——这是一个效率问题。List中的间接引用可能会在某个内部循环中总体上变得昂贵。我假设在许多情况下,JIT可以使数组元素访问与C数组访问一样高效,而我认为在大多数情况下访问List元素则不能。因此,C#(或CLR)本可以像许多脚本语言一样设计动态数组,但代价是运行时更慢。 - Peter - Reinstate Monica
数组分配的内存是连续的吗?即使如此,为什么编译器不将数组移动到空闲位置,让我来做呢?最终结果是相同的,但如果我这样做可能会引发一些错误。 - Jake Manet
我之所以问这个问题,是因为我不明白为什么对数组的分散引用会阻止重新分配(引用只是在幕后更新,就像在垃圾回收运行后一样)。--哦,我明白你的意思了:对于日常用途来说太昂贵了。 - Peter - Reinstate Monica
@PeterSchneider:关键是你不希望每次有人向集合中添加一个项目时都进行协调(检查整个堆是否有对数组的引用!)。 - Jon Skeet
@JonSkeet 是的,这正是我所期望的。 - dcastro
显示剩余5条评论

7
因为数组在内存中是连续的,所以在创建数组时必须分配足够的内存来存储其内容。
假设你有一个包含100个项目的数组。现在你再添加一个项目,就必须要声明第101个项目之后的内存地址。如果该地址已被使用,怎么办?
这就是为什么不能动态调整数组大小的原因。

但在C#中,你有各种愿意自动调整大小的容器,其中一些还是连续的(比如List)。那么它们之间有什么区别呢?也许可以将原始问题重新表达为“为什么还需要数组?”考虑到数组和列表之间广泛的功能重叠。(Jon Skeet给出了答案,并进行了后续讨论。) - Peter - Reinstate Monica
2
我认为这是一个后续问题,而不是主要问题。但我的答案是 - 它们是不同抽象级别。这就像问我们为什么需要一个 Queue<T> 当我们已经有了一个 ConcurrentQueue<T>?不同的抽象级别。 - dcastro
数组存在的原因是有时候,这就是你所需要的 - 你不需要 List<T> 给你的任何花哨的东西。它们也是大多数其他集合的构建块。 - dcastro

2
我的猜测是“因为它被指定为这样”-这使得数组具有特定的性能特征和高度理想的优化能力。虽然这些优点可能已经被设计者注意到,并影响了使用类似结构的其他语言(如C、C++、Java-Footnote 1),但他们确实可以定义它来做 任何事情
例如,List<T>必须调整内部数组的大小,通常会分配比必要更多的内存(除非您刚好使用了分配的空间),同时创建垃圾。
C#语言规范的1.8节中有以下内容
一个数组是一种数据结构,包含通过计算的索引访问的多个变量。数组中包含的变量,也称为数组的元素,都是相同类型的,这种类型称为数组的元素类型。
数组类型是引用类型,数组变量的声明只是为数组实例保留了引用空间。实际的数组实例是在运行时使用new运算符动态创建的。新操作指定了新数组实例的长度,该长度在实例的生命周期内保持不变。数组元素的索引范围从0到Length-1。new运算符会自动将数组的元素初始化为它们的默认值,例如,对于所有数值类型,其默认值为零,对于所有引用类型,其默认值为null。
此外,没有提到的是,数组边界检查被认为是“软件工程原则”之一-如ECMA 334规范中描述设计目标时所述:
语言及其实现应支持软件工程原则,如强类型检查、数组边界检查、检测未初始化变量的尝试以及自动垃圾回收。软件健壮性、耐久性和程序员生产力都很重要。
C#(发音为“See Sharp”)是一种简单、现代、面向对象和类型安全的编程语言。C#起源于C家族语言,对C、C++和Java程序员来说将会非常熟悉。脚注1 - C#语言规范介绍

2
@mwhow,为什么会这样呢?你知道吗?John Skeet的回答指向了实现方式,我的也是,但如果他们愿意,他们可以采用不同的实现方式。 - NPSF3000
“将会立即熟悉C、C++和Java程序员。”这完全是题外话。在C#中,数组与C、Java等的数组实际上有些不同 - user2140173
@Mehow,与其完全偏离主题,这是C#的一个关键设计目标。 C、C++或Java是否非固定长度?注意:“熟悉”≠“相同”。 - NPSF3000

1
那是因为数组需要分配内存,框架需要知道需要多少内存。
如果你想要一个动态大小的“数组”,请使用List。
列表实际上包装了一个数组。当添加的项目超过当前数组时,会创建一个新数组(通常是两倍大小),旧项目会移动到新数组中。删除则相反。

2
OP 想知道为什么。 - Sam Leach
1
@Sam,“因为规范这样说”?说真的,这就像问“为什么int是32位宽度?”一样。语言就是这样工作的。 - Frédéric Hamidi

1
因为数组的大小是静态的,定义时不可变的。如果您想要一个动态大小的集合,有其他的选择,比如您提到的列表。
但是您可以延迟声明数组的大小。
int[] numbers;
numbers = new int[10];  
numbers = new int[20];  

2
OP 想知道为什么。 - Sam Leach

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接