创建一个指向另一个数组部分的数组?

5
我有一个包含引用类型元素的巨大数组,我想创建很多其他数组,这些数组实际上只是指向那个大数组的特定部分。
换句话说,我想创建“索引器”或“带长度的指针”。
在C ++中,使用指针很容易做到这一点,并为每个指针分配一个长度,例如创建一个包含指针和长度的结构体。
如何在C#/.NET中实现这一点?
整个重点是避免复制任何内容,我只想获取指向内存中已经存在的数组特定部分的指针。
有什么想法吗?

你尝试过使用 Skip..Take 吗? - Sruti
如果你想的话,可以使用指针。 - Kamil Budziewski
1
数组使用整数进行索引,因此“指向数组中特定位置的指针”显然是一个整数。长度也表示为整数。因此,您在这里谈论一对整数。您可以将它们打包在Tuple<int, int>中,或者如果您喜欢更具描述性的名称,则可以创建自己的struct - Jon
这可能是相关的 https://dev59.com/snE95IYBdhLWcg3wY85l ,但是@Jon描述了我想建议的内容。 - Andrew Savinykh
如果将数据存储为对象数组,则默认情况下始终会通过引用传递数组元素。 (这是C#的默认行为) - naed21
显示剩余2条评论
3个回答

11

Jon 建议使用 ArraySegment<T> ,这可能是您想要的内容。但如果您希望表示指向数组内部的指针(就像在 C++ 中一样),请参考下面的代码。此代码不以任何方式跟踪内部指针的“长度”,但如果需要,很容易添加该功能。

免责声明:此代码不提供任何保证,请自行承担风险。

internal struct ArrayPtr<T>
{
  public static ArrayPtr<T> Null { get { return default(ArrayPtr<T>); } }
  private readonly T[] source;
  private readonly int index;

  private ArrayPtr(ArrayPtr<T> old, int delta)
  {
    this.source = old.source;
    this.index = old.index + delta;
    Debug.Assert(index >= 0);
    Debug.Assert(index == 0 || this.source != null && index < this.source.Length);
  }

  public ArrayPtr(T[] source)
  {
    this.source = source;
    index = 0;
  }

  public bool IsNull()
  {
    return this.source == null;
  }

  public static bool operator <(ArrayPtr<T> a, ArrayPtr<T> b)
  {
    Debug.Assert(Object.ReferenceEquals(a.source, b.source));
    return a.index < b.index;
  }

  public static bool operator >(ArrayPtr<T> a, ArrayPtr<T> b)
  {
    Debug.Assert(Object.ReferenceEquals(a.source, b.source));
    return a.index > b.index;
  }

  public static bool operator <=(ArrayPtr<T> a, ArrayPtr<T> b)
  {
    Debug.Assert(Object.ReferenceEquals(a.source, b.source));
    return a.index <= b.index;
  }

  public static bool operator >=(ArrayPtr<T> a, ArrayPtr<T> b)
  {
    Debug.Assert(Object.ReferenceEquals(a.source, b.source));
    return a.index >= b.index;
  }

  public static int operator -(ArrayPtr<T> a, ArrayPtr<T> b)
  {
    Debug.Assert(Object.ReferenceEquals(a.source, b.source));
    return a.index - b.index;
  }

  public static ArrayPtr<T> operator +(ArrayPtr<T> a, int count)
  {
    return new ArrayPtr<T>(a, +count);
  }

  public static ArrayPtr<T> operator -(ArrayPtr<T> a, int count)
  {
    return new ArrayPtr<T>(a, -count);
  }

  public static ArrayPtr<T> operator ++(ArrayPtr<T> a)
  {
    return a + 1;
  }

  public static ArrayPtr<T> operator --(ArrayPtr<T> a)
  {
    return a - 1;
  }

  public static implicit operator ArrayPtr<T>(T[] x)
  {
    return new ArrayPtr<T>(x);
  }

  public static bool operator ==(ArrayPtr<T> x, ArrayPtr<T> y)
  {
    return x.source == y.source && x.index == y.index;
  }

  public static bool operator !=(ArrayPtr<T> x, ArrayPtr<T> y)
  {
    return !(x == y);
  }

  public override bool Equals(object x)
  {
    if (x == null) return this.source == null;
    var ptr = x as ArrayPtr<T>?;
    if (!ptr.HasValue) return false;
    return this == ptr.Value;
  }

  public override int GetHashCode()
  {
    unchecked
    {
      int hash = this.source == null ? 0 : this.source.GetHashCode();
      return hash + this.index;
    }
  }

  public T this[int index]
  {
    get { return source[index + this.index]; }
    set { source[index + this.index] = value; }
  }
}

现在我们可以做这样的事情:

double[] arr = new double[10];
var p0 = (ArrayPtr<double>)arr;
var p5 = p0 + 5;
p5[0] = 123.4; // sets arr[5] to 123.4
var p7 = p0 + 7;
int diff = p7 - p5; // 2

我很惊讶它有各种我没想到的运算符(尤其是比较运算符),但却没有实现IEnumerable<T> - 我本来以为在一个段上迭代会比那些运算符更常用。如果你把它看作指针,那么这样做是有道理的 - 但如果你试图将其视为对数组的视图,则不太合适。 - Jon Skeet
@JonSkeet:你说得很对。我的意图并不是让这段代码具备完整的功能 - 我想要一个Length属性和IEnumerable<T>,正如你所指出的那样。我打算尽快将一段复杂的C++代码移植到C#,以便我可以在闲暇时重构它。所涉及的代码进行了指针比较、加法和减法,因此我只实现了这些功能。 - Eric Lippert
非常感谢,我想过这样做(创建自己的类),但我希望有一个内置的选项,不会浪费我的时间。Jon建议我使用ArraySegment,使用它会更快吗?我知道我很懒,因为我可以自己测试性能,但我猜想你肯定已经尝试过了。 - SpaceMonkey
@Spacemonkey:你知道你的性能要求是什么,而不是我。试一试,看看吧! - Eric Lippert
结果证明,这种方法比使用ArraySegment要快得多! - SpaceMonkey
显示剩余2条评论

11

听起来你正在寻找类似于 ArraySegment<T> 的东西。与我早先的想法相反,它确实具有索引器并且实现了 IEnumerable<T> 等接口 - 只是使用显式接口实现。

示例代码:

using System;
using System.Collections.Generic;

static class Test
{
    static void Main()
    {
        string[] original = { "The", "quick", "brown", "fox", "jumped", "over",
                "the", "lazy", "dog" };

        IList<string> segment = new ArraySegment<string>(original, 3, 4);
        Console.WriteLine(segment[2]); // over
        foreach (var word in segment)
        {
            Console.WriteLine(word); // fox jumped over the
        }
    }
}

编辑:如评论中所述,ArraySegment<T> 在 .NET 4.5 中才真正“完全可用”。.NET 4版没有实现任何接口。


这个结构体 ArraySegment<> 在 .NET 4.5 中得到了很大的扩展。在 4.5 之前,它缺乏很多“自然”的功能。 - Jeppe Stig Nielsen
@JeppeStigNielsen:啊,那就很有道理了。谢谢,我会记住的。 - Jon Skeet
非常感谢,我没想到会从你那里得到答案!不过,我有一个问题,使用ArraySegment的索引器是否会带来任何性能损失? - SpaceMonkey
1
@Spacemonkey:是的,我预计会有性能损失。因为接口是显式实现的,所以你必须通过接口进行操作,这意味着要进行虚函数调用。数组元素访问可能会更快。然而,我预计大多数应用程序实际上不会在这种访问方式上出现瓶颈。像往常一样,设定一些性能标准,然后进行测试。 - Jon Skeet

1
您可以使用LINQ:
yourArray.Skip(startIndex).Take(numberToTake)

查询是惰性评估的。


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