如何巧妙地从IEnumerable<T>创建匿名类型?

4
我希望使用LINQ来解决以下问题,我有如下集合:
List<byte> byteList = new List<byte() { 0x01, 0x00, 0x01, 0x02, 0x01, 0x00, 0x3, 0x4, 0x02 };

这个例子中的数据遵循以下模式:
byteList[0] = 地址 (1, 2, 3, ... n)
byteList[1] = 旧状态,基本上代表一个枚举
byteList[2] = 新状态,与上面相同
我正在与一个嵌入式设备进行接口交互,这是我查看输入变化的方式。
为了清理代码并使其更易于维护程序员跟踪我的逻辑,我想将一些涉及的细节抽象出来,并将每个三字节数据集提取到匿名类型中,在函数内部使用它们进行一些额外处理。我已经写了一个快速实现,但我相信它可以大大简化。我试图清理代码,而不是让事情变得更加混乱!肯定有更简单的方法来执行以下操作:
List<byte> byteList = new List<byte>()
{
    0x01, 0x09, 0x01, 0x02, 0x08, 0x02, 0x03, 0x07, 0x03
};
var addresses = byteList
    .Where((b, i) => i % 3 == 0)
    .ToList();
var oldValues = byteList
    .Where((b, i) => i % 3 == 1)
    .ToList();
var newValues = byteList
    .Where((b, i) => i % 3 == 2)
    .ToList();

var completeObjects = addresses
    .Select((address, index) => new 
    { 
        Address = address,
        OldValue = oldValues[index],
        NewValue = newValues[index]
    })
    .ToList();
foreach (var anonType in completeObjects)
{
    Console.WriteLine("Address: {0}\nOld Value: {1}\nNew Value: {2}\n",
        anonType.Address, anonType.OldValue, anonType.NewValue);
}

对于非负整数i,(i + 2) % 3 == 0 等价于 i % 3 == 1 - phoog
我不确定LINQ在这里能提供多少帮助。MoreLINQ包括“Batch()”方法,这是将可枚举对象分成指定大小的桶的好方法。另外,一个简单的“for”循环可能会非常容易和清晰。 - dlev
6个回答

5
您可以使用Enumerable.Range和一些数学知识:
List<byte> byteList = new List<byte>()
{
    0x01, 0x09, 0x01, 0x02, 0x08, 0x02, 0x03, 0x07, 0x03
};
var completeObjects = Enumerable.Range(0, byteList.Count / 3).Select(index =>
    new
    {
        Address = byteList[index * 3],
        OldValue = byteList[index * 3 + 1],
        NewValue = byteList[index * 3 + 2],
    });

如果字节数不是3的倍数,则多余的一个或两个字节将被忽略。

这似乎是使用LINQ解决我的问题最干净的方法。我知道LINQ可能不是解决这个问题的最佳方式,但问题确实指定了要使用LINQ。这个问题旨在作为学习工具,我的生产代码可能会使用phoog解决方案的变体。 - Nate

3

为了简化操作,我会创建一个记录类型并使用 for 循环:

class RecordType
{
    //constructor to set the properties omitted
    public byte Address { get; private set; }
    public byte OldValue { get; private set; }
    public byte NewValue { get; private set; }
}

IEnumerable<RecordType> Transform(List<byte> bytes)
{
    //validation that bytes.Count is divisible by 3 omitted

    for (int index = 0; index < bytes.Count; index += 3)
        yield return new RecordType(bytes[index], bytes[index + 1], bytes[index + 2]);
}

如果您确实需要匿名类型,而不想使用 LINQ,则可以这样做:

for (int index = 0; index < bytes.Count; index += 3)
{
    var anon = new { Address = bytes[index], OldValue = bytes[index + 1], NewValue = bytes[index + 3] };
    //... do something with anon
}

Linq非常有用,但在这个任务中却很笨拙,因为序列项根据它们在序列中的位置具有不同的含义。


