はじめに
昨今は文字列を UTF-8 バイト列でそのまま扱いたい需要が高まりまして、UTF-8 関係の標準ライブラリの機能も充実してきました。
というわけで今回は UTF-8 文字列用の StringBuilder
を作ってみます。
サンプルコード
InlineUtf8StringBuilder.cs
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
/// <summary>
/// 内部バッファに Span を使用する UTF-8 StringBuilder
/// バッファ長を超えて Append すると新しいバッファを確保する
/// </summary>
[DebuggerDisplay("{Span}")]
[CollectionBuilder(typeof(InlineUtf8StringBuilder), nameof(Create))]
public ref struct InlineUtf8StringBuilder
{
// NOTE: インラインバッファの長さ。コンパイル定数
// 大きくしすぎるとスタックオーバーフローの危険がある
private const int BufferLength = 128;
[InlineArray(BufferLength)]
private struct InlineBuffer
{
internal byte Value;
}
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_length")]
private static extern ref int InlineBufferLength(ref Span<byte> span);
private InlineBuffer _buffer;
private byte[]? _array;
private int _length;
/// <summary>
/// 文字列長
/// </summary>
public readonly int Length
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this._length;
}
/// <summary>
/// Span
/// </summary>
public readonly Span<byte> Span
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (this._array is not null)
return this._array.AsSpan(0, this._length);
var result = new Span<byte>(ref Unsafe.AsRef(in this._buffer.Value));
InlineBufferLength(ref result) = this._length;
return result;
}
}
/// <summary>
/// 内部配列長
/// </summary>
public readonly int Capacity
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this._array is not null ? this._array.Length : BufferLength;
}
private readonly Span<byte> NextSpan
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (this._array is not null)
return this._array.AsSpan(this._length);
var result = new Span<byte>(ref Unsafe.AsRef(in this._buffer.Value));
InlineBufferLength(ref result) = BufferLength;
return result[this._length..];
}
}
private readonly Span<byte> Buffer
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (this._array is not null)
return this._array.AsSpan();
var result = new Span<byte>(ref Unsafe.AsRef(in this._buffer.Value));
InlineBufferLength(ref result) = BufferLength;
return result;
}
}
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="capacity"></param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public InlineUtf8StringBuilder(int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
if (capacity > BufferLength)
this._array = new byte[capacity];
this._length = 0;
}
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="value"></param>
public InlineUtf8StringBuilder(scoped ReadOnlySpan<byte> value) => this.Append(value);
private unsafe ref InlineUtf8StringBuilder RefThis
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#pragma warning disable
get => ref this;
#pragma warning restore
}
/// <summary>
/// 文字列化
/// </summary>
/// <returns></returns>
public readonly override string ToString() => Encoding.UTF8.GetString(this.Span);
/// <summary>
/// バッファをクリア
/// </summary>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder Clear()
{
this._length = default;
return ref this.RefThis;
}
/// <summary>
/// 文字列化してバッファをクリア
/// </summary>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ToStringAndClear()
{
var result = this.ToString();
this.Clear();
return result;
}
/// <summary>
/// 16 以上で指定数以上の、最小の 2 の階乗を取得
/// </summary>
/// <param name="min"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
internal static int GetCapacity(int min)
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(min, 1073741824);
var result = 16;
for (int n = 0; n < 26 && result < min; ++n)
result <<= 1;
return result;
}
[SkipLocalsInit]
[MethodImpl(MethodImplOptions.NoInlining)]
private void SizeUp(int next = 0)
{
if (next is 0)
next = this.Capacity * 2;
var capacity = GetCapacity(next);
Span<byte> span = this._array is not null ? this._array.AsSpan() : this._buffer;
var newArray = new byte[capacity];
span.CopyTo(newArray);
this._array = newArray;
}
/// <summary>
/// 文字列を追加
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder Append(scoped ReadOnlySpan<byte> value)
{
var max = this._length + value.Length;
if (max > this.Capacity)
this.SizeUp(max);
value.CopyTo(this.NextSpan);
this._length = max;
return ref this.RefThis;
}
/// <summary>
/// 文字列を追加
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder Append(scoped ReadOnlySpan<char> value)
{
int written;
while (!Encoding.UTF8.TryGetBytes(value, this.NextSpan, out written))
this.SizeUp();
this._length += written;
return ref this.RefThis;
}
/// <summary>
/// 改行
/// </summary>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder AppendLine() => ref this.Append(Environment.NewLine);
/// <summary>
/// 末尾に文字列を追加して改行
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder AppendLine(scoped ReadOnlySpan<byte> value) => ref this.Append(value).AppendLine();
/// <summary>
/// 末尾に文字列を追加して改行
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public ref InlineUtf8StringBuilder AppendLine(scoped ReadOnlySpan<char> value) => ref this.Append(value).AppendLine();
/// <summary>
/// 末尾に文字を追加
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="format"></param>
/// <param name="provider"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder Append<T>(scoped T value, scoped ReadOnlySpan<char> format = default, IFormatProvider? provider = null) where T : IUtf8SpanFormattable, allows ref struct
{
int count;
while (!value.TryFormat(this.NextSpan, out count, format, provider))
this.SizeUp();
this._length += count;
return ref this.RefThis;
}
/// <summary>
/// 末尾に文字を追加して改行
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="format"></param>
/// <param name="provider"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder AppendLine<T>(scoped T value, scoped ReadOnlySpan<char> format = default, IFormatProvider? provider = null) where T : IUtf8SpanFormattable, allows ref struct
=> ref this.Append<T>(value, format, provider).AppendLine();
/// <summary>
/// 指定位置に文字を追加
/// </summary>
/// <param name="index"></param>
/// <param name="c"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder Insert(int index, byte c)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
var size = this._length;
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, size);
if (this.Length == this.Capacity)
{
this.SizeUp(size + 1);
}
this._length = size + 1;
var destination = this.Span[(index + 1)..];
var source = this.Span.Slice(index, destination.Length);
for (int n = 0; n < destination.Length; ++n)
destination[n] = source[n];
this._buffer[index] = c;
return ref this.RefThis;
}
/// <summary>
/// 指定位置に文字列を追加
/// </summary>
/// <param name="index"></param>
/// <param name="span"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder Insert(int index, scoped ReadOnlySpan<byte> span)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThan(index, this._length);
var size = this.Length;
var newLength = this._length + span.Length;
if (this.Capacity < newLength)
this.SizeUp(newLength);
this._length = newLength;
var len = size - index;
var source = this.Span.Slice(index, len);
ref var destination = ref MemoryMarshal.GetReference(this.Span[(index + span.Length)..]);
for (int n = source.Length - 1; n >= 0; --n)
Unsafe.Add(ref destination, n) = source[n];
var newDestination = this.Span.Slice(index, span.Length);
span.CopyTo(newDestination);
return ref this.RefThis;
}
/// <summary>
/// 指定位置の文字を削除
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder RemoveAt(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this._length);
this._length--;
if (index < this._length)
{
for (int n = index; n < this._length; ++n)
this._buffer[n] = this._buffer[n + 1];
}
return ref this.RefThis;
}
/// <summary>
/// 指定位置の文字列を削除
/// </summary>
/// <param name="index"></param>
/// <param name="length"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder Remove(int index, int length)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThan(index, this._length - 1);
ArgumentOutOfRangeException.ThrowIfNegative(length);
ArgumentOutOfRangeException.ThrowIfGreaterThan(index + length, this._length);
var len = this._length - index - length;
ref var ptr = ref MemoryMarshal.GetReference(this.Span);
for (int n = 0; n < len; ++n)
Unsafe.Add(ref ptr, index + n) = Unsafe.Add(ref ptr, index + n + length);
this._length -= length;
return ref this.RefThis;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RemoveInsert(int index, int oldValueLength, scoped ReadOnlySpan<byte> newValue)
{
var shift = newValue.Length - oldValueLength;
var buffer = this.Buffer;
var source = buffer[(index + oldValueLength)..];
ref var destination = ref MemoryMarshal.GetReference(buffer[(index + oldValueLength + shift)..]);
for (int n = 0; n < source.Length; ++n)
Unsafe.Add(ref destination, n) = source[n];
newValue.CopyTo(buffer[index..]);
this._length += shift;
}
/// <summary>
/// 置き換え
/// </summary>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder Replace(scoped ReadOnlySpan<byte> oldValue, scoped ReadOnlySpan<byte> newValue)
{
var index = 0;
var span = this.Buffer;
Label:
var find = span.IndexOf(oldValue);
if (find < 0) return ref this.RefThis;
index += find;
this.RemoveInsert(index, oldValue.Length, newValue);
if (index >= this._length) return ref this.RefThis;
span = this.Buffer[index..];
goto Label;
}
/// <summary>
/// 削除
/// </summary>
/// <param name="oldValue"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref InlineUtf8StringBuilder ReplaceEmpty(scoped ReadOnlySpan<byte> oldValue)
{
var index = 0;
var span = this.Buffer;
Label:
var find = span.IndexOf(oldValue);
if (find < 0) return ref this.RefThis;
index += find;
this.Remove(index, oldValue.Length);
if (index >= this._length) return ref this.RefThis;
span = this.Buffer[index..];
goto Label;
}
/// <summary>
/// Span への暗黙の変換
/// </summary>
/// <param name="value"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Span<byte>(in InlineUtf8StringBuilder value) => value.Span;
/// <summary>
/// ReadOnlySpan への暗黙の変換
/// </summary>
/// <param name="value"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator ReadOnlySpan<byte>(in InlineUtf8StringBuilder value) => value.Span;
/// <summary>
/// 指定長追加し指定長のバッファを取得
/// </summary>
/// <param name="length"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> AppendRange(int length)
{
ArgumentOutOfRangeException.ThrowIfNegative(length);
if (length is 0) return default;
if (this.NextSpan.Length < length)
this.SizeUp(this._length + length);
var result = this.NextSpan[..length];
this._length += length;
result.Clear();
return result;
}
/// <summary>
/// 要素を列挙
/// </summary>
/// <returns></returns>
// NOTE: メンバーを候補に表示しないようにする属性
// 同じアセンブリからアクセスする場合は表示される
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Span を使用してください。")]
public Span<byte>.Enumerator GetEnumerator() => this.Span.GetEnumerator();
internal static InlineUtf8StringBuilder Create(scoped ReadOnlySpan<byte> span) => new(span);
}
テストコード
using Xunit;
public class _InlineUtf8StringBuilderTest()
{
[Fact]
void HowToUse()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("PI="u8);
// 数値型(System.IUtf8SpanFormattable)をサポート
builder.Append(3.14);
Assert.Equal("PI=3.14"u8, builder);
builder.Clear();
// 補間文字列(※最適化されておらず ReadOnlySpan<char> を経由)
builder.Append($"PI={3.14}");
Assert.Equal("PI=3.14"u8, builder);
// 暗黙的な変換
Span<byte> _ = builder;
// 内部バッファを超える場合、new byte[] でヒープにバッファが作成される
builder.Clear();
var capacity = builder.Capacity;
builder.Append(stackalloc byte[129]);
Assert.True(builder.Capacity > capacity);
// メソッド連結可能
builder.Clear().Append("decimal:").Append(1.0M);
Assert.Equal("decimal:1.0"u8, builder);
}
[Fact]
void HowToUseUtf8()
{
var x = 100;
var y = Math.PI;
var buffer = (stackalloc byte[64]);
System.Text.Unicode.Utf8.TryWrite(buffer, $"x: {x:0.0}, y: {y:0.00}", out var written);
var utf8text = buffer[..written];
Assert.Equal("x: 100.0, y: 3.14"u8, utf8text);
}
[Fact]
void TestConstructor()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder(-1));
Assert.Equal([], new InlineUtf8StringBuilder().Span);
Assert.Equal("123"u8, new InlineUtf8StringBuilder("123"u8));
}
[Fact]
void LengthSpan()
{
var builder = new InlineUtf8StringBuilder();
Assert.Equal(0, builder.Length);
Assert.Equal(""u8, builder.Span);
builder.Append("abc"u8);
Assert.Equal(3, builder.Length);
Assert.Equal("abc"u8, builder.Span);
}
[Fact]
void Clear()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("abc"u8);
builder.Clear();
Assert.Equal(""u8, builder.Span);
}
[Fact]
void ToStringAndClear()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("abc"u8);
Assert.Equal("abc", builder.ToStringAndClear());
Assert.Equal(""u8, builder.Span);
}
[Fact]
void BuilderToString()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("abc"u8);
Assert.Equal("abc", builder.ToString());
}
[Fact]
void Append_String()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("abc");
Assert.Equal("abc"u8, builder.Span);
}
[Fact]
void Append_ReadOnlySpan()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("abc"u8);
Assert.Equal("abc"u8, builder.Span);
}
[Fact]
void SizeUp()
{
var builder = new InlineUtf8StringBuilder();
Assert.Equal(128, builder.Capacity);
var capacity = builder.Capacity;
builder.Append(stackalloc byte[129]);
Assert.True(capacity < builder.Capacity);
}
[Fact]
void AppendLine()
{
var builder = new InlineUtf8StringBuilder();
builder.AppendLine();
Assert.Equal(Environment.NewLine, builder.ToString());
}
[Fact]
void AppendLine_String()
{
var builder = new InlineUtf8StringBuilder();
builder.AppendLine("abc");
Assert.Equal("abc" + Environment.NewLine, builder.ToString());
}
[Fact]
void Append_T()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("n=");
builder.Append(123456789);
Assert.Equal("n=123456789"u8, builder.Span);
}
[Fact]
void AppendLine_T()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("n=");
builder.AppendLine(123456789);
Assert.Equal("n=123456789" + Environment.NewLine, builder.ToString());
}
[Fact]
void Insert()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder().Insert(-1, (byte)'a'));
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder("abc"u8).Insert(5, (byte)'a'));
var builder = new InlineUtf8StringBuilder();
builder.Append("13");
builder.Insert(1, (byte)'2');
Assert.Equal("123"u8, builder.Span);
}
[Fact]
void InsertRange()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder().Insert(-1, "123"u8));
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder("ab"u8).Insert(5, "123"u8));
var builder = new InlineUtf8StringBuilder();
builder.Append("123890"u8);
builder.Insert(3, "4567"u8);
Assert.Equal("1234567890"u8, builder.Span);
}
[Fact]
void RemoveAt()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder().RemoveAt(-1));
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder().Remove(0, 1));
var builder = new InlineUtf8StringBuilder();
builder.Append("123"u8);
builder.RemoveAt(1);
Assert.Equal("13"u8, builder.Span);
}
[Fact]
void RemoveAtRange()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder().Remove(-1, 1));
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder().Remove(1, 1));
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder(stackalloc byte[10]).Remove(0, 11));
Assert.Throws<ArgumentOutOfRangeException>(() => new InlineUtf8StringBuilder(stackalloc byte[10]).Remove(10, 5));
var builder = new InlineUtf8StringBuilder();
builder.Append("123456"u8);
builder.Remove(1, 2);
Assert.Equal("1456"u8, builder.Span);
}
[Fact]
void ImplicitOperatorSpan()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("abc"u8);
Span<byte> span = builder;
Assert.Equal("abc"u8, span);
}
[Fact]
void ImplicitOperatorReadOnlySpan()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("abc"u8);
ReadOnlySpan<byte> span = builder;
Assert.Equal("abc"u8, span);
}
[Fact]
void RefThis()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("abc"u8).Append("def"u8);
Assert.Equal("abcdef"u8, builder.Span);
}
[Fact]
void ReplaceEmpty()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("1234567890"u8);
builder.ReplaceEmpty("123"u8);
Assert.Equal("4567890"u8, builder);
builder = new InlineUtf8StringBuilder();
builder.Append("1234567890"u8);
builder.ReplaceEmpty("4567"u8);
Assert.Equal("123890"u8, builder);
builder = new InlineUtf8StringBuilder();
builder.Append("1234567890"u8);
builder.ReplaceEmpty("567890"u8);
Assert.Equal("1234"u8, builder);
}
[Fact]
void Replace()
{
var builder = new InlineUtf8StringBuilder();
builder.Append("12345"u8);
builder.Replace("123"u8, "777"u8);
Assert.Equal("77745"u8, builder);
builder = new InlineUtf8StringBuilder();
builder.Append("123abc456"u8);
builder.Replace("123"u8, "777"u8);
Assert.Equal("777abc456"u8, builder);
builder = new InlineUtf8StringBuilder();
builder.Append("123abc456"u8);
builder.Replace("456"u8, "7777"u8);
Assert.Equal("123abc7777"u8, builder);
}
[Fact]
void GetEnumerator()
{
var builder = new InlineUtf8StringBuilder("123"u8);
var got = new List<byte>();
#pragma warning disable CS0618
foreach (var n in builder)
got.Add(n);
#pragma warning restore CS0618
Assert.Equal("123"u8, [.. got]);
}
[Fact]
void Create()
{
InlineUtf8StringBuilder builder = [.. "12345"u8];
Assert.Equal("12345"u8, builder);
}
[Fact]
void GetCapacity()
{
Assert.Equal(16, InlineUtf8StringBuilder.GetCapacity(-1));
Assert.Equal(16, InlineUtf8StringBuilder.GetCapacity(0));
Assert.Equal(16, InlineUtf8StringBuilder.GetCapacity(15));
Assert.Equal(16, InlineUtf8StringBuilder.GetCapacity(16));
Assert.Equal(32, InlineUtf8StringBuilder.GetCapacity(17));
Assert.Equal(256, InlineUtf8StringBuilder.GetCapacity(255));
Assert.Equal(16384, InlineUtf8StringBuilder.GetCapacity(10000));
Assert.Equal(1073741824, InlineUtf8StringBuilder.GetCapacity(1073741824));
Assert.Throws<ArgumentOutOfRangeException>(() => InlineUtf8StringBuilder.GetCapacity(1073741824 + 1));
Assert.Throws<ArgumentOutOfRangeException>(() => InlineUtf8StringBuilder.GetCapacity(int.MaxValue));
}
[Fact]
void Capacity()
{
var builder = new InlineUtf8StringBuilder();
Assert.Equal(128, builder.Capacity);
builder.Append(stackalloc byte[129]);
Assert.Equal(256, builder.Capacity);
}
[Fact]
void AppendRange()
{
{
var builder = new InlineUtf8StringBuilder();
var got = builder.AppendRange(2);
Assert.Equal(stackalloc byte[2], got);
}
{
var builder = new InlineUtf8StringBuilder("123"u8);
builder.AppendRange(3).Fill((byte)'4');
Assert.Equal("123444"u8, builder);
}
}
}
使い方
var builder = new InlineUtf8StringBuilder();
builder.Append("PI="u8);
// 数値型(System.IUtf8SpanFormattable)をサポート
builder.Append(3.14);
Assert.Equal("PI=3.14"u8, builder);
builder.Clear();
// 補間文字列(※最適化されておらず ReadOnlySpan<char> を経由)
builder.Append($"PI={3.14}");
Assert.Equal("PI=3.14"u8, builder);
// 暗黙的な変換
Span<byte> _ = builder;
// 内部バッファを超える場合、new byte[] でヒープにバッファが作成される
builder.Clear();
var capacity = builder.Capacity;
builder.Append(stackalloc byte[129]);
Assert.True(builder.Capacity > capacity);
// メソッド連結可能
builder.Clear().Append("decimal:").Append(1.0M);
Assert.Equal("decimal:1.0"u8, builder);
IUtf8SpanFormattable
public void Append<T>(
scoped T value,
scoped ReadOnlySpan<char> format = default,
IFormatProvider? provider = null)
where T : IUtf8SpanFormattable, allows ref struct
{
int count;
while (!value.TryFormat(this.NextSpan, out count, format, provider))
this.SizeUp();
this._length += count;
return ref this.RefThis;
}
- 今回のメイン部分で、
TryFormat()
で書き込んでいます - バッファ長が足りない場合はバッファ長を確保します
- ジェネリックメソッドにしてボックス化を回避しています
- ジェネリックの制約で
ref
構造体を許容しています
InlineArray
System.Runtime.CompilerServices.InlineArrayAttribute
属性 を使用して、内部的にバッファを確保しています。これは値渡ししたときにおかしな挙動をするので注意が必要です。
関連
文字列補間(今回は未実装)
System.Text.Unicode.Utf8
クラス によって実現できそうですが、これは通常の文字列補間(ReadOnlySpan<char>
)を UTF-8 に変換すればいいような気がするため、今回は見送ります。
var x = 100;
var y = Math.PI;
var buffer = (stackalloc byte[64]);
System.Text.Unicode.Utf8.TryWrite(buffer, $"x: {x:0.0}, y: {y:0.00}", out var written);
var utf8text = buffer[..written];
Assert.Equal("x: 100.0, y: 3.14"u8, utf8text);
おわりに
今回はUTF-8 エンコード用の StringBuilder を作ってみました。
パフォーマンス比較は次回やります。