记录:阅读 C# 中string的源码

发布时间 2023-09-01 19:14:01作者: hellofriland

string

Unsafe.Add

Unsafe.Add 是string中一个常用的方法,它不是用于向某个对象添加元素的,而是用于计算字符在内存中的偏移位置。

Split 是如何运行的

string 的 split 操作是直接进行内存操作实现的,这样可以在不创建大量新字符串副本的情况下,从原始字符串中提取子字符串。

它使用底层的内存操作来提高性能,并且会进行断言检查以确保输入参数的有效性。这有助于减少内存分配和复制操作,提高了字符串操作的效率。

private string InternalSubString(int startIndex, int length)
{
    Debug.Assert(startIndex >= 0 && startIndex <= this.Length, "StartIndex is out of range!");
    Debug.Assert(length >= 0 && startIndex <= this.Length - length, "length is out of range!");

    string result = FastAllocateString(length);


    // 这一行代码的目的是计算从 _firstChar 开始偏移 startIndex 指定的字符数量后的内存位置,并返回该位置的引用。
    Buffer.Memmove(
        elementCount: (uint)length,
        destination: ref result._firstChar,
        source: ref Unsafe.Add(ref _firstChar, (nint)(uint)startIndex /* force zero-extension */));

    return result;
}

Trim是如何运行的

可以看到调用分成了三个阶段:

第一阶段:判断字符串的第一个和最后一个字符是否有空格字符

第二阶段:通过 startend 获取非空字符的位置

第三阶段:使用 InternalSubString 截取字符串返回

public string Trim()
{
    if (Length == 0 || (!char.IsWhiteSpace(_firstChar) && !char.IsWhiteSpace(this[^1])))
    {
        return this;
    }
    return TrimWhiteSpaceHelper(TrimType.Both);
}

private string TrimWhiteSpaceHelper(TrimType trimType)
{
    // end will point to the first non-trimmed character on the right.
    // start will point to the first non-trimmed character on the left.
    int end = Length - 1;
    int start = 0;

    // Trim specified characters.
    if ((trimType & TrimType.Head) != 0)
    {
        for (start = 0; start < Length; start++)
        {
            if (!char.IsWhiteSpace(this[start]))
            {
                break;
            }
        }
    }

    if ((trimType & TrimType.Tail) != 0)
    {
        for (end = Length - 1; end >= start; end--)
        {
            if (!char.IsWhiteSpace(this[end]))
            {
                break;
            }
        }
    }

    return CreateTrimmedString(start, end);
}

private string CreateTrimmedString(int start, int end)
{
    int len = end - start + 1;
    return
      len == Length ? this :
      len == 0 ? Empty :
      InternalSubString(start, len);
}

Replace 是如何实现的

字符匹配算法部分存放在 System.Globalization 中,这里不做分析。

private static string? ReplaceCore(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue, CompareInfo compareInfo, CompareOptions options)
{
    Debug.Assert(!oldValue.IsEmpty);
    Debug.Assert(compareInfo != null);

    var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]);
    result.EnsureCapacity(searchSpace.Length);

    bool hasDoneAnyReplacements = false;

    // 获取到第一个需要替换的字符的索引,并将该索引之前的所有内容添加到result中,再将newValue追加到result中
    // 将已匹配过的 searchSpace 内容切割出去,进入下一轮循环
    // 当IndexOf匹配不到字符时结束循环,将result转换为字符串返回
    while (true)
    {
        // searchSpace:要在其中执行查找和替换操作的只读字符序列。
        // oldValue:要查找并替换的目标子序列。
        // newValue:用于替换目标子序列的新子序列。
        // compareInfo:用于执行文化特定的比较和查找操作的 CompareInfo 对象。
        // options:比较选项,用于指定查找操作的比较方式。
        int index = compareInfo.IndexOf(searchSpace, oldValue, options, out int matchLength);

        if (index < 0 || matchLength == 0)
        {
            break;
        }
        
        result.Append(searchSpace.Slice(0, index));
        result.Append(newValue);

        searchSpace = searchSpace.Slice(index + matchLength);
        
        // 设置 hasDoneAnyReplacements 为 true,表示已经执行了至少一个替换
        hasDoneAnyReplacements = true;
    }

    // 检查是否进行了替换
    // 如果没有进行替换,它会释放 ValueStringBuilder 对象并返回 null,以节省内存。
    if (!hasDoneAnyReplacements)
    {
        result.Dispose();
        return null;
    }

    result.Append(searchSpace);
    return result.ToString();
}

Remove 是如何实现的

Remove方法使用了两次内存操作将字符串的内容移动到了新的string中。

