C#数组“push”最佳方法

37

大家好

我知道这是一个广泛讨论的问题,但我似乎找不到一个确定的答案。我知道可以使用List来完成我要求的内容,但这不会解决我的问题。

我教授一门初级C#课程。我有PHP和JavaScript背景,因此倾向于以这些语言为基础思考问题。

我一直在寻找向学生展示如何动态添加元素到数组的最佳方法。我们不能使用List,因为它们不是我目前教授的课程的一部分(尽管它们会在本学期后期出现)。

因此,我正在寻找最简单的方法来执行Java array.push(newValue)类型的情况,这对新手编码人员易于理解,而不会教给他们不良实践。

以下是我目前解决这个问题的方法:

for (int i = 0; i < myArray.Length; i++)
{
   if(myArray[i] == null)
   {
       myArray[i] = newValue;
       break;
   }
}

我只需要知道这是否是可接受的方法,是否教给他们任何不良实践,或者是否有更好/更简单的方法来实现我的目标。

编辑 问题的关键是元素需要被添加到数组中的第一个空插槽中,就像Java push函数一样。

如果有任何建议,将不胜感激。


3
那不是向数组添加元素,只是将其赋值 - mjwills
3
@phunder,你不能在C#中向数组中添加元素。数组是固定大小的。 - aloisdg
1
@phunder:Java中的数组(而不是列表)是否有push方法并允许添加项目?你能发一份证明吗?我不是Java专家,试图搜索push文档以帮助您找到C#等效项,但我发现Java中的数组与C#中的行为相同。您是指JS而不是Java吗? - Dennis
1
不能,在 C# 中无法使用数组推入操作。 - mjwills
3
@phunder 我建议你避免尝试以这种方式使用数组,特别是当你在教初学者时。在C#中,数组是固定大小的,课程应该是不同的数据类型适用于不同的任务。 - Kyle
显示剩余5条评论
14个回答

31

array.push 就像 List<T>.Add。.NET 数组是固定大小的,因此您实际上无法添加新元素。您只能创建一个比原始数组大一个元素的新数组,然后设置最后一个元素,例如:

Array.Resize(ref myArray, myArray.Length + 1);
myArray[myArray.GetUpperBound(0)] = newValue;

编辑:

我不确定这个答案是否适用于问题的这次编辑:

问题的关键在于需要将元素添加到数组中第一个空位,就像Java的push函数一样。

我提供的代码实际上是在追加一个元素。如果目标是设置第一个空元素,那么可以这样做:

int index = Array.IndexOf(myArray, null);

if (index != -1)
{
    myArray[index] = newValue;
}

编辑:

这里有一个扩展方法,封装了上述逻辑,并返回插入值的索引,如果没有空元素则返回-1。请注意,此方法也适用于值类型,将具有该类型的默认值的元素视为“空”。

public static class ArrayExtensions
{
    public static int Push<T>(this T[] source, T value)
    {
        var index = Array.IndexOf(source, default(T));

        if (index != -1)
        {
            source[index] = value;
        }

        return index;
    }
}

3
这个方案是可行的,但我不确定学生们是否会喜欢。 :) - aloisdg
谢谢您的建议。我同意@aloisdg的看法,这可能对新手来说有点复杂。 - phunder
我喜欢你的第二个建议。这比我的方法更简单。非常感谢! - phunder
6
请注意,这仅适用于数组类型为引用类型或可为空值类型。对于标准值类型,它无法工作,因为它们不能为null - jmcilhinney
我正要说@jmcilhinney - Brett Caswell
1
仅供记录,答案的编辑是在我的先前评论之后添加的,因此只要您将类型的默认值(例如数字类型的零)视为“空”,则该编辑可以适用于标准值类型。 - jmcilhinney

9
您的问题有些偏离主题。特别是,您说“该元素需要添加到数组中的第一个空槽位,就像Java的push函数一样。”。
  1. Java的数组没有push操作 - JavaScript有。 Java和JavaScript是两种非常不同的语言
  2. JavaScript的push函数不会按照您描述的方式运行。当您将值“推入”JavaScript数组时,该数组会扩展一个元素,并将该新元素分配给推送的值,请参见:Mozilla的Array.prototype.push函数文档

动词“Push”在除JavaScript以外的任何语言中都不会与数组一起使用。我怀疑它只在JavaScript中存在,因为它可能存在(因为JavaScript是一种完全动态的语言)。我相当确定它不是有意设计的。

