如何在不装箱的情况下存储不同类型的结构体

14

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

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

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

我想将不同类型的消息存储在我的消息队列中,但是我希望在此过程中不对它们进行任何装箱操作。

如果我让结构体实现一个名为IMessage的接口并尝试将它们存储到List中,它们就会被装箱。

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

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


你为什么想让它们成为结构体?而且为什么不想让它们装箱?你担心性能吗? - svick
我希望它们是结构体,因为我认为对于我的消息对象来说,值类型语义感觉更正确。我不想让它们装箱,因为我在一个XNA游戏中使用它们作为我的消息传递机制,我不想创建任何垃圾需要垃圾收集器清理。 - BowserKingKoopa
“我不想创建任何垃圾” 为什么不呢?你知道它会给你带来性能问题吗? - svick
3
垃圾会在Xbox上制造问题。该消息系统每帧被使用了许多次,如果它产生垃圾,垃圾回收器将一直在工作。 - BowserKingKoopa
7个回答

13

这是做不到的。

备选方案1

然而,你可以通过使用两个列表 (List<MyMessageType1>List<MyMessageType2>) 来模拟这些东西。

然后,你可以构造一个超级索引 (可能只是另一个 int 数组 (longs?) ),使得能够(间接)像处理一个列表一样地处理其中的项。

你可能想要优化此索引 (运行长度编码: 仅存储后备数组切换的索引: 当迭代已知为一个后备数组连续子范围时,这也将极大地帮助)

列表在内部使用阵列存储,所以: - 你不会遭遇装箱问题 - 快速随机访问 - 使用 list.ForEach 快速迭代

备选方案2

查看 StructLayout 特性,并通过进行所有操作来模拟 Union。如果你真的准备好了,请使用unsafe {}代码块(并启用 / unsafe 编译选项)... 然而,如果非常重要,认真考虑 P/Invoke C DLL 或使用 C++/CLI。

备选方案3 (新增)

因为我真的很喜欢 Marc Gravell 指出的可以使用我提到的 StructLayout,来定位 .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结构中添加一个字段,但根据负载的不同,其他方法可能更加高效(空间/时间)。


我的两分钱

噢,我从未真正这样做,因为它似乎过于复杂了。我假设你有一个有效的理由来优化这个。


另外,如果你在看完我的回答之后才问这个问题(昨天: 应该使用结构体还是类来表示Lat/Lng坐标?),我将会怀疑这是一种过早的优化。


我在考虑使用备选方案1,即多个通用列表解决方案,但由于我不知道编译时所有可能的消息类型,因此我需要动态创建这些通用列表,这是可以做到的。但现在我必须想办法存储这些列表。如果我将它们放在List<IList>中,那么当我尝试从ILists中获取值时,就会产生装箱操作。如果我将它们存储为ILists,我无法想出一种在运行时将其强制转换回其List<MyMessageType1>本身的方法,以避免任何装箱操作。有什么办法可以做到这一点吗? - BowserKingKoopa
@BowserKingKoopa:如果你的元素将是有限类型集合,我会简单地使用List<Ilist>;只要你尽早将_List_本身转换,而不是其中的项,访问底层项时绝不会产生装箱。任何比这更多的都是在寻求值类型变量实现。如果允许这样做,就不会发明装箱了。 - sehe
1
我在Marc Gravells的PoC上实现了更多的粘合剂;这几乎是透明的(请参见Main中的用法) - sehe

12

基本上,你不能优雅地处理;

  • 将其视为object或接口:需要装箱
  • 使用具有抽象基类的泛型类型进行包装:重新发明一个盒子
  • 反射:使用object,需要装箱
  • dynamic:本质上是object,需要装箱

然而,封装对象在更大的结构体中是一个选项,即:

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

现在,这应该可以工作,但明显的缺点是更大了。你可能能够通过使用显式布局将它们全部定位到字节0(创建一个联合体)来解决这个问题,但我怀疑在Xbox上不允许这样做。但在常规的.NET中:

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

2
实际上,在 Xbox 上你可以使用 StructLayoutFieldOffset - Andrew Russell

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

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技巧和事先知道所有可能的消息类型。

1

我认为你做不到。普适性是有代价的。我的建议是,如果你担心的是性能问题,不要过早地进行优化。如果不是这个问题,而你真的需要按值复制的行为,请考虑使用不可变类型(例如 System.String)。


1

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

