在C#中将不规则数组转换为双指针

14

简单的问题:是否有办法将锯齿数组转换为双指针?

例如,将double[][] 转换成 double**

遗憾的是,这不能像在普通的C语言中那样只需进行强制转换就能完成。使用fixed语句似乎也无法解决问题。是否有任何(最好是尽可能高效的)方法来在C#中实现这一点?我怀疑解决方案可能并不是非常明显,但我仍然希望有一个简单明了的解决方法。


我想这个问题归结为:你能否将单指针(指向对象)转换为指向指针的指针? - Andriy Volkov
4
“使用'ToPointer'扩展方法的解决方案是一个不好的主意,因为这样你会在'fixed'区域之外使用指针,而此时.NET运行时可能已经将数组移动到另一个内存位置。” - Greg
微软应该考虑的更智能的设计:将“fixed”作为数组对象的基本属性,然后使数组可转换为指针(与C/C++匹配),而不需要奇怪的固定“语句”。这样,当您将数组转换为指针时,作为转换的一部分,数组会自动无限期地“固定”自身(因为无法预测指针可能存在多长时间)。我想不出任何其他功能性、安全的方法来在托管的OOP语言中实现数组指针支持 - 我个人认为微软错过了这一点。 - Giffyguy
只是好奇。@Noldorin,您是否知道为每个内部数组分配的内存可能不连续?换句话说,每个内部数组之间可能存在间隙。 - cassandrad
我认为现在是时候改变接受的答案了。:) @cassandradied提供了一个更详细的答案,修复了所有不安全指针的问题。 - Giffyguy
3个回答

7

一点点安全。
如第一个解决方案的评论中所提到的,嵌套数组可以被移动,因此它们也应该被固定。

unsafe
{
    double[][] array = new double[3][];
    array[0] = new double[] { 1.25, 2.28, 3, 4 };
    array[1] = new double[] { 5, 6.24, 7.42, 8 };
    array[2] = new double[] { 9, 10.15, 11, 12.14 };

    GCHandle[] pinnedArray = new GCHandle[array.Length];
    double*[] ptrArray = new double*[array.Length];

    for (int i = 0; i < array.Length; i++)
    {
        pinnedArray[i] = GCHandle.Alloc(array[i], GCHandleType.Pinned);
    }

    for (int i = 0; i < array.Length; ++i)
    {
        // as you can see, this pointer will point to the first element of each array
        ptrArray[i] = (double*)pinnedArray[i].AddrOfPinnedObject();
    }

    // here is your double**
    fixed(double** doublePtr = &ptrArray[0])
    {
        Console.WriteLine(**doublePtr);
    }

    // unpin all the pinned objects,
    // otherwise they will live in memory till assembly unloading
    // even if they will went out of scope
    for (int i = 0; i < pinnedArray.Length; ++i)
        pinnedArray[i].Free();
}

问题的简要说明:
当我们在堆上分配一些对象时,它们可能会在垃圾回收时移动到另一个位置。因此,想象一下下面的情况:您已经分配了一些对象和内部数组,它们都放置在堆的零代中。

enter image description here

现在,有些对象已经超出了作用域并成为垃圾,有些对象刚刚被分配。垃圾收集器将移动旧对象出堆,并将其他对象靠近开头甚至移到下一代,压缩堆。结果将如下所示:

enter image description here

因此,我们的目标是“固定”堆中的某些对象,以便它们不会移动。我们需要什么来实现这个目标?我们有fixed语句和GCHandle.Allocate方法。
首先,GCHandle.Allocate做了什么?它在内部系统表中创建一个新条目,该表具有对作为参数传递给方法的对象的引用。因此,当垃圾收集器检查堆时,它将检查条目的内部表,如果找到一个条目,它将将对象标记为活动对象,并且不会将其移出堆。然后,它将查看如何固定此对象,并且在紧缩阶段不会移动内存中的对象。fixed语句几乎执行相同的操作,只是在离开作用域时自动“取消固定”对象。
总结一下:使用fixed固定的每个对象都会在离开作用域时自动“取消固定”。在我们的情况下,这将在下一次循环迭代中发生。
如何检查您的对象不会被移动或垃圾回收:只需消耗零代的所有堆预算并强制GC压缩堆。换句话说:在堆上创建大量对象。并在固定您的对象或“修复”它们之后执行此操作。
for(int i = 0; i < 1000000; ++i)
{
    MemoryStream stream = new MemoryStream(10);
    //make sure that JIT will not optimize anything, make some work
    stream.Write(new Byte[]{1,2,3}, 1, 2);
}
GC.Collect();

小提示:堆有两种类型——用于大对象和用于小对象。如果您的对象很大,应该创建大对象来检查代码,否则小对象不会强制GC开始垃圾回收和压缩。

最后,这里有一些示例代码,演示了使用未固定指针访问底层数组的危险——供任何感兴趣的人参考。

