【问题标题】:How to truncate an array in place in C#如何在 C# 中原地截断数组
【发布时间】:2015-10-29 16:05:49
【问题描述】:

我的意思是真的有可能吗? MSDN 说数组是固定大小的,调整大小的唯一方法是“复制到新位置”。但也许内部 CLR 结构的不安全/一些魔法是可能的,它们都是用 C++ 编写的,我们有一个完整的内存控制,可以调用 realloc 等等。

我没有为这个问题提供代码,因为我什至不知道它是否存在。


我不是在谈论 Array.Resize 方法等,因为它们显然没有所需的行为。

假设我们有一个带有 2GB 内存的标准 x86 进程,而我有 1.9GB 由单个数组填充。然后我想释放一半。所以我想写这样的东西:

MagicClass.ResizeArray(ref arr, n)

并且不要得到 OutOfMemoryException。 Array.Resize 将尝试分配另一 GB 的 RAM,但会因 1.9+1 > 2GB OutOfMemory 而失败。

【问题讨论】:

  • 你可以声明一个像下面var someArray = {}这样的数组然后例如如果你想在运行时加载它并且不知道你可以这样做的潜在大小..var someList = new List{"apple", "orange", "pears"};然后加载像这样的数组someArray = someList.ToArray();
  • Array.Resize(ref yourArray, newCount);
  • 问题是,您实际上并没有询问您遇到的问题。这使得问题变得相当琐碎,答案可能会错过你的分数。 为什么要截断数组?在什么样的情况下你想要这个,尝试进行就地截断的原因是什么?想要保留内存的每一个小字节?让复制发生——这就是 .NET 的设计目的。想要在更改数组大小的同时避免昂贵的副本?使用一个列表,并好好使用它——如果它不适合你,甚至可以自己编写。

标签: c# c++ .net arrays memory-management


【解决方案1】:

你可以试试Array.Resize():

  int[] myArray = new int[] { 1, 2, 3, 4 };
  int myNewSize = 1;

  Array.Resize(ref myArray, myNewSize);

  // Test: 1
  Console.Write(myArray.Length);

【讨论】:

  • 这仍然是一个副本 - 如果您保留原始参考的副本,很容易看到。并不是说真的有其他方法可以使用它,即使在 C++ 中也是如此。
  • Array.Resize 不要就地调整大小。
【解决方案2】:

realloc尝试进行就地调整大小 - 但它保留将整个内容复制到其他地方并返回完全不同的指针的权利。

.NET 的List<T> 类暴露了几乎相同的外部行为——如果您发现自己经常更改数组大小,则无论如何都应该使用它。它对您隐藏了实际的数组引用,以便更改在对同一列表的所有引用中传播。当您从末尾删除项目时,只有列表的长度会发生变化,而内部数组保持不变 - 避免复制。

它不会释放内存(您始终可以使用 Capacity = XXX 显式执行此操作,但这会生成数组的新副本),但话又说回来,除非您正在使用大型阵列,realloc 也没有 - 如果你正在使用大型阵列,yada,yada - 我们去过那里:)

realloc 在 .NET 的内存模型中并没有什么意义——堆会随着时间的推移不断地被收集和压缩。因此,如果您在修剪数组时尝试使用它来避免复制,同时还要保持较低的内存使用量......不要打扰。在下一次堆压缩时,数组上方的整个内存将被移动以填充空白。即使可以执行realloc,与简单地复制数组相比,您拥有的唯一好处是您可以将数组保留在旧堆中 - 无论如何这不一定是您想要的。

【讨论】:

    【解决方案3】:

    BCL 中的数组类型都不支持您想要的。话虽这么说-您可以实现自己的类型来支持您的需求。它可以由标准数组支持,但会实现自己的 Length 和 indexer 属性,这将对您“隐藏”数组的一部分。

    public class MyTruncatableArray<T>
    {
        private T[] _array;
        private int _length;
    
        public MyTruncatableArray(int size)
        {
            _array = new T[size];
            _length = size;
        }
    
        public T this[int index]
        {
            get
            {
                CheckIndex(index, _length);
                return _array[index];
            }
            set
            {
                CheckIndex(index, _length);
                _array[index] = value;
            }
        }
    
        public int Length
        {
            get { return _length; }
            set
            {
                CheckIndex(value);
                _length = value;
            }
        }
    
        private void CheckIndex(int index)
        {
            this.CheckIndex(index, _array.Length);
        }
    
        private void CheckIndex(int index, int maxValue)
        {
            if (index < 0 || index > maxValue)
            {
                throw new ArgumentException("New array length must be positive and lower or equal to original size");
            }
        }
    }
    

    这真的取决于到底需要什么。 (例如,您是否需要截断以便您可以更轻松地从代码中使用它。或者性能/GC/内存消耗是一个问题?如果是后者 - 您是否执行了任何证明标准 Array.Resize 方法不可用的测量你的情况?)

    【讨论】:

    • 数组调整大小只是复制到另一个数组中。见例子。
    • @Alex Zhukovskiy 我没有在我的代码示例中的任何地方使用 Array.Resize。再一次 - 没有标准的 BCL 类型支持您的需求。根据您更新的问题 - 您有两个选择:1)分配和手动管理非托管内存(请参阅 Marshal.AllocHGlobal)和 P/Invoke 到您提到的 C++ 方法(尽管这些方法仍然不能保证)。 2)重写我的简化示例以使用数组数组并根据请求释放//添加内存子段
    • 这就是我要说的。在 GC 之上创建另一个“内存管理”层很奇怪,因为这是使用 .Net 的唯一原因。我认为有一些方法可以破解内部 CLR 结构(EEClasses/Type 引用)以达到预期的结果。
    • 我明白你在说什么。在这种情况下,最好的办法是自己调查 CoreCLR 源 (github.com/dotnet/coreclr) 并尝试查看 GC 如何通过数组类型区分占用内存的长度。这是高度不受支持的,并且将来很可能会中断(类似于如何更改或截断字符串的技巧 - 它们可以工作,但对于不同版本的 .NET,它们可能会导致您的代码意外崩溃)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-26
    • 2011-09-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多