在C#中编写类似JavaScript样式的Push操作可以采用以下效率略低的方式:

int [] myArray = new int [] {1, 2, 3, 4};
var tempList = myArray.ToList();
tempList.Add(5);
myArray = tempList.ToArray();   //equiv: myArray.Push(5);

在一些容器类型中特别是堆栈、队列和双端队列中,“推”(push)是一个常用操作(其中双端队列有两个“推”- 一个从前面,一个从后面)。我建议您在解释数组时不要将Push作为动词。这对计算机科学专业的学生没有任何帮助。

在C#中,与大多数传统过程化语言一样,数组是具有单个类型元素的集合,包含在固定长度的连续内存块中。当您分配数组时,为每个数组元素分配空间(在C#中,这些元素初始化为类型的默认值,引用类型的null)。

在C#中,引用型数组填充了对象引用,而值类型数组填充了该值类型的实例。因此,4个字符串的数组使用的内存与4个应用程序类实例的数组相同(因为它们都是引用类型)。但是,4个DateTime实例的数组要比4个short整数的数组长得多。

在C#中,数组的实例是System.Array的一个引用类型实例。数组有一些属性和方法(如Length属性),除此之外,你不能做太多事情:你可以使用数组索引读取(或写入)单个元素。T类型的数组也实现了IEnumerable,因此你可以迭代数组的元素。
数组是可变的(数组中的值可以被写入),但它们具有固定的长度,无法扩展或缩短。它们是有序的,无法重新排列(除非手动调整值)。
C#数组是协变的。如果你问C#语言设计者,这将是他们最后悔的功能之一。这是破坏C#类型安全性的少数几种方式之一。考虑以下代码(假设Cat和Dog类继承自Animal):
Cat[] myCats = new Cat[]{myCat, yourCat, theirCat};
Animal[] animals = (Animal[]) myCats;     //legal but dangerous
animals[1] = new Dog();                   //heading off the cliff
myCats[1].Speak();                        //Woof!

“特性”是由于.NET Framework初始版本缺乏泛型和显式协变/逆变,以及复制Java的“特性”而产生的结果。
数组确实出现在许多核心.NET API中(例如System.Reflection)。它们之所以存在,是因为最初的版本不支持泛型集合。
一般来说,有经验的C#程序员不会在应用程序中使用太多数组,而更喜欢使用功能更强大的集合,如List、Dictionary、HashSet等。特别地,该程序员倾向于使用IEnumerable传递集合,这是所有集合都实现的接口。使用IEnumerable作为参数和返回类型(在可能和合理的情况下)的重要优点是,通过IEnumerable引用访问的集合是不可变的。这有点类似于在C++中正确使用const。
在每个人掌握基础知识后,您可以考虑在关于数组的讲座中添加一个新的Span类型。Span可能使C#数组变得有用。
最后,LINQ (语言集成查询) 通过向 IEnumerable<T> 添加扩展方法,为集合引入了许多功能。请确保您的学生在代码顶部没有使用 using System.Linq;语句-将LINQ混合到初学者的数组课程中会使他们感到困惑。

顺便问一下:你教的是什么类?在什么级别?


1
myArray = myArray.ToList().Add(5).ToArray(); 的问题在于 Add() 方法的返回类型是 void - Wiktor Zychla
@WiktorZychla:谢谢。已经修复了。旧代码更容易阅读(即使它不能编译)。 - Flydog57
是的,这很不幸,一些简单的方法,比如“Add”,并没有被设计成可以流畅地调用。 - Wiktor Zychla
1
如果你要使用Linq,最好跳过ToList()。只需要myArray.Append(5).ToArray()就可以了。当然,Append(5)并不是将5添加到数组中,而是创建一个枚举器,它会遍历数组,然后加上5,因此之后需要使用ToArray() - Ben Voigt

5

如前所述,List提供了一种干净的方式来添加元素,如果要使用数组完成相同的操作,则需要调整其大小以容纳额外的元素,参见下面的代码:

int[] arr = new int[2];
arr[0] = 1;
arr[1] = 2;
//without this line we'd get a exception
Array.Resize(ref arr, 3);
arr[2] = 3;

关于您的循环想法:

初始化数组时,数组的元素会被设置为它们的默认值。所以,如果您想填充存储引用类型(其默认值为null)的数组中的“空白”,则您的方法是有效的。

但对于值类型,它们会被初始化为0,因此这种方法不起作用!


谢谢您提供的信息!我没有考虑到默认值可能不是null! - phunder