namespace DangerousNamespace
{
    // WARNING!
    // This code includes possible memory access errors with unfixed/unpinned pointers!
    public class DangerousClass
    {
        public static void Main()
        {
            unsafe
            {
                double[][] array = new double[3][];
                array[0] = new double[] { 1.25, 2.28, 3, 4 };
                array[1] = new double[] { 5, 6.24, 7.42, 8 };
                array[2] = new double[] { 9, 10.15, 11, 12.14 };

                fixed (double* junk = &array[0][0])
                {
                    double*[] arrayofptr = new double*[array.Length];
                    for (int i = 0; i < array.Length; i++)
                        fixed (double* ptr = &array[i][0])
                        {
                            arrayofptr[i] = ptr;
                        }

                    for (int i = 0; i < 10000000; ++i)
                    {
                        Object z = new Object();
                    }
                    GC.Collect();

                    fixed (double** ptrptr = &arrayofptr[0])
                    {
                        for (int i = 0; i < 1000000; ++i)
                        {
                            using (MemoryStream z = new MemoryStream(200))
                            {
                                z.Write(new byte[] { 1, 2, 3 }, 1, 2);
                            }
                        }
                        GC.Collect();
                        // should print 1.25
                        Console.WriteLine(*(double*)(*(double**)ptrptr));
                    }
                }
            }
        }
    }
}

你能否编辑这个答案,展示如何创建一个double?正如你在早前的评论中提到的,数组不是连续的,所以你需要创建一个单独的固定指针数组,然后返回一个指向该单独数组的指针。也许还需要另一种方法,在不再需要double时取消固定指针。实际上,这听起来像是System.Array的扩展方法的一个很好的候选者。 :) - Giffyguy
@Giffyguy 嗯,是的,我可以这样做,但我认为这会像我们回答不同的问题。对我来说,创建连续数组的必要性是主观的,并且可以用几种方式实现,这些方式将取决于初始目的和接收方。但无论如何,我会在我的答案中添加一些“简单”的实现方法。 - cassandrad
@Giffyguy,现在我意识到我没有理解你的意思。好吧,我已经编辑了答案,展示了double**的位置。希望这就是你要求我的。一开始我以为你让我编写一个类,可以获取一个锯齿数组并将其转换为连续数组,使用GetAddr函数将此数组公开,具有线程安全性、资源管理、IDispossable和其他SafeHandleZeroOrMinusOneIsInvalid接口。花了几个小时来实现它,哈哈。 - cassandrad
哈哈,希望这段时间没有完全浪费。留着那个类,以防将来需要用到它。 :) 是的,我们只是需要在答案中看到双星号。再次感谢您在许多方面都额外努力! - Giffyguy
我编辑了你的答案,包括你在评论中提到的示例代码。我认为这个答案非常棒。我现在会授予奖励。 - Giffyguy
@Giffyguy 谢谢。如果我在某些地方理解有误,对不起。 - cassandrad

4

double[][]是一个double[]的数组,而不是double*的数组,因此要获得double**,我们首先需要一个double*[]。

double[][] array = //whatever
//initialize as necessary

fixed (double* junk = &array[0][0]){

    double*[] arrayofptr = new double*[array.Length];
    for (int i = 0; i < array.Length; i++)
        fixed (double* ptr = &array[i][0])
        {
            arrayofptr[i] = ptr;
        }

    fixed (double** ptrptr = &arrayofptr[0])
    {
        //whatever
    }
}

我不禁想知道这是干嘛的,是否有比要求双指针更好的解决方案。


很遗憾,我无法避免使用双指针,因为我正在调用外部的C函数,而C#无法自动转换交错数组。 - Noldorin
我会尽快开始进行这个尝试,顺便说一句。谢谢。 - Noldorin
我不得不编辑这个内容大约6次,以避免SO将*解析为斜体。预览窗口和实际发布的解释不一致... - bsneeze
@zachrrs:你的方法看起来已经足够好了。我觉得在我的想法中,通过循环遍历维度并不是理想的解决方案,但我认为在C#中可能是必要的。我打算再开放一段时间以便其他人有新的东西可以添加。如果没有,答案就是你的。 :) - Noldorin
4
你确定这样会起作用吗?在我看来,你只有在将内部数组赋给arrayofptr[i]时才固定了指针。这意味着当你使用指针时数组可以移动,这可能会破坏内存并导致不可预测的错误。 - svick
2
如果有人感兴趣,这是一个证明@bsneeze的代码可能会导致错误,如果内部数组被移动的示例代码。 - cassandrad

-5

目前我选择了zachrrs的解决方案(这也是我一开始怀疑可能需要做的)。这是一个扩展方法:

public static double** ToPointer(this double[][] array)
{
    fixed (double* arrayPtr = array[0])
    {
        double*[] ptrArray = new double*[array.Length];
        for (int i = 0; i < array.Length; i++)
        {
            fixed (double* ptr = array[i])
                ptrArray[i] = ptr;
        }

        fixed (double** ptr = ptrArray)
            return ptr;
    }
}

6
你不能在声明指针的fixed代码块之外使用它,因为对象可能会在此期间移动。 - svick
@svick:当然可以。只是有时可能不起作用。恰好在这种情况下它确实有效...也许使用Marshal静态方法之一可以使其更加健壮。 - Noldorin
5
也许现在看起来可以运行。但是如果你改动了一个不相关的代码行,或者有一天运气不好,它就不能工作了。你的代码是错误的。当前它能够工作大多是偶然的。 - svick
是的,这就是我刚才说的。没必要那么狂热。 ;) 发布解决方案而不仅仅是投票和批评也有帮助。 - Noldorin

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