如何在没有拳击的情况下存储不同类型的结构

时间:2011-05-28 17:43:26

标签: c# generics struct xna boxing

我正在创建一个用于XNA游戏的消息传递系统。我的消息类型是结构,因为我希望它们以值类型的方式运行。

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

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

我希望能够在我的消息队列中存储不同类型的消息,但我想这样做而不会将它们中的任何一个打包。

如果我有结构实现了一个接口,如IMessage,我尝试将它们存储在List中,它们就会被装箱。

我不知道所有可能的消息类型,所以我不能只为每种类型硬编码一个列表。

所以问题是如何在没有盒装的情况下存储不同类型的结构列表?

5 个答案:

答案 0 :(得分:7)

这不可能。

备选方案1

但是,您可以使用两个列表(List<MyMessageType1>List<MyMessageType2>)来模拟内容。

然后您编写一个超级索引(可能只是另一个整数数组(多头?)),以便(可以)间接地对项目进行寻址,就像它是一个列表一样。

您可能希望优化索引(runlength编码:仅存储支持阵列切换的索引:这在迭代已知在其中一个支持数组中连续的子范围时也会非常有用)

列表在内部使用数组存储,所以    - 你没有拳击    - 快速随机访问    - 使用list.ForEach进行炽热迭代

备选方案2

查看StructLayout属性,并以某种方式通过执行所有操作来模拟Union。如果你真的准备好弄脏你的东西,抛出unsafe {}块(并使用/ unsafe编译)......但是,认真考虑P / Invoke C DLL或使用C ++ / CLI如果重要 很多

备选方案3(已添加)

因为我真的很喜欢Marc Gravell指出你可以使用我提到的StructLayout这一事实,以确定同一偏移量的 union .NET结构的所有三个成员;我以为我会采取额外的步骤,看看我是否能够做到更多 leaky tranparent 。这非常接近透明:

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。如果你在这里阅读我的答案之后问这个问题(昨天:Should I use a struct or a class to represent a Lat/Lng coordinate?),我将快速判断这个过早优化

答案 1 :(得分:6)

基本上,你不能很好地;

  • 视为object或接口:盒装
  • 使用抽象基类包装泛型类型:重新发明一个框
  • 反射:使用object,装箱
  • dynamic:基本上object,装箱

选项,但封装对象在更大的结构中,即

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

现在,这应该可行,但显然是更大的缺点。您可能能够使用显式布局解决这个问题,将它们全部放在字节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 :(得分:2)

我认为你不能。一般性是有代价的。我的建议是,如果您担心的是性能,请不要过早优化。如果它不是,你真的需要按值行为复制考虑使用inmutable类型(一个系统.String)

答案 3 :(得分:2)

你可以创建一个存储你的结构而不用装箱的队列,然后使用带有这样的通用方法的接口来处理它:

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诡计,也不会事先知道所有可能的邮件。

答案 4 :(得分:1)

完全在托管代码中,可以创建一个实现接口的单一非泛型类型的结构(我称之为MagicInvoker),并保存对实现相同接口的任意数量的其他结构的引用,所有这些都不使用反射,装箱或其他会导致GC压力的东西。实际上,一旦数组达到了它们的最大大小,就可以创建和删除数十亿的值类型对象,而无需进行任何堆分配。

这种方法最大的警告是,这种结构在许多方面变得像“旧C”中的指针一样。虽然MagicInvokers本身就是一个值类型,并且它们引用了值类型,但它们的语义更像是旧式指针。如果复制一个MagicInvoker,它将引用与原始相同的结构。创建一个MagicInvoker然后在不使用Dispose的情况下放弃它会导致内存泄漏,并且使用或尝试处理已经处置的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的类的实例(对于某些实现IDoSomething的T来说恰好是一个Invoker&lt; T&gt;)和一个数组索引。对于与MagicInvoker.Create一起使用的每个类型T,将存在一个类Invoker&lt; T&gt;的实例。创建;同一个实例将用于从该类型创建的所有MagicInvokers。