Как хранить структуры разных типов без бокса - PullRequest
9 голосов
/ 28 мая 2011

Я создаю систему обмена сообщениями для использования в игре XNA. Мои типы сообщений являются структурами, потому что я хочу, чтобы они вели себя как тип значения.

struct MyMessageType1 : IMessage {}
struct MyMessageType2 : IMessage {}

List<IMessage> messageQueue = new List<IMessage>();

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

Если у меня есть структуры, реализующие интерфейс, такой как IMessage, и я пытаюсь сохранить их в Списке, они упаковываются.

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

Итак, вопрос в том, как я могу сохранить список структур разных типов, не упаковывая их?

Ответы [ 5 ]

7 голосов
/ 28 мая 2011

Это не может быть сделано.

Альтернатива 1

Однако вы можете эмулировать вещи, используя два списка (List<MyMessageType1> и List<MyMessageType2>).

Затем вы создаете один супериндекс (возможно, просто другой массив целых чисел (длинных?)), Чтобы можно было (косвенно) адресовать элемент, как если бы он был одним списком.

Возможно, вы захотите оптимизировать индекс (кодирование длины выполнения: хранить только индексы, на которых переключается резервный массив: это также очень поможет при итерации поддиапазона, который, как известно, является смежным в одном из резервных массивов)

Списки используют Array storage для внутреннего использования, поэтому - вы не получите бокс - быстрый произвольный доступ - сверкающая итерация со списком. ForEach

Альтернатива 2

Посмотрите на атрибут StructLayout и каким-то образом эмулируйте Союз, выполняя все манипуляции. Если вы действительно готовы испачкать руки, добавьте блоки unsafe {} (и скомпилируйте с / unsafe) ... однако, серьезно подумайте о P / Invoke C DLL или используйте C ++ / CLI, если это имеет значение что много

Альтернатива 3 (добавлено)

Поскольку мне действительно понравился тот факт, что Марк Гравелл указал, что вы можете использовать упомянутый мной StructLayout, чтобы точно определить все три члена структуры union .NET с одинаковым смещением; Я думал, что сделаю еще один шаг и посмотрю, смогу ли я сделать это намного более дырявым прозрачным еще. Это очень близко к прозрачности:

using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace LeakyAbstractions
{
    struct TypeA {}
    struct TypeB {}
    struct TypeC {}

    [StructLayout(LayoutKind.Explicit)] internal struct AnyMessage {
        [FieldOffset(0)] public TypeA A;
        [FieldOffset(0)] public TypeB B;
        [FieldOffset(0)] public TypeC C;

        AnyMessage(TypeA a) { A = a; }
        AnyMessage(TypeB b) { B = b; }
        AnyMessage(TypeC c) { C = c; }

        public static implicit operator TypeA(AnyMessage msg) { return msg.A; }
        public static implicit operator TypeB(AnyMessage msg) { return msg.B; }
        public static implicit operator TypeC(AnyMessage msg) { return msg.C; }

        public static implicit operator AnyMessage(TypeA a) { return a; }
        public static implicit operator AnyMessage(TypeB b) { return b; }
        public static implicit operator AnyMessage(TypeC c) { return c; }
    }

    public class X
    {
        public static void Main(string[] s) 
        {
            var anyMessages = new List<AnyMessage> { 
                new TypeA(),
                new TypeB(),
                new TypeC(),
            };

            TypeA a = anyMessages[0];
            TypeB b = anyMessages[1];
            TypeC c = anyMessages[2];

            anyMessages.Add(a);
            anyMessages.Add(b);
            anyMessages.Add(c);
        }
    }
}

Я оставлю вам задачу различения этого варианта бедных мужчин как упражнение. Самый простой способ - добавить поле в структуру AnyMessage, но в зависимости от полезной нагрузки другие стратегии могут быть гораздо более эффективными (пространство / время).


Мои $ 0,02

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


