Лучший способ реализовать сбор массива разных типов

Я ищу полууниверсальную структуру данных на С# для хранения массивов различных целых и плавающих типов. В некоторых случаях целые числа представляют собой битовые поля, где каждый бит одинаково важен, и потеря точности недопустима. Я нахожу это сложным и запутанным из-за системы типов С# и отсутствия свободного владения языком С#.

Проект: периодические пакеты Ethercat прибывают, преобразуются в структуру (пакет) и накапливаются как Packet[] в ходе эксперимента. Каждое поле пакета из Packet[] преобразуется в массив.

Я считаю, что ищу способ «обернуть» эти массивы в один тип, чтобы они могли быть частью коллекции. Их обертывание имеет некоторые другие преимущества (именование, аппаратные коэффициенты масштабирования SI и т. Д.), Чтобы облегчить отделение аппаратного обеспечения от более поздней реализации.

Моя лучшая «обертка» называется «DataWrapper» (упрощенно ниже), но с ней я пошел на неудобные компромиссы в хранении, потере точности, использовании объекта и количестве кода.

Есть ли «лучший» способ в С#? Мой золотой стандарт — очевидно тривиальная реализация без очевидных компромиссов в Python с использованием списка списков или numpy.arrays.

Можно ли использовать «объект»? Как? Можно ли упаковать весь массив или каждый элемент массива должен быть упакован отдельно (неэффективно)?

Я видел список нескольких типов данных? однако это похоже на много кода и передовых методов программирования для того, что по сути является списком списка.

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));  
    }
}

person pathfinder    schedule 30.09.2016    source источник
comment
Почему бы просто не передать Packet[] вместо того, чтобы проецировать его значения и передавать их?   -  person Travis J    schedule 30.09.2016
comment
Я бы посмотрел на List<T>, где T — это класс, который имеет int, double и float, а также поле типа, указывающее, какой из них используется. Обычно у меня было просто object и поле типа, но вы упомянули накладные расходы на распаковку. Это проблема, которую вы никогда не сможете решить очень чисто на языке со строгой типизацией, таком как C#. Самое чистое, что вы можете получить для его потребления, это тип шаблона посетителя для итерации по коллекции. Тогда вы могли бы, по крайней мере, избежать переключения в поле типа. ПОМОЩЬ.   -  person 15ee8f99-57ff-4f92-890c-b56153    schedule 30.09.2016
comment
Я рассматривал оболочку, которая имеет различную поддержку для каждого типа - имел конструкторы для каждого типа и т. д. В некоторых отношениях лучше, но много кода и результат не очень пригоден для использования. Казалось, должен быть лучший способ. Я был бы в порядке с боксом, если бы на каждый массив приходилось по одному блоку, а не по одному блоку на элемент.   -  person pathfinder    schedule 30.09.2016
comment
В моем вопросе не упоминается тот факт, что Packet является лишь примером. Существует множество различных структур Packet - особенно, поскольку программа C # настраивает оборудование для создания Packet. Основная цель — указать структуру пакета один раз в коде. В прошлом я создам таблицу, определяющую пакет и физические характеристики данных (например, коэффициенты масштабирования, имена и т. д.). Описание используется для программирования оборудования, а также для декодирования буфера. Пока в С# кажется, что мне нужно сделать несколько копий описания - это один из способов возникновения ошибок.   -  person pathfinder    schedule 02.10.2016
comment
Единственный выход - потерять информацию о типе. Теперь я понимаю, почему мой коллега преобразовал пакеты в текстовую строку! Я, вероятно, просто изменю свою оболочку выше, чтобы преобразовать в long и либо вернуть long[] или double[] (при запросе в единицах SI).   -  person pathfinder    schedule 02.10.2016
comment
Я переформулировал этот вопрос, написав программу на Python, которая делает то, что я не могу сделать на С# ссылка   -  person pathfinder    schedule 03.10.2016


Ответы (5)


Вы можете рассматривать это как список 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);
person PhillipH    schedule 30.09.2016
comment
Как узнать, когда преобразовать элемент в float, а когда int? Хотя я думаю, что у OP такая же проблема .. - person Quantic; 30.09.2016
comment
Превратите List‹byte[]› в List‹Tuple‹Type, Byte[]››, чтобы элемент в списке имел встроенный в него собственный Type. - person PhillipH; 01.10.2016

Можете ли вы просто сериализовать данные в JSON или MessagePack и сохранить их в виде массива строк? Кажется, что это было бы относительно просто реализовать и легко использовать.

person abraganza    schedule 30.09.2016
comment
Зачем обращаться к внешней библиотеке с большим количеством настроек/конфигураций, чем необходимо? BitConverter существует именно для этой цели и является частью .Net framework. - person IAbstract; 30.09.2016

floats будут проблематичными, когда требуется устойчивая точность в C # ... период. Это печально, потому что мы все знаем, как сильно мы любим точность. Но я не думаю, что C# — единственный язык, страдающий от этой болезни. Тем не менее, я думаю, что есть способ добиться того, чего вы хотите, и ваша оболочка — хорошее начало.

Я не знаком с тем, что вы используете (сторонняя библиотека), поэтому я просто предложу решение вопроса.

Если вы знаете, какой тип вы извлекаете, я бы рекомендовал использовать 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;
}
person IAbstract    schedule 30.09.2016
comment
Я использовал double, так как это дает мне 53 бита точных целых чисел. Это работает для всех объектов с битовыми флагами, о которых я знаю. Страдает только long (time_uS) - что пока нормально. Тем не менее, ваш подход интересен - особенно, поскольку я фактически начинаю с byte[][], прежде чем он станет Packet, а затем Packet[]. - person pathfinder; 30.09.2016

В качестве контрпримера к подходу с общим списком я хотел бы упомянуть, что пример списка, связанный с вопросом, не следует считать продвинутым. Он использует интерфейс, который является тривиальным 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, исправляющей меня от глупых ошибок, поэтому, если это не скомпилируется, пожалуйста, потерпите меня. Я думаю, что общая идея ясна.

person bastijn    schedule 30.09.2016
comment
bastijn - спасибо за подробный ответ. Я нашел решение, которое лучше для меня, и это List‹dynamic› - person pathfinder; 06.10.2016
comment
Спасибо, что нашли время вернуться и поделиться своим ответом. Я рассматривал динамику, но никогда не был большим поклонником, поскольку я работаю в больших командах, и отключение проверок времени компиляции (динамика) может легко привести к сбоям во время выполнения, если они не проверены/не учтены должным образом. Я всегда находил его отличным для небольших решений или быстрого кода, так как он работает. Но как только его нужно масштабировать до производства и должно быть около нескольких лет, я бы предпочел решение, которое дает сбой в моей системе сборки при неправильном использовании. Отлично, что вы нашли свой ответ. Вы, должно быть, многому научились! :) - person bastijn; 06.10.2016
comment
Так что я все еще на пути к лучшему! Самая большая проблема, которую я вижу, заключается в том, что я не могу ограничить тип объектов (во время компиляции), добавляемых в массивы базовых числовых типов. Пока это ограничение соблюдается, я считаю, что foreach, length, indexing и select будут работать. - person pathfinder; 07.10.2016

Ответ на вопрос "Есть ли лучший способ в C#?" - Да.

Используйте List<dynamic> в качестве коллекции для ваших массивов.

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

Конечно, в него можно передать что угодно, но это можно проверить.

List<dynamic> в этой ситуации лучше, чем 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, вероятно, является более правильным техническим объяснением.

person pathfinder    schedule 06.10.2016