C#中的数组切片

303

你怎么做?给定一个字节数组:

byte[] foo = new byte[4096];

如何将数组的前x个字节作为单独的数组获取?(具体而言,我需要将其作为 IEnumerable<byte>

这是用于使用 Socket 时的处理。我想最简单的方法就是使用数组切片,类似于Perl语法:

@bar = @foo[0..40];

有没有什么C#的方法可以将前41个元素存储到@bar数组中? 如果我遗漏了什么内容,或者应该进行其他操作,请告诉我。

LINQ是我的选择之一(.NET 3.5),如果有用的话。


3
数组切片是针对 C# 7.2 的一个提议。详见 https://github.com/dotnet/csharplang/issues/185 。 - Mark
5
C# 8.0将引入本地数组切片。详情请参见答案 - Remy
2
你可能会对ArraySlice<T>感兴趣,它实现了使用步长切片数组的功能,并作为原始数据的视图:https://github.com/henon/SliceAndDice - henon
20个回答

248

你可以使用ArraySegment<T>。它非常轻量级,因为它不会复制数组:

string[] a = { "one", "two", "three", "four", "five" };
var segment = new ArraySegment<string>( a, 1, 2 );

5
很不幸,它不是IEnumerable类型。 - recursive
29
有人知道为什么它不是IEnumerable吗?我不知道。看起来应该是IEnumerable。 - Fantius
3
@RonKlein,我知道这是一个包装器(wrapper),它不会创建副本。你接下来的解释让我有点困惑。 - Fantius
50
从 .Net 4.5 开始,ArraySegment 是 IList 和 IEnumerable。对于旧版本的用户来说有点遗憾。 - Todd Li
8
@Zyo 我的意思是从 .Net 4.5 开始,ArraySegment<T> 实现了 IEnumerable<T> 接口,而不是说 IEnumerable<T> 接口本身是新的。 - Todd Li
显示剩余9条评论

240

数组是可枚举的,所以你的 foo 已经是一个 IEnumerable<byte> 本身。简单地使用 LINQ 序列方法,比如 Take(),从中获取你想要的内容(不要忘记在代码中加入命名空间 Linq,即 using System.Linq;):

byte[] foo = new byte[4096];

var bar = foo.Take(41);

如果你确实需要从任何的 IEnumerable<byte> 值中获取一个数组,你可以使用 ToArray() 方法。但这似乎不是本例的情况。


5
如果我们只是要将内容复制到另一个数组中,可以使用Array.Copy静态方法。不过我认为其他回答已经正确解释了意图,其实并不需要另一个数组,只需要一个能够遍历前41个字节的IEnumberable<byte>即可。 - AnthonyWJones
2
请注意,只有一维和锯齿数组是可枚举的,多维数组不是。 - Abel
15
注意,使用 Array.Copy 的性能比使用 LINQ 的 Take 或 Skip 方法要快得多。 - Michael
7
@Abel,那实际上是非常不正确的。多维数组是可枚举的,但它们像这样进行枚举:[2,3] => [1,1]、[1,2]、[1,3]、[2,1]、[2,2]、[2,3]。交错数组也是可枚举的,但在枚举时,它们返回其内部数组而不是值。就像这样:type[][] jaggedArray; foreach (type[] innerArray in jaggedArray) { } - Aidiakapi
3
@Aidiakapi说“非常不正确”? ;)。但你部分是对的,我应该写成“多维数组没有实现IEnumerable<T>”,这样我的陈述就更清晰了。另请参见:https://dev59.com/AUfRa4cB1Zd3GeqP6iEL - Abel

167
你可以使用数组的 CopyTo() 方法。或者,使用 LINQ 可以使用 Skip()Take()...
byte[] arr = {1, 2, 3, 4, 5, 6, 7, 8};
var subset = arr.Skip(2).Take(2);

1
+1 很好的想法,但是我需要将返回的数组作为另一个函数的输入,这就需要 CopyTo 使用一个临时变量。我会等待其他答案的。 - Matthew Scharley
5
我对LINQ还不够熟悉,或许这是我确实应该学习它的进一步证明。 - Matthew Scharley
15
这种方法比Array.Copy慢至少50倍。在许多情况下这不是问题,但在循环中进行数组分片时,性能下降非常明显。 - Valentin V
3
我只进行一次调用,因此性能对我来说不是问题。这对于可读性非常有帮助...谢谢。 - Rich
2
感谢 Skip()。 只有Take() 不足以获得任意片段。 此外,我无论如何都在寻找LINQ解决方案(切片IEnumerable,但我知道关于数组的结果更容易找到)。 - Tomasz Gandor

79

从C# 8.0/.Net Core 3.0开始

将支持数组切片,并且新增了IndexRange类型。

Range结构文档
Index结构文档

Index i1 = 3;  // number 3 from beginning
Index i2 = ^4; // number 4 from end
int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine($"{a[i1]}, {a[i2]}"); // "3, 6"

var slice = a[i1..i2]; // { 3, 4, 5 }

上面的代码示例摘自于C# 8.0博客

请注意,^前缀表示从数组的末尾开始计数。如在文档示例中所示。