这种方法的最大警告是,这些结构在许多方面都像“旧C”中的指针。虽然MagicInvokers本身是值类型,并且它们引用值类型,但它们的语义更像是旧式指针。如果复制MagicInvoker,则它将引用与原始结构相同的结构。创建MagicInvoker,然后在没有Dispose的情况下放弃它将导致内存泄漏,并且使用或尝试处理已经被处理的MagicInvoker的副本将导致未定义行为。

公共接口IDoSomething 子过程DoSomething() 结束接口 结构体MagicInvoker 实现IDoSomething,IDisposable 私有变量holder作为InvokerBase 私有变量index作为整数 子过程DoSomething()实现IDoSomething.Dosomething holder.DoDoSomething(index) 结束子过程 共享函数Create(Of T As IDoSomething)(ByVal thing As T)作为MagicInvoker Dim newInvoker As MagicInvoker newInvoker.holder = Invoker(Of T).HolderInstance newInvoker.index = Invoker(Of T).DoAdd(thing) 返回newInvoker 结束函数 函数Clone()作为MagicInvoker Dim newHolder As New MagicInvoker newHolder.holder = Me.holder newHolder.index = Me.holder.DoClone(Me.index) 返回newHolder 结束函数 私有抽象类InvokerBase 必须重写子过程DoDoSomething(ByVal Index As Integer) 必须重写函数DoClone(ByVal srcIndex As Integer)作为整数 必须重写子过程DoDelete(ByVal srcIndex As Integer) 结束类 私有类Invoker(Of T As IDoSomething) 继承InvokerBase 共享myInstances(15)作为T,numUsedInstances作为整数 共享myDeleted(15)作为整数,numDeleted作为整数
公共共享HolderInstance作为新的Invoker(Of T) 重写子过程DoDoSomething(ByVal index As Integer) myInstances(index).Dosomething() 结束子过程 私有共享函数GetNewIndex()作为整数 如果numDeleted > 0 Then numDeleted -= 1 返回myDeleted(numDeleted) Else 如果numUsedInstances >= myInstances.Length Then ReDim Preserve myInstances(myInstances.Length * 2 - 1) End If numUsedInstances += 1 返回numUsedInstances - 1 End If 结束函数 共享函数DoAdd(ByVal value As T)作为整数 Dim newIndex As Integer = GetNewIndex() myInstances(newIndex) = value 返回newIndex 结束函数 重写子过程DoDelete(ByVal srcIndex As Integer) 如果numDeleted >= myDeleted.Length Then ReDim Preserve myDeleted(myDeleted.Length * 2 - 1) End If myDeleted(numDeleted) = srcIndex numDeleted += 1 结束子过程 重写函数DoClone(ByVal srcIndex As Integer)作为整数 Dim newIndex As Integer = GetNewIndex() myInstances(newIndex) = myInstances(srcIndex) 返回newIndex 结束函数 结束类
'注意:在MagicInvoker上调用Dispose将导致其所有副本无效;尝试使用或Dispose一个将导致未定义的行为。 '相反,放弃MagicInvoker的最后一个副本将导致内存泄漏。
公共子过程Dispose()实现System.IDisposable.Dispose 如果holder IsNot Nothing Then holder.DoDelete(index) holder = Nothing End If 结束子过程 结束结构体
一个 MagicInvoker 持有某个派生自 InvokerBase 的类的实例(它将恰好是一个 Invoker<T>,其中 T 是实现了 IDoSomething 接口的某个类型),以及一个数组索引。对于每个在 MagicInvoker.Create 中使用的类型 T,都会创建一个 Invoker<T> 类的实例;该实例将用于从该类型创建的所有 MagicInvoker。

谢谢你的回答。我在问题中没有提到我的目标是性能,所以这个回答可惜对我没有帮助,我应该在一开始就提到这一点。 - MHGameWork
@MHGameWork:MagicInvoker技术可以实现卓越的性能;我没有表达清楚吗? - supercat
我在一个非常热的代码路径中使用这个,所以我不想使用虚函数调用并冒险出现缓存未命中。 - MHGameWork
@MHGameWork:.NET框架的即时编译器将为每个不同使用的MagicInvoker<T>类生成单独的机器代码,从而避免了运行时虚函数调用的需要。这种方法的优点之一就是避免了虚函数调用。 - supercat
我会更深入地了解一下,距离我上次使用VB.NET已经有15年了 :) 我还需要运行基准测试来确保。 - MHGameWork

0

我曾经遇到过一个类似的问题,即以非装箱方式存储结构体,并在谷歌上找到了这篇帖子。所以即使这个答案并不是严格意义上解决你的问题的方法,我还是想把它写在这里,因为我相信那些试图解决我遇到的同样问题的人可能会找到它。

