Over the weekend this article inspired me to write an ASCII string implementation that avoids memory allocation during basic operations like Substring
and Trim
. It's still a work-in-progress but I think this is a solid foundation to build off of and wanted some help checking my logic/maths.
The main method of interest is the overload of Substring
that accepts an offset and a count since most other methods are implemented by calling it.
using System;
using System.Text;
namespace ByteTerrace.CSharp.Data
{
/// <summary>
/// Represents text as a series of ASCII characters.
/// </summary>
/// <remarks>
/// Inspired by Christopher Wright's FastString work: https://github.com/dhasenan/FastString.
/// </remarks>
public struct AsciiString : IEquatable<AsciiString>
{
private readonly ArraySegment<byte> m_bytes;
/// <summary>
/// Gets the <see cref="char"/> object at a specified position in this <see cref="AsciiString"/> instance.
/// </summary>
/// <param name="index">A position in the current string.</param>
public char this[int index] {
get {
if ((index < 0) || ((m_bytes.Offset + index) > (m_bytes.Offset + Length))) {
throw new ArgumentOutOfRangeException(nameof(index));
}
return (char)m_bytes.Array[m_bytes.Offset + index];
}
}
/// <summary>
/// Indicates whether this <see cref="AsciiString"/> instance is empty.
/// </summary>
public bool IsEmpty {
get {
return m_bytes.Count == 0;
}
}
/// <summary>
/// Indicates whether this <see cref="AsciiString"/> instance is empty or consists of only whitespace characters.
/// </summary>
public bool IsEmptyOrWhitespace {
get {
return Trim().IsEmpty;
}
}
/// <summary>
/// The number of characters in this <see cref="AsciiString"/> instance.
/// </summary>
public int Length {
get {
return m_bytes.Count;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="AsciiString"/> structure to the value indicated by an <see cref="Array"/> of ASCII bytes.
/// </summary>
/// <param name="bytes">An <see cref="Array"/> of ASCII bytes.</param>
public AsciiString(ArraySegment<byte> bytes) { m_bytes = bytes; }
/// <summary>
/// Initializes a new instance of the <see cref="AsciiString"/> structure to the value indicated by an <see cref="ArraySegment{T}"/> of ASCII bytes.
/// </summary>
/// <param name="bytes">An <see cref="ArraySegment{T}"/> of ASCII bytes.</param>
public AsciiString(byte[] bytes) : this(new ArraySegment<byte>(bytes ?? new byte[0])) { }
/// <summary>
/// Initializes a new instance of the <see cref="AsciiString"/> structure to the value indicated by an <see cref="Array"/> of ASCII bytes,
/// a starting position within that array, and a length.
/// </summary>
/// <param name="bytes">An array of ASCII bytes.</param>
/// <param name="offset">The zero-based starting character position of a substring in bytes.</param>
/// <param name="count">The number of characters in the substring.</param>
public AsciiString(byte[] bytes, int offset, int count) : this(new ArraySegment<byte>(bytes, offset, count)) { }
/// <summary>
/// Initializes a new instance of the <see cref="AsciiString"/> structure to the value indicated by a <see cref="string"/>.
/// </summary>
/// <param name="value">A string value that will be transcoded to ASCII.</param>
public AsciiString(string value) : this(Encoding.ASCII.GetBytes(value ?? string.Empty)) { }
/// <summary>
/// Create a new copy of this <see cref="AsciiString"/> instance.
/// </summary>
public unsafe AsciiString Copy() {
var buffer = new byte[Length];
var count = m_bytes.Offset + Length;
fixed (byte* leftPointer = &buffer[0])
fixed (byte* rightPointer = &m_bytes.Array[m_bytes.Offset]) {
byte* left = leftPointer;
byte* right = rightPointer;
while (31 < count) {
(*(long*)(left + 0)) = (*(long*)(right + 0));
(*(long*)(left + 8)) = (*(long*)(right + 8));
(*(long*)(left + 16)) = (*(long*)(right + 16));
(*(long*)(left + 24)) = (*(long*)(right + 24));
count -= 32;
left += 32;
right += 32;
}
if (15 < count) {
(*(long*)(left + 0)) = (*(long*)(right + 0));
(*(long*)(left + 8)) = (*(long*)(right + 8));
count -= 16;
left += 16;
right += 16;
}
while (0 < count) {
*left = *right;
count--;
left++;
right++;
}
}
return new AsciiString(buffer);
}
/// <summary>
/// Determines whether this instance and another specified <see cref="AsciiString"/> object have the same value.
/// </summary>
/// <param name="other">The <see cref="AsciiString"/> to compare to this instance.</param>
public unsafe bool Equals(AsciiString other) {
var count = Length;
if (count != other.Length) {
return false;
}
else { // shamelessly adapted from http://referencesource.microsoft.com/#mscorlib/system/string.cs,11648d2d83718c5e
fixed (byte* leftPointer = &m_bytes.Array[m_bytes.Offset])
fixed (byte* rightPointer = &other.m_bytes.Array[other.m_bytes.Offset]) {
byte* left = leftPointer;
byte* right = rightPointer;
while (31 < count) {
if ((*(long*)(left + 0) != *(long*)(right + 0))
|| (*(long*)(left + 8) != *(long*)(right + 8))
|| (*(long*)(left + 16) != *(long*)(right + 16))
|| (*(long*)(left + 24) != *(long*)(right + 24))
) { return false; }
count -= 32;
left += 32;
right += 32;
}
if (15 < count) {
if ((*(long*)(left + 0) != *(long*)(right + 0))
|| (*(long*)(left + 8) != *(long*)(right + 8))
) { return false; }
count -= 16;
left += 16;
right += 16;
}
while (0 < count) {
if (*left != *right) {
return false;
}
count--;
left++;
right++;
}
return true;
}
}
}
/// <summary>
/// Determines whether this instance and another specified <see cref="object"/>, which also must be a <see cref="AsciiString"/>, have the same value.
/// </summary>
/// <param name="other">The <see cref="object"/> to compare to this instance.</param>
public override bool Equals(object other) {
var asciiString = other as AsciiString?;
if (asciiString.HasValue) {
return Equals(asciiString.Value);
}
else {
return false;
}
}
/// <summary>
/// Returns the hash code for the underlying <see cref="ArraySegment{T}"/> of this <see cref="AsciiString"/> instance.
/// </summary>
public override int GetHashCode() {
return m_bytes.GetHashCode();
}
/// <summary>
/// Retrieves a substring from this <see cref="AsciiString"/> instance. The slice will be performed left to right and has a specified length.
/// </summary>
/// <param name="count">The number of characters in the substring.</param>
public AsciiString SliceLeft(int count) {
return Substring(0, count);
}
/// <summary>
/// Retrieves a substring from this <see cref="AsciiString"/> instance. The slice will be performed right to left and has a specified length.
/// </summary>
/// <param name="count">The number of characters in the substring.</param>
public AsciiString SliceRight(int count) {
return Substring(Length - count, count);
}
/// <summary>
/// Retrieves a substring from this <see cref="AsciiString"/> instance. The substring starts at a specified character position and has a specified length.
/// </summary>
/// <param name="offset">The zero-based starting character position of a substring in this instance.</param>
/// <param name="count">The number of characters in the substring.</param>
public AsciiString Substring(int offset, int count) {
if ((offset < 0) || ((m_bytes.Offset + offset + count) > (m_bytes.Offset + Length))) {
throw new ArgumentOutOfRangeException(nameof(offset));
}
if (count > m_bytes.Count) {
throw new ArgumentOutOfRangeException(nameof(count));
}
return new AsciiString(m_bytes.Array, m_bytes.Offset + offset, count);
}
/// <summary>
/// Retrieves a substring from this <see cref="AsciiString"/> instance. The substring starts at a specified character position and continues to the end of the string.
/// </summary>
/// <param name="offset">The zero-based starting character position of a substring in this instance.</param>
public AsciiString Substring(int offset) {
return Substring(offset, Length - offset);
}
/// <summary>
/// Converts this <see cref="AsciiString"/> value to its equivalent <see cref="string"/> representation.
/// </summary>
public override string ToString() {
return Encoding.ASCII.GetString(m_bytes.Array, m_bytes.Offset, m_bytes.Count);
}
/// <summary>
/// Removes all leading whitespace characters from this <see cref="AsciiString"/> instance.
/// </summary>
public AsciiString TrimLeft() {
var index = m_bytes.Offset;
var count = Length;
while (0 < count--) {
if (!IsWhitespaceCharacter(m_bytes, index++)) {
break;
}
}
return SliceRight(++count);
}
/// <summary>
/// Removes all trailing whitespace characters from this <see cref="AsciiString"/> instance.
/// </summary>
public AsciiString TrimRight() {
var index = Length - 1;
var count = Length;
while (0 < count--) {
if (!IsWhitespaceCharacter(m_bytes, index--)) {
break;
}
}
return SliceLeft(++count);
}
/// <summary>
/// Removes all leading and trailing whitespace characters from this <see cref="AsciiString"/> instance.
/// </summary>
public AsciiString Trim() {
return TrimLeft().TrimRight();
}
/// <summary>
/// Determines whether two <see cref="AsciiString"/> objects have the same value.
/// </summary>
public static bool operator ==(AsciiString left, AsciiString right) {
return left.Equals(right);
}
/// <summary>
/// Determines whether two <see cref="AsciiString"/> objects have different values.
/// </summary>
public static bool operator !=(AsciiString left, AsciiString right) {
return !left.Equals(right);
}
/// <summary>
/// Initializes a new instance of the <see cref="AsciiString"/> structure to the value indicated by an <see cref="Array"/> of ASCII bytes.
/// </summary>
/// <param name="bytes">An <see cref="Array"/> of ASCII bytes.</param>
public static implicit operator AsciiString(byte[] bytes) {
return new AsciiString(bytes);
}
/// <summary>
/// Initializes a new instance of the <see cref="AsciiString"/> structure to the value indicated by an <see cref="ArraySegment{T}"/> of ASCII bytes.
/// </summary>
/// <param name="bytes">An <see cref="ArraySegment{T}"/> of ASCII bytes.</param>
public static implicit operator AsciiString(ArraySegment<byte> bytes) {
return new AsciiString(bytes);
}
/// <summary>
/// Initializes a new instance of the <see cref="AsciiString"/> structure to the value indicated by a <see cref="string"/>.
/// </summary>
/// <param name="value">A string value that will be transcoded to ASCII.</param>
public static implicit operator AsciiString(string value) {
return new AsciiString(value);
}
private static bool IsWhitespaceCharacter(ArraySegment<byte> bytes, int index) {
var c = bytes.Array[bytes.Offset + index];
if ((c == 0x20) || ((c > 0x08) && (c < 0x0E))) {
return true;
}
else {
return false;
}
}
}
}
Splitter
,Slicer
and what not. \$\endgroup\$