Ссылки и указатели. Часть 2
Ниже представлен удобный тип, который я создал при переводе сложного кода, манипулирующего указателями, с языка C на язык 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; // поместить 123.4 в arr[5]
var p7 = p0 + 7;
int diff = p7 - p5; // 2
Правда здорово?
Уточнение:
В комментариях несколько людей спросили, почему этот код запрещает указатель «за концом» массива. На самом деле, мой исходный код на языке С, который я портировал, использовал маркер «конца массива» и код, таким образом работал с указателем, указывающим «за конец» массива. Исходная версия класса ArrayPtr поддерживала указатель за последним элементом массива и генерировала исключение при попытке его разыменовывания. Я думал, что эта деталь будет отвлекать от цели этой статьи, поэтому я убрал эту возможность из C#-кода перед его публикацией. Возможно, это было преждевременной оптимизацией.
У меня есть аналогичная обертка для строк, в которой, опять-таки, я разрешаю указывать «за последний элемент», которым будет символ конца строки (null-terminating character) в программе на С. Этот класс также поддерживает распространенные идиомы языка С, такие как “strlen” и тому подобные. Такие типы очень удобны при переносе С-кода на C#; в конце концов, конечно лучше использовать идиомы языка C#, но такие вещи полезны, чтобы быстро получить работающую версию.
------------
(*) Если бы этот тип был открытым, тогда вместо утверждений я бы использовал исключения, ведь открытые классы могут использоваться каким угодно дурацким образом; поскольку это внутренний тип, я могу гарантировать его корректное использование, поэтому я просто использую утверждения.