在 C# 中从锯齿状数组转换为双指针

Posted

技术标签:

【中文标题】在 C# 中从锯齿状数组转换为双指针【英文标题】:Converting from a jagged array to double pointer in C# 【发布时间】:2010-10-27 18:25:08 【问题描述】:

这里有一个简单的问题:有什么方法可以将锯齿状数组转换为双指针?

例如将double[][] 转换为double**

不幸的是,这不能仅通过强制转换来完成(就像在普通的旧 C 中一样)。使用fixed 语句似乎也不能解决问题。在 C# 中是否有任何(最好尽可能高效)方法来完成此任务?我怀疑解决方案可能根本不是很明显,尽管我希望有一个简单的解决方案。

【问题讨论】:

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

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

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

问题的简要说明:

当我们在堆上分配一些对象时,它们可以在垃圾收集时移动到另一个位置。所以,想象下一种情况:你已经分配了一些对象和你的内部数组,它们都放在堆上的零代中。

现在,一些对象已经从作用域中消失并变成了垃圾,一些对象刚刚被分配。垃圾收集器会将旧对象移出堆,并将其他对象移到更靠近开始甚至下一代的位置,从而压缩堆。结果将如下所示:

因此,我们的目标是将一些对象“固定”在堆中,这样它们就不会移动。 我们需要什么来实现这个目标?我们有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 现在我意识到我没有明白你的意思。好吧,我已经编辑了答案以显示 double** 的位置。希望这是你要求我做的。我第一次想到你让我写一个类,它可以获取一个锯齿状数组并将其转换为一个连续数组,使用 GetAddr 函数将这个数组暴露在外部,具有线程安全、资源管理、IDispossable 和其他 SafeHandleZeroOrMinusOneIsInvalid 接口。花了几个小时来实现它,哈哈。 哈哈,希望时间没有完全浪费。保留该课程以防您将来需要它。 :) 是的,我们只需要在答案中看到双**,仅此而已。再次感谢您在许多方面付出的额外努力! 我编辑了您的答案,以包含您在其中一个 cmets 中提到的示例代码。我觉得这个答案太棒了。我现在就奖励赏金。【参考方案2】:

我暂时使用了 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;
    

【讨论】:

您不能在声明它的fixed 块之外使用指针,因为此时对象可能已经移动。 @svick:当然可以。它可能不会一直有效。碰巧的是,在这种情况下确实如此......也许Marshal 静态方法之一将有助于使其更加健壮。 好吧,也许它现在似乎工作了。但是你在不相关的地方换了一行,或者有一天运气不好,它就行不通了。您的代码错误。它目前有效的事实主要是一个意外。 是的,这就是我刚才所说的。没必要这么***。 ;) 这也有助于发布解决方案,而不仅仅是投反对票和批评。【参考方案3】:

一个 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# 无法自动编组锯齿状数组。 顺便说一句,我会尽快尝试一下。谢谢。 我不得不编辑这个 6 次才能绕过所以想要将 *s 解析为斜体。预览窗口和实际帖子的解释不一致... 你确定这会起作用吗?在我看来,只有在将内部数组分配给arrayofptr[i] 时才固定它们。这意味着数组可以在您使用其指针时移动,这可能会破坏内存并导致不可预知的错误。 Sample 的代码证明如果有人感兴趣,如果移动内部数组,@bsneeze 的代码可能会导致错误。

以上是关于在 C# 中从锯齿状数组转换为双指针的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中将字符串转换为双精度

在 C# 中将十进制数转换为双精度数会产生差异

在 C# 中“转换”浮点数为双精度值

如何在Java中将浮点数组转换为双精度数组?

C# 将字符转换为双精度浮点型

java.nio.BufferUnderflowException 将字节数组转换为双精度