【问题标题】:A better way to implement collection of array of different types实现不同类型数组集合的更好方法
【发布时间】:2017-02-09 10:37:41
【问题描述】:

我正在寻找 C# 中的半通用数据结构来存储不同整数和浮点类型的数组。在某些情况下,整数是位字段,其中每个位都同样重要,并且不能容忍精度损失。由于 C# 类型系统和我缺乏 C# 流畅性,我发现这很困难和混乱。

项目:Ethercat 周期性数据包到达并转换为结构(数据包)并在实验过程中累积为Packet[]。来自Packet[] 的Packet 的每个字段都被转换为一个数组。

我相信我正在寻找一种将这些数组“包装”成单一类型的方法,以便它们可以成为集合的一部分。包装它们还有其他一些优点(命名、硬件到 SI 比例因子等),以方便将硬件与后面的实现分离。

我最好的“包装器”被称为“DataWrapper”(下面简化了),但我在存储、精度损失、对象使用和代码数量方面做出了令人不安的妥协。

C# 中是否有“更好”的方法?我的黄金标准是在 Python 中使用列表或 numpy.arrays 的明显微不足道的实现,没有明显的妥协。

可以使用“对象”吗?如何?是否可以将整个数组装箱或必须将每个数组元素单独装箱(效率低下)?

我见过A list of multiple data types? 但是,对于本质上是列表的内容,似乎有很多代码和高级编程技术。

public class DataWrapper
{
    private double[] double_array;  // backing with double, but it could if I don't use float 
    private string name;
    private double scale_factor_to_SI;

    public DataWrapper(string name, double scale_factor, dynamic dynamic_array)
    {

        this.name = name;
        this.scale_factor_to_SI = scale_factor;
        this.double_array = new double[dynamic_array.Length];

        for (int cnt = 0; cnt < dynamic_array.Length; cnt++)
        {
            this.double_array[cnt] = (double)dynamic_array[cnt];
        }
    }

    public void Get(out int[] i_array)
    {
        i_array = this.double_array.Select(item => (int)item).ToArray();
    }

    public void Get(out long[] i_array)
    {
        i_array = this.double_array.Select(item => (long)item).ToArray();
    }

    public double[] GetSI()
    {
        return this.double_array.Select(item => this.scale_factor_to_SI * (double)item).ToArray();
    }
}

public struct Packet  // this is an example packet - the actual packet is much larger and will change over time.  I wish to make the change in 1 place not many.
{
    public long time_uS;
    public Int16 velocity;
    public UInt32 status_word;
};

public class example
{
    public Packet[] GetArrayofPacketFromHardware()
    {
        return null;
    }

    public example() {
        Packet[] array_of_packet = GetArrayofPacketFromHardware();

        var time_uS = array_of_packet.Select(p => p.time_uS).ToArray();
        var velocity = array_of_packet.Select(p => p.velocity).ToArray();
        var status_bits = array_of_packet.Select(p => p.status_word).ToArray();

        List<DataWrapper> collection = new List<DataWrapper> { };
        collection.Add(new DataWrapper("time", 1.0e-6, time_uS));
        collection.Add(new DataWrapper("velocity", 1/8192, velocity));
        collection.Add(new DataWrapper("status", 1, status_bits));  
    }
}

【问题讨论】:

  • 为什么不直接传递 Packet[] 而不是将其值投射出来并传递它们?
  • 我会查看List&lt;T&gt;,其中T 是一个类,它有一个int、一个double 和一个float,还有一个type 字段说明使用了哪一个。我通常只有一个object 和一个类型字段,但你提到了拆箱开销。这是一个你永远不会用像 C# 这样的强类型语言非常干净地解决的问题。使用它可能得到的最干净的东西是访问者模式类型的东西,用于遍历集合。然后,您至少可以避免在类型字段上进行切换。 FWIW。
  • 我考虑过一个包装器,而不是为每种类型提供不同的支持——每种类型都有构造函数,等等。在某些方面更好,但是很多代码和输出不是很有用。似乎必须有更好的方法。如果每个数组有一个盒子,我可以装箱 - 而不是每个元素一个盒子。
  • 我的问题中没有提到的是 Packet 只是一个例子。 Packet 有许多不同的结构 - 特别是因为 C# 程序配置硬件来生成 Packet。基本目标是在代码中一次性指定数据包的结构。在过去,我将创建一个表来定义数据包和物理特性(例如比例因子、名称等)。该描述用于对硬件进行编程,也用于对缓冲区进行解码。到目前为止,在 C# 中,我似乎必须制作描述的多个副本 - 这是出现错误的一种方式。
  • 唯一的出路似乎是丢失类型信息。我现在明白为什么我的同事将数据包转换为文本字符串了!我可能只是将上面的包装器更改为转换为 long 并返回 long[] 或 double[](当请求缩放为 SI 单位时。)

标签: c# data-structures collections type-systems typed-arrays