PS. Если вы спросите об этом после прочтения моего ответа здесь (вчера: Должен ли я использовать структуру или класс для представления координат широты / долготы? ), я собираюсь оценить это преждевременно оптимизация

6 голосов
/ 28 мая 2011

По сути, вы не можете красиво ;

  • рассматривать как object или интерфейс: в штучной упаковке
  • обертка в общий тип с абстрактнымбазовый класс: переизобретает коробку
  • отражение: использует object, в штучной упаковке
  • dynamic: по существу object, в штучной упаковке

Там - это опция, однако, инкапсулирует объект в большую структуру, т.е.

struct AnyMessage {
    public TypeA A;
    public TypeB B;
    public TypeC C;
}
struct TypeA {...}
struct TypeB {...}
struct TypeC {...}

сейчас, это должно работать, но hsaнедостаток быть намного больше, очевидно.Вы могли бы обойти это, используя явный макет, чтобы расположить их все в байте 0 (делая union ), но я подозреваю это не такразрешено на xbox.Но на обычном .NET:

[StructLayout(LayoutKind.Explicit)] struct AnyMessage {
    [FieldOffset(0)] public TypeA A;
    [FieldOffset(0)] public TypeB B;
    [FieldOffset(0)] public TypeC C;
}
2 голосов
/ 29 мая 2011

Вы можете создать очередь, которая хранит ваши структуры без упаковки, а затем обрабатывает ее, используя интерфейс с универсальным методом, подобным этому:

interface IMessageProcessor
{
    void Process<T>(T message) where T : struct, IMessage;
}

class MessageQueue
{
    abstract class TypedMessageQueue
    {
        public abstract void ProcessNext(IMessageProcessor messageProcessor);
    }

    class TypedMessageQueue<T> : TypedMessageQueue where T : struct, IMessage
    {
        Queue<T> m_queue = new Queue<T>();

        public void Enqueue(T message)
        {
            m_queue.Enqueue(message);
        }

        public override void ProcessNext(IMessageProcessor messageProcessor)
        {
            messageProcessor.Process(m_queue.Dequeue());
        }
    }

    Queue<Type> m_queueSelectorQueue = new Queue<Type>();
    Dictionary<Type, TypedMessageQueue> m_queues =
        new Dictionary<Type, TypedMessageQueue>();

    public void Enqueue<T>(T message) where T : struct, IMessage
    {
        TypedMessageQueue<T> queue;
        if (!m_queues.ContainsKey(typeof(T)))
        {
            queue = new TypedMessageQueue<T>();
            m_queues[typeof(T)] = queue;
        }
        else
            queue = (TypedMessageQueue<T>)m_queues[typeof(T)];

        queue.Enqueue(message);
        m_queueSelectorQueue.Enqueue(typeof(T));
    }

    public void ProcessNext(IMessageProcessor messageProcessor)
    {
        var type = m_queueSelectorQueue.Dequeue();
        m_queues[type].ProcessNext(messageProcessor);
    }
}

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

2 голосов
/ 28 мая 2011

Не думаю, что ты можешь. Общность приходит за плату. Мой совет: не делайте преждевременную оптимизацию, если вас беспокоит производительность. Если это не так, и вам действительно нужно поведение копирования по значению, подумайте об использовании неизменяемых типов (например, System.String)

1 голос
/ 28 августа 2011

Возможно, полностью в управляемом коде, создать единый неуниверсальный тип структуры (который я назову MagicInvoker), который реализует интерфейс и содержит ссылки на произвольное число других структур, реализующих тот же интерфейс, все без использования отражения, бокса или чего-либо еще, что могло бы вызвать давление ГХ. Действительно, как только массивы достигли своих максимальных размеров, можно создавать и удалять миллиарды объектов типа значения без дополнительных выделений кучи.

