From c1a71b806c0db3f7b4f788264347402da5344567 Mon Sep 17 00:00:00 2001 From: Antyss77 Date: Sat, 8 Aug 2026 00:01:52 +0200 Subject: [PATCH] perf: use ArrayPool for large buffers, simplify null-forwarding via AsSpan (Sebastian's feedback) --- StringExtension/StringExtension.cs | 135 ++++++++++++++++++----------- 1 file changed, 84 insertions(+), 51 deletions(-) diff --git a/StringExtension/StringExtension.cs b/StringExtension/StringExtension.cs index a5cb5d0..e4bfa4a 100644 --- a/StringExtension/StringExtension.cs +++ b/StringExtension/StringExtension.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Buffers; +using System.Text.RegularExpressions; namespace StringExtension; @@ -7,6 +8,12 @@ namespace StringExtension; /// public static partial class StringExtension { + /// + /// Above this length, buffers are rented from instead + /// of stack-allocated, to avoid excessive stack usage for large inputs. + /// + private const int StackAllocThreshold = 256; + /// /// Represents a regular expression that can be used to validate an email address. /// @@ -27,6 +34,7 @@ public static partial class StringExtension /// The input string. /// An array of characters to remove. /// A new string with specified characters removed. + /// Returns if is . public static string RemoveCharacters(this string input, char[] charactersToRemove) { if (string.IsNullOrEmpty(input) || charactersToRemove is null || charactersToRemove.Length == 0) @@ -50,18 +58,31 @@ public static string RemoveCharacters(this ReadOnlySpan input, ReadOnlySpa return input.ToString(); } - Span buffer = input.Length <= 256 ? stackalloc char[input.Length] : new char[input.Length]; - var count = 0; + char[]? pooledBuffer = null; + Span buffer = (uint)input.Length <= StackAllocThreshold + ? stackalloc char[input.Length] + : (pooledBuffer = ArrayPool.Shared.Rent(input.Length)); - foreach (var c in input) + try + { + var count = 0; + foreach (var c in input) + { + if (charactersToRemove.IndexOf(c) < 0) + { + buffer[count++] = c; + } + } + + return new string(buffer[..count]); + } + finally { - if (charactersToRemove.IndexOf(c) < 0) + if (pooledBuffer is not null) { - buffer[count++] = c; + ArrayPool.Shared.Return(pooledBuffer); } } - - return new string(buffer[..count]); } /// @@ -112,11 +133,6 @@ public static bool IsValidPhoneNumber(this ReadOnlySpan phoneNumber) /// The number of occurrences of the substring in the input string. public static int CountSubstring(this string input, string substring) { - if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(substring)) - { - return 0; - } - return CountSubstring(input.AsSpan(), substring.AsSpan()); } @@ -142,6 +158,7 @@ public static int CountSubstring(this ReadOnlySpan input, ReadOnlySpan /// The input to reverse words. /// The input string with the order of words reversed. + /// Returns if is . public static string ReverseWords(this string input) { if (string.IsNullOrEmpty(input)) @@ -149,8 +166,6 @@ public static string ReverseWords(this string input) return input; } - // string.Create needs the length up front, and it is preserved by a - // simple word swap around single-space separators. return string.Create(input.Length, input, static (span, source) => { foreach (Range word in source.AsSpan().Split(' ')) @@ -179,7 +194,7 @@ public static string ReverseWords(this string input) /// true if the string is a palindrome; otherwise, false. public static bool IsPalindrome(this string input) { - return !string.IsNullOrEmpty(input) && IsPalindrome(input.AsSpan()); + return IsPalindrome(input.AsSpan()); } /// @@ -231,11 +246,6 @@ public static bool IsPalindrome(this ReadOnlySpan input) /// The number of letters in the input string. public static int CountLetters(this string input) { - if (string.IsNullOrEmpty(input)) - { - return 0; - } - return CountLetters(input.AsSpan()); } @@ -263,6 +273,7 @@ public static int CountLetters(this ReadOnlySpan input) /// /// The input string. /// A new string with duplicate characters removed. + /// Returns if is . public static string RemoveDuplicateCharacters(this string input) { if (string.IsNullOrEmpty(input)) @@ -286,19 +297,33 @@ public static string RemoveDuplicateCharacters(this ReadOnlySpan input) return string.Empty; } - Span buffer = input.Length <= 256 ? stackalloc char[input.Length] : new char[input.Length]; - var seen = new HashSet(input.Length); - var count = 0; + char[]? pooledBuffer = null; + Span buffer = (uint)input.Length <= StackAllocThreshold + ? stackalloc char[input.Length] + : (pooledBuffer = ArrayPool.Shared.Rent(input.Length)); - foreach (var c in input) + try { - if (seen.Add(c)) + var seen = new HashSet(input.Length); + var count = 0; + + foreach (var c in input) { - buffer[count++] = c; + if (seen.Add(c)) + { + buffer[count++] = c; + } } - } - return new string(buffer[..count]); + return new string(buffer[..count]); + } + finally + { + if (pooledBuffer is not null) + { + ArrayPool.Shared.Return(pooledBuffer); + } + } } /// @@ -308,11 +333,6 @@ public static string RemoveDuplicateCharacters(this ReadOnlySpan input) /// The input string converted to camel case. public static string ToCamelCase(this string input) { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } - return ToCamelCase(input.AsSpan()); } @@ -328,30 +348,43 @@ public static string ToCamelCase(this ReadOnlySpan input) return string.Empty; } - Span buffer = input.Length <= 256 ? stackalloc char[input.Length] : new char[input.Length]; - - var count = 0; - var shouldCapitalize = false; + char[]? pooledBuffer = null; + Span buffer = (uint)input.Length <= StackAllocThreshold + ? stackalloc char[input.Length] + : (pooledBuffer = ArrayPool.Shared.Rent(input.Length)); - foreach (var c in input) + try { - if (char.IsWhiteSpace(c) || c == '_') + var count = 0; + var shouldCapitalize = false; + + foreach (var c in input) { - shouldCapitalize = true; - continue; + if (char.IsWhiteSpace(c) || c == '_') + { + shouldCapitalize = true; + continue; + } + + buffer[count++] = shouldCapitalize ? char.ToUpperInvariant(c) : char.ToLowerInvariant(c); + shouldCapitalize = false; } - buffer[count++] = shouldCapitalize ? char.ToUpperInvariant(c) : char.ToLowerInvariant(c); - shouldCapitalize = false; - } + // The very first character is always lowercase, even if the input + // started with a separator (e.g. "_hello" gives "hello", not "Hello"). + if (count > 0) + { + buffer[0] = char.ToLowerInvariant(buffer[0]); + } - // The very first character is always lowercase, even if the input - // started with a separator (e.g. "_hello" gives "hello", not "Hello"). - if (count > 0) + return new string(buffer[..count]); + } + finally { - buffer[0] = char.ToLowerInvariant(buffer[0]); + if (pooledBuffer is not null) + { + ArrayPool.Shared.Return(pooledBuffer); + } } - - return new string(buffer[..count]); } } \ No newline at end of file