var words = new string[]
{
                // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};              // 9 (or words.Length) ^0

RangeIndex也可以在数组切片之外使用,例如在循环中。

Range range = 1..4; 
foreach (var name in names[range])

将循环遍历1到4的条目


请注意,撰写本答案时,C# 8.0尚未正式发布
C# 8.x和.Net Core 3.x现在可在Visual Studio 2019及以上版本中使用


1
这会不会创建数组的副本? - Tim Pohlmann
4
看起来这是一份副本: https://www.codejourney.net/2019/02/csharp-8-slicing-indexes-ranges/ - Tim Pohlmann
3
令人惊讶的是,文档中都没有说明切片后得到的类型。它是另一个数组吗?还是一个ArraySegment?或者是其他什么类型呢?... - C-F
@C-F,原来这是一个新数组。 - C-F
我真希望第二个数字是包含在内的... 1..4 意味着1、2和3,而不是4。 - undefined

59
static byte[] SliceMe(byte[] source, int length)
{
    byte[] destfoo = new byte[length];
    Array.Copy(source, 0, destfoo, 0, length);
    return destfoo;
}

//

var myslice = SliceMe(sourcearray,41);

13
我认为使用Buffer.BlockCopy()更高效并且能够达到相同的结果。 - Matt Davis
@MattDavis 不,某些情况下它是稍微高效的。请看这篇关于数组和缓冲区复制的帖子:https://dev59.com/v3M_5IYBdhLWcg3waSX9。 - Péter Szilvási

28
C# 7.2中,你可以使用Span<T>。新的System.Memory系统的好处是它不需要复制数据。
你需要的方法是Slice
Span<byte> slice = foo.Slice(0, 40);

现在许多方法都支持SpanIReadOnlySpan,因此使用这个新类型将非常简单。
请注意,在撰写本文时,Span<T>类型尚未在最新版本的.NET(4.7.1)中定义,因此要使用它,您需要从NuGet安装System.Memory package

24

自2019年起,C# 8支持Ranges。这使得你可以更轻松地实现切片(类似于JS语法):

var array = new int[] { 1, 2, 3, 4, 5 };
var slice1 = array[2..^3];    // array[new Range(2, new Index(3, fromEnd: true))]
var slice2 = array[..^3];     // array[Range.EndAt(new Index(3, fromEnd: true))]
var slice3 = array[2..];      // array[Range.StartAt(2)]
var slice4 = array[..];       // array[Range.All]
你可以使用范围(ranges)来替代众所周知的LINQ函数:Skip()Take()Count()

3
变量 变量 变量。无法确定返回类型。 - IC_
1
@IC_ 是来自微软文档的代码示例(链接在答案中)。而且在等号后面清楚地看到了 int[]。所以你问题的答案是 INT,但当你在代码 IDE 中输入时,也会显示 var 的类型(悬停等)。 - Major
1
@IC_ 这是来自 MS 文档 的代码示例。原始答案中已添加链接。他们解释得很清楚,如果您有任何疑问可以向 Microsoft 投诉。在 IDE 中仍然会显示类型,所以很容易找到。但为了帮助您,它将是 int[] - Major
1
那么这实际上产生了什么?例如,array[2:3]的长度是多少?在我熟悉的大多数语言中,我会期望是2。但在Python中,我会期望是1。 - Harry
1
@Major 那个文档是我在网上搜索到的第一件事情之一,但是非常啰嗦,在阅读了一个小时后,我仍然不明白。我只想知道array[2..3]的长度是1还是2。 - Harry
显示剩余2条评论

17

这里还有一种可能性,我没有在这里看到过:Buffer.BlockCopy() 比 Array.Copy() 稍微快一些,并且还具有额外的好处,可以在转换时即时从基元数组(例如 short[]) 转换为字节数组,当你需要通过套接字传输数字数组时非常方便。


2
Buffer.BlockCopyArray.Copy() 虽然接受相同的参数,但产生了不同的结果 - 有很多空元素。为什么? - jocull
9
Array.Copy()和Buffer.BlockCopy()的参数略有不同。Array.Copy()方法中,长度和位置是按元素来确定的;而Buffer.BlockCopy()方法中,长度和位置是按字节来确定的。换句话说,如果你想要复制一个由10个整数元素组成的数组,你可以使用Array.Copy(array1, 0, array2, 0, 10);但是如果你想要使用Buffer.BlockCopy()方法,你需要使用Buffer.BlockCopy(array1, 0, array2, 0, 10 * sizeof(int)) - Ken Smith

15

如果你想要 IEnumerable<byte>,那么只需要

IEnumerable<byte> data = foo.Take(x);

13

这里有一个简单的扩展方法,可以将一个切片作为一个新数组返回:

public static T[] Slice<T>(this T[] arr, uint indexFrom, uint indexTo) {
    if (indexFrom > indexTo) {
        throw new ArgumentOutOfRangeException("indexFrom is bigger than indexTo!");
    }

    uint length = indexTo - indexFrom;
    T[] result = new T[length];
    Array.Copy(arr, indexFrom, result, 0, length);

    return result;
}

那么你可以这样使用:

byte[] slice = foo.Slice(0, 40);

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