我使用内存数据库来存储整个应用程序的状态,所有的数据都是结构体,但不知道有多少种不同的结构体类型。内存数据库至少应该支持更新、插入和获取数据,而且这些操作可能会经常进行,不能进行装箱。

如果我们将所有东西都按照其原始类型存储,就不会有装箱的问题。那么为什么不为每种结构体类型创建一个单独的字典/列表/回收包装对象呢?然后当调用时,我们可以将字典存储到另一个带有类型键的字典中,并在需要访问时将其转换为正确的类型。如果我们想通过像IIdentifiable或IMessage这样的接口通用处理结构体,我们可以使用where T: IIdentifier约束来使用IIdentifiable的特性而不进行装箱。然而,如果我们想对字典中的每个IIdentifiable运行一个方法,我们可能需要一些额外的包装,但我认为这有点超出了范围。

我目前的解决方案是这样的:

        private readonly Dictionary<Type, object> StateDictionary = new Dictionary<Type, object>();


        public Task Upsert<T>(T identifiable)
            where T : struct, IIdentifiable
        {
            if (string.IsNullOrEmpty(identifiable.Id))
                throw new ArgumentNullException($"{typeof(T)} had null id");

            if (!TryGetInnerDictionary<T>(StateDictionary, out var innerDictionary))
            {
                StateDictionary[typeof(T)] = innerDictionary;
            }
            
            innerDictionary[identifiable.Id] = identifiable;

            Debug.Log($"Upsert {typeof(T)} {identifiable.Id}");

            SendChangedEvent(identifiable);

            return Task.CompletedTask;
        }

        private static bool TryGetInnerDictionary<T>(Dictionary<Type, object> outerDictionary,
                                                     out Dictionary<string, T> innerDictionary)
        {
            if (!outerDictionary.TryGetValue(typeof(T), out var objInnerDictionary))
            {
                innerDictionary = new Dictionary<string, T>();
                outerDictionary[typeof(T)] = innerDictionary;

                return false;
            }

            innerDictionary = (Dictionary<string, T>)objInnerDictionary;

            return true;
        }


        public T Get<T>(string id)
            where T : struct, IIdentifiable
        {
            TryGetInnerDictionary<T>(StateDictionary, out var innerDictionary);
            var success = innerDictionary.TryGetValue(id, out var state);

            return success ? (T)state : throw new ArgumentNullException($"Unable to find {typeof(T)} with id {id}");
        }


0

这是我解决这个问题的方法。 在入队时不分配内存,只有在并发队列扩展大小时才进行分配。

public class JobQueue { 
    abstract class Invoker {
        public abstract void Do();
    }
    class Invoker<F>:Invoker where F : IJob {
        static volatile Invoker<F> _ins = null;
        static object syncRoot = new Object();

        public static Invoker<F> ins {
            get {
                if (_ins == null) {
                    lock (syncRoot) {
                        if (_ins == null)
                            _ins = new Invoker<F>();
                    }
                }
                return _ins;
            }
        }
        public int registerIndex { get; private set; } = -1;
        ConcurrentQueue<F> queue;

        Invoker() {
            queue = new ConcurrentQueue<F>();
        }
        public void Register(int index) {
            registerIndex = index;
        }
        public void Enqueue(F t) {
            queue.Enqueue(t);
        }
        public override void Do() {
            if (queue.TryDequeue(out var msg)) {
                msg.DoJob();
            }
        }
    }

    public bool isWork { get; set; } = false;
    ManualResetEvent manualResetEvent = new ManualResetEvent(false);
    Invoker[] invokers = new Invoker[1000];
    int index = 0;
    ConcurrentQueue<int> jobIndexs = new ConcurrentQueue<int>();

    public void Enqueue<T>(T t)where T : IJob {
        var invoker = Invoker<T>.ins;
        lock (invoker) {
            if (invoker.registerIndex == -1) {
                lock (invokers) {
                    invokers[index] = invoker;
                    invoker.Register(index);
                    jobIndexs.Enqueue(index);
                    index++;
                }               
            } else {
                jobIndexs.Enqueue(invoker.registerIndex);
            }
            invoker.Enqueue(t);
        }
        manualResetEvent.Set();
    }
    public void Run() {
        while (true) {
            if(jobIndexs.TryDequeue(out var index)) {
                invokers[index].Do();
            } else {
                manualResetEvent.Reset();
                return;
            }
            //if (!isWork)
            //    return;
            manualResetEvent.WaitOne();
        }
    }
}

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