Самым большим предостережением при таком подходе является то, что такие структуры во многом становятся похожими на указатели в «старом С». Хотя сами MagicInvokers являются типами значений и ссылаются на типы значений, их семантика больше похожа на указатели старого стиля. Если кто-то копирует MagicInvoker, он будет ссылаться на ту же структуру, что и оригинал. Создание MagicInvoker, а затем отказ от него без утилизации приведет к утечке памяти, а использование или попытка утилизировать уже удаленную копию MagicInvoker приведет к неопределенному поведению.

Public Interface IDoSomething
    Sub Dosomething()
End Interface
Structure MagicInvoker
    Implements IDoSomething, IDisposable

    Private holder As InvokerBase
    Private index As Integer
    Sub DoSomething() Implements IDoSomething.Dosomething
        holder.DoDoSomething(index)
    End Sub
    Shared Function Create(Of T As IDoSomething)(ByVal thing As T) As MagicInvoker
        Dim newInvoker As MagicInvoker
        newInvoker.holder = Invoker(Of T).HolderInstance
        newInvoker.index = Invoker(Of T).DoAdd(thing)
        Return newInvoker
    End Function
    Function Clone() As MagicInvoker
        Dim newHolder As New MagicInvoker
        newHolder.holder = Me.holder
        newHolder.index = Me.holder.DoClone(Me.index)
        Return newHolder
    End Function
    Private MustInherit Class InvokerBase
        MustOverride Sub DoDoSomething(ByVal Index As Integer)
        MustOverride Function DoClone(ByVal srcIndex As Integer) As Integer
        MustOverride Sub DoDelete(ByVal srcIndex As Integer)
    End Class
    Private Class Invoker(Of T As IDoSomething)
        Inherits InvokerBase
        Shared myInstances(15) As T, numUsedInstances As Integer
        Shared myDeleted(15) As Integer, numDeleted As Integer

        Public Shared HolderInstance As New Invoker(Of T)
        Overrides Sub DoDoSomething(ByVal index As Integer)
            myInstances(index).Dosomething()
        End Sub
        Private Shared Function GetNewIndex() As Integer
            If numDeleted > 0 Then
                numDeleted -= 1
                Return myDeleted(numDeleted)
            Else
                If numUsedInstances >= myInstances.Length Then
                    ReDim Preserve myInstances(myInstances.Length * 2 - 1)
                End If
                numUsedInstances += 1
                Return numUsedInstances - 1
            End If
        End Function
        Public Shared Function DoAdd(ByVal value As T) As Integer
            Dim newIndex As Integer = GetNewIndex()
            myInstances(newIndex) = value
            Return newIndex
        End Function
        Public Overrides Sub DoDelete(ByVal srcIndex As Integer)
            If numDeleted >= myDeleted.Length Then
                ReDim Preserve myDeleted(myDeleted.Length * 2 - 1)
            End If
            myDeleted(numDeleted) = srcIndex
            numDeleted += 1
        End Sub
        Public Overrides Function DoClone(ByVal srcIndex As Integer) As Integer
            Dim newIndex As Integer = GetNewIndex()
            myInstances(newIndex) = myInstances(srcIndex)
            Return newIndex
        End Function
    End Class

    ' Note: Calling Dispose on a MagicInvoker will cause all copies of it to become invalid; attempting
    '       to use or Dispose one will cause Undefined Behavior.  Conversely, abandoning the last copy of
    '       a MagicInvoker will cause a memory leak.

    Public Sub Dispose() Implements System.IDisposable.Dispose
        If holder IsNot Nothing Then
            holder.DoDelete(index)
            holder = Nothing
        End If
    End Sub
End Structure

MagicInvoker содержит экземпляр некоторого класса, производного от InvokerBase (который будет Invoker для некоторого T, который реализует IDoSomething), и индекс массива. Для каждого типа T, который используется с MagicInvoker.Create, будет создан один экземпляр класса Invoker ; этот же экземпляр будет использоваться для всех MagicInvoker, созданных из этого типа.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...