.NET 零开销抽象指南( 七 )

如此一来,我们解决了存储和释放的问题,使用时只需要简单的:
NativeBuffer<int> buf = new(new[] { 1, 2, 3, 4, 5 });// ...buf.Dispose();或者让它在作用域结束时自动释放:
using NativeBuffer<int> buf = new(new[] { 1, 2, 3, 4, 5 });或者干脆不管了,等待 GC 回收时自动调用我们的编写的析构函数,这个时候就会从 ~NativeBuffer 调用 Dispose 方法 。
紧接着,为了能够使用 foreach 进行迭代,我们还需要实现一个 Enumerator,但是为了提升效率并且支持引用,此时我们选择实现自己的 GetEnumerator
首先我们实现一个 NativeBufferEnumerator
public ref struct NativeBufferEnumerator{private unsafe readonly ref T* pointer;private readonly nuint length;private ref T current;private nuint index;public ref T Current{get{unsafe{// 确保指向的内存仍然有效if (pointer == (T*)0){return ref Unsafe.NullRef<T>();}else return ref current;}}}public unsafe NativeBufferEnumerator(ref T* pointer, nuint length){this.pointer = ref pointer;this.length = length;this.index = 0;this.current = ref Unsafe.NullRef<T>();}public bool MoveNext(){unsafe{// 确保没有越界并且指向的内存仍然有效if (index >= length || pointer == (T*)0){return false;}if (Unsafe.IsNullRef(ref current)) current = ref *pointer;else current = ref Unsafe.Add(ref current, 1);}index++;return true;}}然后只需要让 NativeBuffer.GetEnumerator 方法返回我们的实现好的迭代器即可:
public NativeBufferEnumerator GetEnumerator(){unsafe{return new(ref pointer, Length);}}从此,我们便可以轻松零分配地迭代我们的 NativeBuffer 了:
int[] buffer = new[] { 1, 2, 3, 4, 5 };using NativeBuffer<int> nb = new(buffer);foreach (int i in nb) Console.WriteLine(i); // 1 2 3 4 5foreach (ref int i in nb) i++;foreach (int i in nb) Console.WriteLine(i); // 2 3 4 5 6并且由于我们的迭代器中保存着对 NativeBuffer.pointer 的引用,如果 NativeBuffer 被释放了,运行了一半的迭代器也能及时发现并终止迭代:
int[] buffer = new[] { 1, 2, 3, 4, 5 };NativeBuffer<int> nb = new(buffer);foreach (int i in nb){Console.WriteLine(i); // 1nb.Dispose();}结构化数据我们经常会需要存储结构化数据,例如在进行图片处理时,我们经常需要保存颜色信息 。这个颜色可能是直接从文件数据中读取得到的 。那么此时我们便可以封装一个 Color 来代表颜色数据 RGBA:
[StructLayout(LayoutKind.Sequential)]public struct Color : IEquatable<Color>{public byte R, G, B, A;public Color(byte r, byte g, byte b, byte a = 0){R = r;G = g;B = b;A = a;}public override int GetHashCode() => HashCode.Combine(R, G, B, A);public override string ToString() => $"Color {{ R = {R}, G = {G}, B = {B}, A = {A} }}";public override bool Equals(object? other) => other is Color color ? Equals(color) : false;public bool Equals(Color other) => (R, G, B, A) == (other.R, other.G, other.B, other.A);}这么一来我们就有能表示颜色数据的类型了 。但是这么做还不够,我们需要能够和二进制数据或者字符串编写的颜色值相互转换,因此我们编写 SerializeDeserializeParse 方法来进行这样的事情:
[StructLayout(LayoutKind.Sequential)]public struct Color : IParsable<Color>, IEquatable<Color>{public static byte[] Serialize(Color color){unsafe{byte[] buffer = new byte[sizeof(Color)];MemoryMarshal.Write(buffer, ref color);return buffer;}}public static Color Deserialize(ReadOnlySpan<byte> data){return MemoryMarshal.Read<Color>(data);}[DoesNotReturn] private static void ThrowInvalid() => throw new InvalidDataException("Invalid color string.");public static Color Parse(string s, IFormatProvider? provider){if (s.Length is not 7 and not 9 || (s.Length > 0 && s[0] != '#')){ThrowInvalid();}return new(){R = byte.Parse(s[1..3], NumberStyles.HexNumber, provider),G = byte.Parse(s[3..5], NumberStyles.HexNumber, provider),B = byte.Parse(s[5..7], NumberStyles.HexNumber, provider),A = s.Length is 9 ? byte.Parse(s[7..9], NumberStyles.HexNumber, provider) : default};}public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Color result){result = default;if (s?.Length is not 7 and not 9 || (s.Length > 0 && s[0] != '#')){return false;}Color color = new Color();return byte.TryParse(s[1..3], NumberStyles.HexNumber, provider, out color.R)&& byte.TryParse(s[3..5], NumberStyles.HexNumber, provider, out color.G)&& byte.TryParse(s[5..7], NumberStyles.HexNumber, provider, out color.B)&& (s.Length is 9 ? byte.TryParse(s[7..9], NumberStyles.HexNumber, provider, out color.B) : true);}}

经验总结扩展阅读