如何在 C# 中创建一个包含数组但不使用堆的结构?

Posted

技术标签:

【中文标题】如何在 C# 中创建一个包含数组但不使用堆的结构?【英文标题】:How do i create a struct in C# that contains an array but does not use heap? 【发布时间】:2020-12-20 20:33:23 【问题描述】:

我需要什么:

具有任意数量顶点(或至少达到max顶点数量)的多边形 应该是struct,这样才能快,可以按值赋值/传递

似乎我不能使用数组或集合来存储顶点,因为那样我的多边形结构将指向堆上的对象,并且当一个多边形按值分配给另一个多边形时,只会执行浅拷贝,并且我会让两个多边形都指向同一个顶点数组。例如:

Polygon a = new Polygon(); 
Polygon b = a; 
// both polygons would be changed 
b.vertices[0] = 5; 

那么我如何创建一个可以有任意数量(或一些固定数量)顶点但根本不使用堆的结构?

我可以使用很多变量,例如 v1, v2, v3 ... v10 等,但我想或多或少地保持我的代码干净。

【问题讨论】:

你真正关心的是stack vs. heap还是reference type vs. value type?如果是前者,请查看stackalloc。 在堆栈内存中完全拥有一个潜在的大多边形并传递它(每次将它作为函数参数传递时复制它等)不一定比使用标准更快(甚至可能更慢)收藏。您是否有需要解决的实际问题,或者您是否正在尝试先发制人地优化甚至可能不存在的问题? 我觉得在这种情况下,您的问题更多是关于深度复制列表,而不是关于堆栈与堆。 Stack 和 Heap 是我们 99% 的时间不需要担心的实现细节。 另见:.NET Collection that is a struct 和 Deep copy of List<T> 恕我直言,如果你解释你想做什么,你可能会得到更好的答案。你的问题是关于如何做到这一点。请edit您的问题。请记住,C# / Roslyn 编译器技术在优化代码和处理数据结构方面做得非常出色。如果您需要智取它,您可能确切地知道它对您做错了什么,您应该告诉我们。过早的优化会使代码完全无法维护。 【参考方案1】:

如果它符合您的逻辑,您可以使用分配在堆栈上的Span&lt;T&gt;。阅读更多here

【讨论】:

【参考方案2】:

另一种使用复制构造函数复制数组的方法

public Polygon(Polygon other)

    this.vertices = other.vertices.Clone() as int[];

然后

var a = new Polygon();
a.vertices[0] = 5;

var b = new Polygon(a):
Debug.WriteLine(a.vertices[0]);
// 5
Debug.WriteLine(b.vertices[0]);
// 5

b.vertices[0] = 10;
Debug.WriteLine(a.vertices[0]);
// 5
Debug.WriteLine(b.vertices[0]);
// 10

【讨论】:

【参考方案3】:

您可以选择使用 fixed 关键字定义您的数组,这会将其放入堆栈中。

但是你不能直接访问数组的元素,除非你在 unsafe 上下文中并且使用指针。

要获得以下行为:

    static void Main(string[] args)
    
        FixedArray vertices = new FixedArray(10);
        vertices[0] = 4;

        FixedArray copy = vertices;
        copy[0]  = 8;
        Debug.WriteLine(vertices[0]);
        // 4
        Debug.WriteLine(copy[0]);
        // 8
    

然后使用下面的类定义:

public unsafe struct FixedArray 

    public const int MaxSize = 100;

    readonly int size;
    fixed double data[MaxSize];

    public FixedArray(int size) : this(new double[size])
     

    public FixedArray(double[] values)
    
        this.size = Math.Min(values.Length, MaxSize);
        for (int i = 0; i < size; i++)
        
            data[i] = values[i];
        
    

    public double this[int index]
    
        get
        
            if (index>=0 && index<size)
            
                return data[index];
            
            return 0;
        
        set
        
            if (index>=0 && index<size)
            
                data[index] = value;
            
        
    

    public double[] ToArray()
    
        var array = new double[size];
        for (int i = 0; i < size; i++)
        
            array[i] = data[i];
        
        return array;
            

有几点需要考虑。以上需要用unsafe选项编译。 MaxSize 也是一个常数,并且所需的存储空间不能超过这个值。我正在使用索引器this[int] 来访问元素(而不是字段),并且还有一种方法可以使用ToArray() 转换为本机数组。构造函数也可以采用本机数组,或者它将使用空数组来初始化值。这是为了确保 new FixedArray(10) 例如将在固定数组中初始化至少 10 个值(而不是未定义,因为它是默认值)。

详细了解fixed from Microsoft 的用法或搜索C# Fixed Size Buffers

堆数组字段

 struct StdArray
 
     int[] vertices;

     Foo(int size)
     
         vertices = new int[size];
     
 

堆栈数组字段

 unsafe struct FixedArray
 
     fixed int vertices[100];
     int size;
     Foo(int size)
     
         this.size = size;
         // no initialization needed for `vertices`
     
 

【讨论】:

修复一个对象不会将它移动到堆栈中,也不会让它表现得像一个值类型。它只是确保 GC 在其收集期间不会移动它。 @Servy 其实不是,fixed 在结构中是一个特殊的关键字,用于移动堆栈中的值。我简化了示例并删除了fixed() 语句,它仍然有效。这是自the initial introduction 以来的一项改进。试一试,你会发现这完全符合要求。 据我了解,“fixed”关键字将固定大小的数组作为结构的一部分嵌入。数组是在栈上还是堆上取决于结构体的使用方式。如果它被用作局部变量声明,那么确实,数据将存在于堆栈中,但如果它用作引用类的成员,那么缓冲区将存在于堆上(带有嵌入类的实例整个固定大小的数组作为其字段的一部分,而不是作为对独立数组的引用) 因此,如果声明了class Foo FixedArray data; ,数据可能会驻留在堆上。这是有道理的,因为struct 的目的是将所有数据放在一起。

以上是关于如何在 C# 中创建一个包含数组但不使用堆的结构?的主要内容,如果未能解决你的问题,请参考以下文章

如何从 2 个查询的结果中创建一个数组?

试图在 C++ 中创建一个包含结构的数组?

如何在 C# 中创建包含不同类型的字典类型?

如何在 C# 中创建 List<int> 数组?

如何在 C# 中创建索引从 1 开始的一维数组

在 C# 中创建一个高分列表