5

有另一种方法可以完成这个,这种方法非常适合扩展类。

public static void Push<T>(ref T[] table, object value)
{
    Array.Resize(ref table, table.Length + 1);
    table.SetValue(value, table.Length - 1);
}

这里正在进行的是,我们正在调整大小,然后为由Array.Resize(...)方法创建的新元素设置值。

这里有一个片段,其中包含示例用法。


4

C#中没有array.push(newValue)。在C#中,你不能向数组中添加元素。我们使用的是List<T>。唯一需要考虑的是(仅用于教学目的)ArrayList(没有泛型,并且它是一个IList,因此...)。

static void Main()
{
    // Create an ArrayList and add 3 elements.
    ArrayList list = new ArrayList();
    list.Add("One"); // Add is your push
    list.Add("Two");
    list.Add("Three");
}

感謝您的評論。很不幸,我現在不想用新的數據類型來困擾我的初學者學生。我只需要知道我的方法是否可行,或者我正在教授任何不良實踐。 - phunder
@phunder 如果没有另一个数据结构,你会遇到问题。数组不是你要找的结构。 - aloisdg
@mjwills 的确。我真的认为 OP 应该也使用 List<T>。 - aloisdg
1
@phunder,请不要使用“push”这个动词来描述数组,以免让初学者产生困惑。是的,JavaScript 数组有一个 push 方法,但这并不是任何语言中数组的本质概念。Push 在队列、栈和双端队列中很有用,但不适用于数组。如果您想了解如何实现类似 JavaScript 的 Array.Push,请参考我的答案。但是,将其作为示例而非基本操作。 - Flydog57

4

C#在这方面与JavaScript有些不同。由于严格的检查,定义数组的大小,并且应该了解有关数组的所有内容,例如其边界、最后一个项目的位置以及未使用的项目。如果要调整大小,则应将数组的所有元素复制到新的、更大的数组中。

因此,如果您使用原始数组,则除了维护最后一个空的可用index并在此索引处分配项目之外,没有其他方法,就像您已经做的那样。

但是,如果您想让运行时维护此信息并完全抽象化数组,但仍然在底层使用数组,则C#提供了一个名为ArrayList的类,提供此抽象。

ArrayList抽象了一个松散类型的Object数组。请参见此处的源代码。

它处理所有问题,如调整数组大小、维护最后可用的索引等。但是,它将此信息抽象/封装起来,使您只使用ArrayList

要将任何类型的项推送到底层数组的末尾,请调用ArrayList上的Add方法,如下所示:

/* you may or may not define a size using a constructor overload */
var arrayList = new ArrayList(); 

arrayList.Add("Foo");

编辑:关于类型限制的说明

与所有编程语言和运行时一样,C#将堆对象分类为不会进入堆的仅保留在函数参数堆栈上的对象。 C#通过名称值类型 vs. 引用类型来区分这个差异。所有值进入堆栈的事物都称为值类型,而进入堆的则称为引用类型。这与JavaScript的对象字面量之间的区别类似,但并不完全相同。

在C#中,您可以将任何内容放入ArrayList中,无论该内容是值类型还是引用类型。就类型缺失而言,这使它与JavaScript数组最接近,尽管三者中没有一个(JavaScript数组,JavaScript语言和C#ArrayList)是真正的无类型。

因此,您可以将数字文字,字符串文字,您自己创建的类的对象,布尔值,浮点数,双精度数,结构等任何东西放入ArrayList中。

这是因为ArrayList在内部维护并存储您放入其中的所有内容,将其存储为Object数组,正如您在我的原始答案和链接源代码中所注意到的那样。

当您放入的内容不是对象时,C#会创建一个新的Object类型的对象,将您放入ArrayList中的东西的值存储在这个新的Object类型的对象中。此过程称为装箱,与JavaScript的装箱机制非常相似。

例如,在JavaScript中,虽然您可以使用数字字面量调用Number对象上的函数,但无法向数字字面量的原型添加任何内容。

// Valid javascript
var s = 4.toString();

// Invalid JavaScript code
4.prototype.square = () => 4 * 4;
var square = 4.square();

就像JavaScript在调用toString方法时将数字文字4装箱一样,当将非对象类型放入ArrayList中时,C#会将所有不是对象的东西都装箱为Object类型。
var arrayList = new ArrayList();

arrayList.Add(4); // The value 4 is boxed into a `new Object()` first and then that new object is inserted as the last element in the `ArrayList`.