public string Remove(int startIndex, int count)
{
    ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
    ArgumentOutOfRangeException.ThrowIfNegative(count);
    int oldLength = this.Length;
    ArgumentOutOfRangeException.ThrowIfGreaterThan(count, oldLength - startIndex);

    if (count == 0)
        return this;
    int newLength = oldLength - count;
    if (newLength == 0)
        return Empty;

    // 分配新字符串的内存
    string result = FastAllocateString(newLength);
	// 这行代码将从原始字符串的起始位置开始的前startIndex个字符复制到新字符串的起始位置。
    // 这是保留startIndex之前字符的操作。
    Buffer.Memmove(ref result._firstChar, ref _firstChar, (nuint)startIndex);
    // 这行代码将从原始字符串的startIndex + count位置开始的剩余字符复制到新字符串的startIndex位置开始。
    // 保留startIndex + count 之后字符的操作。
    Buffer.Memmove(ref Unsafe.Add(ref result._firstChar, startIndex), ref Unsafe.Add(ref _firstChar, startIndex + count), (nuint)(newLength - startIndex));

    return result;
}

Join 是如何实现的

需要注意的是,在进行 join 的时候,result只被创建了一次,所有拼接都是调用 CopyStringContent 方法进行内存操作,将 value 复制到 result 所拥有的内存上的。

private static string JoinCore(ReadOnlySpan<char> separator, ReadOnlySpan<string?> values)
{
    if (values.Length <= 1)
    {
        return values.IsEmpty ?
            Empty :
            values[0] ?? Empty;
    }

    long totalSeparatorsLength = (long)(values.Length - 1) * separator.Length;
    if (totalSeparatorsLength > int.MaxValue)
    {
        ThrowHelper.ThrowOutOfMemoryException_StringTooLong();
    }
    int totalLength = (int)totalSeparatorsLength;

	// 计算新的 string 所需的总长度
    foreach (string? value in values)
    {
        if (value != null)
        {
            totalLength += value.Length;
            if (totalLength < 0) // Check for overflow
            {
                ThrowHelper.ThrowOutOfMemoryException_StringTooLong();
            }
        }
    }

    if (totalLength == 0)
    {
        return Empty;
    }

	// 根据先前获取的总长度,创建新的 string
    string result = FastAllocateString(totalLength);
    int copiedLength = 0;

    for (int i = 0; i < values.Length; i++)
    {
        if (values[i] is string value)
        {
            int valueLen = value.Length;
            if (valueLen > totalLength - copiedLength)
            {
                copiedLength = -1;
                break;
            }

            // Fill in the value.
            CopyStringContent(result, copiedLength, value);
            copiedLength += valueLen;
        }

        if (i < values.Length - 1)
        {
            // Fill in the separator.
            // Special-case length 1 to avoid additional overheads of CopyTo.
            // This is common due to the char separator overload.

            ref char dest = ref Unsafe.Add(ref result._firstChar, copiedLength);

            if (separator.Length == 1)
            {
                dest = separator[0];
            }
            else
            {
                separator.CopyTo(new Span<char>(ref dest, separator.Length));
            }

            copiedLength += separator.Length;
        }
    }

    // 如果复制的数量刚刚好,那么说明结果是正确的。
    // 如果数量不对,说明发生了一些错误,需要重新进行拼接
    return copiedLength == totalLength ?
        result :
        JoinCore(separator, values.ToArray().AsSpan());
}


private static void CopyStringContent(string dest, int destPos, string src)
{
    Debug.Assert(dest != null);
    Debug.Assert(src != null);
    Debug.Assert(src.Length <= dest.Length - destPos);

    Buffer.Memmove(
        destination: ref Unsafe.Add(ref dest._firstChar, destPos),
        source: ref src._firstChar,
        elementCount: (uint)src.Length);
}

Insert 是如何实现的

接下来的基本都是老一套了,计算长度,根据长度创建 result,使用内存操作将内容分次复制到result里,返回 result

public string Insert(int startIndex, string value)
{
    ArgumentNullException.ThrowIfNull(value);
    ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)startIndex, (uint)Length, nameof(startIndex));

    int oldLength = Length;
    int insertLength = value.Length;

    if (oldLength == 0)
        return value;
    if (insertLength == 0)
        return this;

    // In case this computation overflows, newLength will be negative and FastAllocateString throws OutOfMemoryException
    int newLength = oldLength + insertLength;
    string result = FastAllocateString(newLength);

    Buffer.Memmove(ref result._firstChar, ref _firstChar, (nuint)startIndex);
    Buffer.Memmove(ref Unsafe.Add(ref result._firstChar, startIndex), ref value._firstChar, (nuint)insertLength);
    Buffer.Memmove(ref Unsafe.Add(ref result._firstChar, startIndex + insertLength), ref Unsafe.Add(ref _firstChar, startIndex), (nuint)(oldLength - startIndex));

    return result;
}