使用 LINQ 在字节数组中搜索以特定字节开始/停止的所有子数组
Posted
技术标签:
【中文标题】使用 LINQ 在字节数组中搜索以特定字节开始/停止的所有子数组【英文标题】:Using LINQ to search a byte array for all subarrays that start/stop with certain byte 【发布时间】:2011-06-04 18:02:01 【问题描述】:我正在处理一个 COM 端口应用程序,我们有一个已定义的可变长度数据包结构,我正在与一个微控制器通信。数据包具有开始和停止字节的分隔符。问题是有时读取缓冲区可能包含无关字符。似乎我总是会得到整个数据包,只是在实际数据之前/之后有一些额外的喋喋不休。所以我有一个缓冲区,每当从 COM 端口接收到新数据时,我都会将数据附加到该缓冲区。搜索此缓冲区以查找我的数据包的任何可能出现的最佳方法是什么?例如:
假设我的数据包分隔符是0xFF
,我有一个这样的数组
0x00, 0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF, 0x04
如何创建一个函数/LINQ 语句来返回所有以分隔符开头和结尾的子数组(几乎就像带有通配符的滑动相关器)?
示例将返回以下 3 个数组:
0xFF, 0x02, 0xDA, 0xFF, 0xFF, 0x55, 0xFF, and
0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF
【问题讨论】:
我原以为返回的数组是 0x00 0x02, 0xDA 和 0x55,但除此之外,如果这表示近似的实际数组大小,并且分隔符是只有一个字节长,为什么不做一个简单的循环呢?它可能会胜过 linq。 @Willem:是的,同意所有观点。 @Willem van Rumpt - 我用 LINQ 措辞了问题标题,因为这通常会在 SO 上获得成功,但在问题中我指定了“函数/LINQ 语句”,因为我知道非常真实的可能性一个优雅的 LINQ 解决方案很可能需要比循环更长的时间。我对任何一个都持开放态度,我只想让一年内落后于我的人知道除了代码中的“//解码数据包”之外我做了什么。 啊哈。我错过了“函数/LINQ 语句”短语。威廉,乔尔领先我们一步。 @yodaj & Joel:我也错过了:) 【参考方案1】:虽然 Trystan 的回答在技术上是正确的,但他同时制作了许多原始数组的副本。如果起始数组很大并且有一堆分隔符,那么它很快就会变得很大。这种方法通过仅使用原始数组和正在评估的当前段的数组来避免大量内存消耗。
public static List<ArraySegment<byte>> GetSubArrays(this byte[] array, byte delimeter)
if (array == null) throw new ArgumentNullException("array");
List<ArraySegment<byte>> retval = new List<ArraySegment<byte>>();
for (int i = 0; i < array.Length; i++)
if (array[i] == delimeter)
for (int j = i + 1; j < array.Length; j++)
if (array[j] == delimeter)
retval.Add(new ArraySegment<byte>(array, i + 1, j - i - 1));
return retval;
可以这样使用:
static void Main(string[] args)
byte[] arr = new byte[] 0x00, 0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF, 0x04 ;
List<ArraySegment<byte>> retval = GetSubArrays(arr, 0xFF);
// this also works (looks like LINQ):
//List<ArraySegment<byte>> retval = arr.GetSubArrays(0xFF);
byte[] buffer = new byte[retval.Select(x => x.Count).Max()];
foreach (var x in retval)
Buffer.BlockCopy(x.Array, x.Offset, buffer, 0, x.Count);
Console.WriteLine(String.Join(", ", buffer.Take(x.Count).Select(b => b.ToString("X2")).ToArray()));
Console.ReadLine();
【讨论】:
很好地使用了鲜为人知的 ArraySegment。 我认为如果数组以分隔符结尾,if (array[j] == delimeter)
会导致越界错误。
用 0x00, 0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF, 0x04, 0xFF 测试。我没有出现越界错误。你的意见是什么?
+1 - 我昨天在 MSDN 上查看了 ArraySegment 以解决这个确切的问题,并且对我以前从未见过有人使用它感到震惊。我想知道为什么没有人使用它?似乎有点帮助。
很惊讶没有人提到它,但这个答案出现在 DotNetRocks 播客的“Get To Know .Net”部分。 #814.【参考方案2】:
这是使用 LINQ 执行此操作的方法...
int[] list = new int[] 0x00, 0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF, 0x04 ;
int MAXLENGTH = 10;
var windows = list.Select((element, i) => list.Skip(i).Take(MAXLENGTH));
var matched = windows.Where(w => w.First() == 0xFF);
var allcombinations = matched.SelectMany(m => Enumerable.Range(1, m.Count())
.Select(i => m.Take(i)).Where(x => x.Count() > 2 && x.Last() == 0xFF));
或者使用索引:
int length = list.Count();
var indexes = Enumerable.Range(0, length)
.SelectMany(i => Enumerable.Range(3, Math.Min(length-i, MAXLENGTH))
.Select(count => new i, count));
var results = indexes.Select(index => list.Skip(index.i).Take(index.count))
.Where(x => x.First() == 0xFF && x.Last() == 0xFF);
【讨论】:
在你的两个变体中,第一个(不使用索引)在我的测试中表现得更快【参考方案3】:如果你真的想使用 LINQ,这应该会很快工作(即使不如旧的 for 循环那么快):
public static IEnumerable<T[]> GetPackets<T>(this IList<T> buffer, T delimiter)
// gets delimiters' indexes
var delimiterIdxs = Enumerable.Range(0, buffer.Count())
.Where(i => buffer[i].Equals(delimiter))
.ToArray();
// creates a list of delimiters' indexes pair (startIdx,endIdx)
var dlmtrIndexesPairs = delimiterIdxs.Take(delimiterIdxs.Count() - 1)
.SelectMany(
(startIdx, idx) =>
delimiterIdxs.Skip(idx + 1)
.Select(endIdx => new startIdx, endIdx )
);
// creates array of packets
var packets = dlmtrIndexesPairs.Select(p => buffer.Skip(p.startIdx)
.Take(p.endIdx - p.startIdx + 1)
.ToArray())
.ToArray();
return packets;
【讨论】:
【参考方案4】:我不会尝试使用 linq 执行此操作,所以这里有一个常规方法,它返回与您想要的相同的输出。
public List<byte[]> GetSubArrays(byte[] array, byte delimeter)
if (array == null) throw new ArgumentNullException("array");
List<byte[]> subArrays = new List<byte[]>();
for (int i = 0; i < array.Length; i++)
if (array[i] == delimeter && i != array.Length - 1)
List<byte> subList = new List<byte>() delimeter ;
for (int j = i+1; j < array.Length; j++)
subList.Add(array[j]);
if (array[j] == delimeter)
subArrays.Add(subList.ToArray());
return subArrays;
如果它必须是就地 lambda 表达式,那么只需将第一行更改为 (byte[] array, byte delimeter) =>
(不带方法修饰符和名称)并以这种方式调用它。
【讨论】:
这个问题是如果原始数组很大并且有很多分隔符,内存消耗会变得非常大。请参阅我的答案,它使用了扩展方法。【参考方案5】:虽然定界符结构似乎有点模糊,但我不会使用 linq 并执行以下操作(未进行广泛的测试)。它将返回所有子集(由分隔符包围的字节),而不包括分隔符(无论如何它是给定的,为什么要包含它?)。它也不返回结果的并集,但始终可以手动组装。
public IEnumerable<byte[]> GetArrays(byte[] data, byte delimiter)
List<byte[]> arrays = new List<byte[]>();
int start = 0;
while (start >= 0 && (start = Array.IndexOf<byte>(data, delimiter, start)) >= 0)
start++;
if (start >= data.Length - 1)
break;
int end = Array.IndexOf<byte>(data, delimiter, start);
if (end < 0)
break;
byte[] sub = new byte[end - start];
Array.Copy(data, start, sub, 0, end - start);
arrays.Add(sub);
start = end;
return arrays;
【讨论】:
这不会返回预期的结果集,其中可以包含嵌套段(问题中的第三个结果)。 @yodaj:啊啊啊啊!我认为第三个结果是每个结果的联合。我想知道他为什么要全部归还 :) 这就是为什么我写了“它也不返回结果的并集,但总是可以手动组装。” 是的。可以使用递归从您的结果中轻松组装联合。【参考方案6】:您可以使用 Linq 聚合器执行此操作,但它比此处建议的其他解决方案要简单得多,还必须添加一个特殊情况来涵盖扩展已完成的数组,如您上面建议的那样。
byte[] myArray = new byte[] 0x00, 0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF, 0x04 ;
var arrayList = myArray.Aggregate(
new completedLists = new List<List<byte>>(),
activeList = new List<byte>() ,
(seed, s) =>
if (s == 0xFF)
if (seed.activeList.Count == 0)
seed.activeList.Add(s);
else
seed.activeList.Add(s);
var combinedLists = new List<List<byte>>();
foreach (var l in seed.completedLists)
var combinedList = new List<byte>(l);
combinedList.AddRange(seed.activeList.Skip(1));
combinedLists.Add(combinedList);
seed.completedLists.AddRange(combinedLists);
seed.completedLists.Add(new List<byte>(seed.activeList));
seed.activeList.Clear();
seed.activeList.Add(s);
else
if (seed.activeList.Count > 0)
seed.activeList.Add(s);
return seed;
).completedLists;
【讨论】:
在这一行不编译:seed.completedLists.Add(new List以上是关于使用 LINQ 在字节数组中搜索以特定字节开始/停止的所有子数组的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 LINQ to Entities 获取字节数组长度?