我承认这是我收到的最实用和最易于维护的答案。你的答案的一个变体可能是我选择用于我的生产代码的路线,但由于问题指定了LINQ,我需要对那些记住我的问题框架的人公平。感谢您提供如此好的答案。 - Nate
@Nate 好的,我有点失去了“我想使用LINQ…”这句话的意思,当我读完问题并开始构思我的答案时。 - phoog

0
如果你必须使用LINQ(不确定这是否是一个好计划),那么一个选项是:
using System;
using System.Collections.Generic;
using System.Linq;

static class LinqExtensions
{
    public static IEnumerable<T> EveryNth<T>(this IEnumerable<T> e, int start, int n)
    {
        int index = 0;
        foreach(T t in e)
        {
            if((index - start) % n == 0)
            {
                yield return t;
            }
            ++index;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        List<byte> byteList = new List<byte>()
        {
            0x01, 0x09, 0x01, 0x02, 0x08, 0x02, 0x03, 0x07, 0x03
        };

        var completeObjects =
            byteList.EveryNth(0, 3).Zip
            (
                byteList.EveryNth(1, 3).Zip
                (
                    byteList.EveryNth(2, 3),
                    Tuple.Create
                ),
                (f,t) => new { Address = f, OldValue = t.Item1, NewValue = t.Item2 }
            );

        foreach (var anonType in completeObjects)
        {
            Console.WriteLine("Address: {0}\nOld Value: {1}\nNew Value: {2}\n", anonType.Address, anonType.OldValue, anonType.NewValue);
        }
    }
}

0

这个怎么样?

var addresses = 
    from i in Enumerable.Range(0, byteList.Count / 3)
    let startIndex = i * 3
    select new
    {
        Address = byteList[startIndex],
        OldValue = byteList[startIndex + 1],
        NewValue = byteList[startIndex + 2]
    };

注意:我独立开发了这个答案,虽然它与Michael Liu的答案几乎相同,但我会把这个答案留在这里,因为我觉得它更漂亮。 :-)

这是一个非常出色、简洁的答案,正是我所寻找的类型。不幸的是,在使用LINQ时,我更喜欢方法语法而不是查询语法。也许我的Python经验正在展现出来,但它感觉更自然。谢谢你给了我另一种完成工作的方式! - Nate

0
这里尝试使用扩展方法 ChunkToList 来将 IEnumerable<T> 分割成一组 IList<T>
用法:
        var compObjs = byteList.ChunkToList(3)
                               .Select(arr => new { 
                                       Address  = arr[0],
                                       OldValue = arr[1],
                                       NewValue = arr[2] 
                               });

实现:

static class LinqExtensions
{
    public static IEnumerable<IList<T>> ChunkToList<T>(this IEnumerable<T> list, int size)
    {
        Debug.Assert(list.Count() % size == 0);

        int index = 0;
        while (index < list.Count())
        {
            yield return list.Skip(index).Take(size).ToList();
            index += size;
        }
    }
}

嗯,SplitIntoListsOf 应该是一个更好的名称,不是吗:byteList.SplitIntoListsOf(3).Select... - markmuetz
我非常喜欢你的解决方案适用于Enumerable<T>并且相当简洁。解决问题的方式非常有趣,感谢你向我展示了另一种完成工作的方法。 - Nate

0

我不确定这是否算作一个聪明的解决方案,但我使用了这个例子来尝试在不创建单独列表的情况下完成它。

var completeObjects = byteList
    // This is required to access the index, and use integer
    // division (to ignore any reminders) to group them into
    // sets by three bytes in each.
    .Select((value, idx) => new { group = idx / 3, value })
    .GroupBy(x => x.group, x => x.value)

    // This is just to be able to access them using indices.
    .Select(x => x.ToArray())

    // This is a superfluous comment.
    .Select(x => new {
        Address = x[0],
        OldValue = x[1],
        NewValue = x[2]
    })

    .ToList();

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