【解决方案1】:

您可以将其视为 byte[] 列表并使用 BitConverter 序列化值类型以将值类型转换为 byte[],然后使用反向调用展开它;

List<byte[]> dataList = new List<byte[]>();
float v = 1.0424f;
byte[] converted = BitConverter.GetBytes(v);
// put converted into a List<byte[]> 
dataList.Add(converted);
// Convert it back again
float z= BitConverter.ToSingle(dataList[0], 0);

【讨论】:

  • 您如何知道何时将元素转换为 floatint?虽然我猜 OP 也有同样的问题..
  • 将 List 变成 List> 以便列表中的项目嵌入自己的 Type。
【解决方案2】:

floats 在 C# ... 期间需要持续精度时会出现问题。这是不幸的,因为我们都知道我们多么喜欢精确。但我不认为 C# 是唯一患有这种疾病的语言。也就是说,我认为有一种方法可以实现您想要的,并且您的包装器是一个好的开始。

我不熟悉您正在使用什么(第 3 方库),所以我会坚持提供问题的解决方案。

如果您知道要检索的类型是什么,我建议您使用byte[]。这样您就可以有效地将 3 个byte[] 存储在一个列表中。

var dataList = new List<byte[]>();
dataList.Add(ConvertUsTime(p.time_US));
dataList.Add(ConvertVelocity(p.velocity));
dataList.Add(ConvertStatus(p.status));

byte[] ConvertToArray(long usTime) {
    return BitConverter.GetBytes(usTime);
}

byte[] ConvertVelocity(Int16 velocity) {
    return BitConverter.GetBytes(velocity);
}

byte[] ConvertStatus(UInt32 status) {
    return BitConverter.GetBytes(status);
}

...对于更通用的方法:

byte[] ConvertValue<T>(T value) where T : struct {
    // we have to test for type of T and can use the TypeCode for switch statement
    var typeCode = Type.GetTypeCode(typeof(T));

    switch(typeCode) {
        case TypeCode.Int64:
            return BitConverter.GetBytes((long)value);

        case TypeCode.Int16:
            return BitConverter.GetBytes((Int16)value);

        case TypeCode.UInt32:
            return BitConverter.GetBytes((UInt32)value);
    }

    return null;
}

【讨论】:

  • 我使用了 double 因为它可以得到 53 位的精确整数。这适用于我知道的所有位标志对象。只有 long (time_uS) 受到影响 - 目前还好。但是,您的方法很有趣-尤其是因为我实际上是从 byte[][] 开始,然后再变成 Packet,然后是 Packet[]。
【解决方案3】:

您可以简单地将数据序列化为 JSON 或 MessagePack 并将其存储为字符串数组吗?看起来这样实现起来相对简单且易于使用。

【讨论】:

  • 为什么要使用比必要更多的设置/配置去外部库? BitConverter 就是为此目的而存在的,它是 .Net 框架的一部分。
【解决方案4】:

作为通用列表方法的反例,我想提一下,问题中链接的列表示例不应被视为高级。它使用一个简单的 C# 接口。

当您希望调试列表的内容或希望不同类型集合上的业务逻辑增长时,使用实现相同接口的不同类型可能是一个更好的解决方案。现在你只有 GetSI() 但它可能会随着更通用的方法而增长,这些方法对每种数据包集合类型都有特定的实现。最后,您的 IDE 调试器可能不太支持包含通用对象或原始字节的调试列表。接口得到很好的支持。下面显示了一个用于说明该想法的实现。

public example() {

    Packet[] array_of_packet = GetArrayofPacketFromHardware();

    var time_uS = array_of_packet.Select(p => p.time_uS).ToArray();
    var velocity = array_of_packet.Select(p => p.velocity).ToArray();
    var status_bits = array_of_packet.Select(p => p.status_word).ToArray();

    List<IPacketCollection> collection = new List<IPacketCollection> { };
    collection.Add(new TimePacketCollection(time_uS));
    collection.Add(new VelocityPacketCollection(velocity));
    collection.Add(new StatusPacketCollection(status_bits));  

    // Now we have benefits over generic objects or byte arrays.
    // We can extend our collections with additional logic as your 
    // Application grows or right now already still benefit from the 
    // GetSI you mentioned as a plus in your question.
    foreach(var velocityPacketCollection in collection.OfType<VelocityPacketCollection>()) {
        // specific velocity collection things here.
        // Also your debugger is perfectly happy peeking in the collection.
    }

    // or generic looping accessing the GetSI()
    foreach(var packetCollection in collection) {
        System.Debug.Println(packetCollection.GetSI());
    }
}

public interface IPacketCollection {
    /// <summary>
    /// Not sure what this method should do but it seems it 
    /// always returns double precision or something?
    /// </summary>
    public double[] GetSI;
} 

public class TimePacketCollection : IPacketCollection {
    private const double scaleFactor = 1.0e-6;
    private long[] timePacketArray;