这涉及到一定的惩罚,就像在JavaScript中一样。

在C#中,您可以避免这种惩罚,因为C#提供了一个强类型版本的ArrayList,称为List<T>。因此,您不能将任何东西放入List<T>; 只有T类型。

然而,我从您的问题文本中假设您已经知道C#具有用于强类型项的通用结构。您的问题是要求类似于JavaScript的数据结构,展现出无类型和弹性的语义,比如JavaScript的Array对象。在这种情况下,ArrayList最接近。

从您的问题中也清楚地看出,您的兴趣是学术性的,而不是在生产应用程序中使用这个结构。

因此,我假设对于生产应用程序,您已经知道泛型/强类型数据结构(例如List<T>)比其非类型化数据结构(例如ArrayList)具有更好的性能。


2
ArrayList 不是任何现代 C# 程序中应该使用的东西。List<T> 是它的替代品,在基本上所有情况下都应该被优先考虑。 - Kyle
@Kyle 是的,但他的问题不是关于强类型和弱类型数据结构之间的区别。他的问题非常清楚,是关于C#中类似于JavaScript中Array对象上的push方法的类似方法。 - Water Cooler v2
这句话“C#将堆对象分类为不会进入堆,只会留在函数参数栈上的对象。C#通过值类型和引用类型的名称来区分它们。”是非常错误的。堆中包含许多值类型的实例。 - Ben Voigt
@BenVoigt,您的意思是堆(heap)可能包含除那些包含在引用类型中的值类型之外的其他值类型?您能否举几个例子? - Water Cooler v2
@WaterCoolerv2:存在“包含在引用类型中的那些元素”正是为什么你声称它们只存在于函数参数栈上是错误的。至少有三种“包含在引用类型中的元素”:装箱、成员变量(字段)和数组元素。 - Ben Voigt

2

这种方法可以用于数组赋值,但如果你想要添加元素,我很确定这是不可能的。相反,可以通过使用栈、队列或其他数据结构来实现。 真正的数组没有这样的功能,但派生类(如ArrayList)有。


1
根据评论中的说法“这不是将其推送到数组中,而只是将其分配给它”。
如果您正在寻找将值分配给数组的最佳实践,则唯一的方法是分配值。
Array[index]= value;

当您不想使用List时,只有一种方式可以赋值。


正如所述,由于我们还没有涵盖列表(List)的相关知识,因此列表无法使用。我会在本学期晚些时候进行介绍,但现在我想了解如何向他们展示数组管理的基本用法。 - phunder
@phunder 数组管理没问题,但你将无法向数组中添加新值。 - aloisdg
@phunder。推送何时成为基本数组管理的一部分? - jaket
@jaket 嗯,这就是我需要知道的。在Java和其他语言中,它是存在的。如果它不是C#的一部分,我只需要知道我的方法是否是一个适合新手的可行方法,直到我们在课程大纲中涉及列表。 - phunder
@phunder。据我了解,在Java中,push是ArrayList的一部分,而不是Array。在C#中,ArrayList具有等效的Insert函数。 - jaket

1
我认为除了将值分配给该数组的特定索引之外,没有其他方法。

0

这可以用几种方法来完成。

首先,是将其转换为列表,然后再转换为数组

List<int> tmpList = intArry.ToList();
tmpList.Add(anyInt);
intArry = tmpList.ToArray();

现在这种方法并不推荐,因为你需要将其转换为列表,然后再转回数组。如果你不想使用列表,可以使用第二种方式,即直接将值赋给数组

int[] terms = new int[400];
for (int runs = 0; runs < 400; runs++)
{
    terms[runs] = value;
}

这是直接的方法,如果您不想与列表和转换纠缠,那么这是推荐的方法。


谢谢您的建议。正如所述,我目前正在尽量避免使用列表。不过您的第二个建议似乎是将相同的值添加到每个空间中?不确定它有什么用处? - phunder
我为您列举了两种常见的方法,这是给您的建议,因为您现在正在教授新手,我认为过多地灌输信息并不明智。 - Barr J
3
如所述,我目前正在尝试避免使用列表。@phunder 的缺点是你在教学生们解决问题的错误方式。在C#中,解决此问题的标准方法是使用List。唯一可能教这种方法的原因是为了引出List的实用性。“好的学生们,刚才很难对吧?如果有一种类型更适合解决这个问题会怎么样?事实证明确实有这样的类型!今天我们就来学习一下List。” - mjwills

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