    public TimePacketCollection(long[] timeArray) {
        timePacketArray = timeArray;
    }

    public double[] GetSI(){
         // no IDE available. Not sure if this automatically converts to 
         // double due to multiplication with a double.
         return timePacketArray.Select(item => scaleFactorToSI * item).ToArray();
    }
}

public class VelocityPacketCollection : IPacketCollection {
    private const double scaleFactor = 1/8192;
    private Int16[] velocityPacketArray;

    public VelocityPacketCollection (Int16[] velocities) {
        velocityPacketArray = velocities;
    }

    public double[] GetSI(){
         // no IDE available. Not sure if this automatically converts to 
         // double due to multiplication with a double.
         return velocityPacketArray.Select(item => scaleFactorToSI * item).ToArray();
    }
}

public class StatusPacketCollection : IPacketCollection {
    private const double scaleFactor = 1.0;
    private UInt32[] statusPacketArray;

    public StatusPacketCollection (UInt32[] statuses) {
        statusPacketArray = statuses;
    }

    public double[] GetSI(){
         // no IDE available. Not sure if this automatically converts to 
         // double due to multiplication with a double.
         return statusPacketArray.Select(item => scaleFactorToSI * item).ToArray();
    }
}

免责声明:我是在没有 IDE 的设备上编写的。如果没有我的 IDE 纠正我的愚蠢错误,我编写代码绝对是灾难性的,所以如果这不能编译,请多多包涵。我认为总体思路很明确。

【讨论】:

  • bastijn - 感谢您的详细回答。我找到了对我更好的解决方案,那就是 List
  • 感谢您抽出宝贵时间回来分享您的答案。我考虑过动态,但由于我在较大的团队中工作,所以我从来不是一个忠实的粉丝,如果没有正确检查/考虑,禁用编译时检查(动态)很容易导致运行时崩溃。我总是发现它非常适合小型解决方案或快速代码,因为它可以工作。但是,一旦它必须扩展到生产并且必须持续多年,我更喜欢一个在未正确使用时会在我的构建系统中崩溃的解决方案。太好了,您找到了答案。你一定学到了很多! :)
  • 所以我还在寻找更好的方法!我看到的最大问题是我无法限制添加到基本数字类型数组的对象类型(在编译时)。只要满足该约束,我相信 foreach、length、indexing 和 select 将起作用。
【解决方案5】:

“C# 中有更好的方法吗?”的答案- 是的。

使用List&lt;dynamic&gt; 作为数组的集合。

List<dynamic> array_list = new List<dynamic> { };
public void AddArray(dynamic dynamic_array)
{
   this.array_list.Add(dynamic_array);
}

当然可以将任何东西传递给它 - 但可以对其进行测试。

List&lt;dynamic&gt; 在这种情况下比ArrayList 更好,因为当尝试索引从“列表”中获取的数组时,IDE 会标记错误。

int ndx = 0;
foreach (var array_from_list in this.array_list) {                
  var v = array_from_list[ndx];  // error if array_from_list is ArrayList
}

下面是一个完整的插图(但它只是在概念上复制了我上面的包装)。

using System;
using System.Collections.Generic;


namespace Application
{
    class MyTest
    {
        List<dynamic> array_list = new List<dynamic> { };
        int length;

        public void AddArray(dynamic dynamic_array)
        {
            this.array_list.Add(dynamic_array);
            this.length = dynamic_array.Length;
        }
        public dynamic GetVector(int ndx)
        {
            return array_list[ndx];
        }
        public void Display()
        {
            for (int ndx = 0; ndx < this.length; ndx++)
            {
                string ln_txt = "";
                foreach (var array_from_list in this.array_list)
                {
                    string s = array_from_list[ndx].ToString();
                    ln_txt += $"{s} ";
                }

                Console.WriteLine(ln_txt);
            }

        }
    }

    static class Program
    {


        [STAThread]
        static void Main(string[] args)
        {

            MyTest test = new MyTest();
            test.AddArray(new long[] { 10, 20, 30, 40 });
            test.AddArray(new int[] { 1, 2, 3, 4 });
            test.AddArray(new double[] { .1, .2, .3, .4 });
            test.AddArray(new string[] { "a", "b", "c", "d" });
            test.Display();


            for (int vecnum = 0; vecnum < 4; vecnum++)
            {
                var vector = test.GetVector(vecnum);
                Console.Write($"vnum:{vecnum} :   ");

                foreach (var value in vector)
                {
                    Console.Write($"{value}   ");
                }
                Console.WriteLine("");
            }

        }
    }
}

我后来了解到https://stackoverflow.com/a/10380448/4462371 可能是更正确的技术解释。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-09-04
    • 1970-01-01
    • 2023-04-05
    • 2017-01-15
    • 1970-01-01
    • 1970-01-01
    • 2021-10-26
    相关资源
    最近更新 更多