StringWeaver 2.0.0
dotnet add package StringWeaver --version 2.0.0
NuGet\Install-Package StringWeaver -Version 2.0.0
<PackageReference Include="StringWeaver" Version="2.0.0" />
<PackageVersion Include="StringWeaver" Version="2.0.0" />
<PackageReference Include="StringWeaver" />
paket add StringWeaver --version 2.0.0
#r "nuget: StringWeaver, 2.0.0"
#:package StringWeaver@2.0.0
#addin nuget:?package=StringWeaver&version=2.0.0
#tool nuget:?package=StringWeaver&version=2.0.0
The StringWeaver package exposes a custom high-performance builder for strings with a mutable, directly accessible buffer and a versatile API for manipulating the contents.
Note:
StringWeaveris not a drop-in replacement forStringBuilder. Its single contiguous buffer enablesSpan<char>access and in-place regex operations, but growing copies the entire buffer. See the wiki Examples page for guidance on when to chooseStringWeavervsStringBuilder.
Consumption
The assembly multi-targets netstandard2.0 and several newer .NETs to support performance-focused APIs introduced in later versions:
- Core functionality is exposed in the
netstandard2.0compilation, meaning any conforming project platform can use it. - A dependency on
PCRE.NETis used across all targets to facilitate regex operations onStringWeaverwithout requiring allocations for match details. This is the primary regex engine. - Several quality-of-life APIs are also introduced in their respective compilations, such as support for
ISpanFormattableon>= net6.0. - For
>= net7.0, for theReplace*andEnumerateIndicesOf*methods that take aPcreRegex, analogous methods that take aRegexinstance and utilize the Span-based APIs introduced inSystem.Text.RegularExpressionsare also exposed. net8.0introduced manySpan-based APIs throughout the entire .NET ecosystem, allowing streamlined code paths on>= net8.0.
Neither PCRE.NET nor System.Text.RegularExpressions expose APIs that produce allocating match-details objects from a Span input. As a result, the traditional MatchEvaluator delegate pattern is not supported. Instead, StringWeaver exposes the StringWeaverWriter delegate (void(Span<char> buffer, ReadOnlySpan<char> match)) together with a bufferSize parameter, which allows dynamically generating replacement content with zero allocations.
Variants
For some usage examples, refer to the wiki Examples page.
General purpose
Namespace: global::StringWeaver
StringWeaver: Default and most versatile implementation. Offers the full API surface and sources its backing buffers bynew'ing and resizingchar[]s on demand.- The default
StringWeavershould be the first choice for the ordinary consumer. Where profiling shows that a different implementation would be more beneficial, consider the alternatives described below.
- The default
UnsafeStringWeaver: Implements aStringWeaverthat sources its backing buffers from unmanaged instead of managed memory. This has the benefit of not causing any GC pressure while exposing exactly the same APIs as the defaultStringWeaver.- Consider using this if you frequently create
StringWeavers with capacities nearint.MaxValueor instances which are very long-lived. - ⚠
UnsafeStringWeaverimplementsIDisposable(StringWeaverdoes not). Not disposing of instances ofUnsafeStringWeaveris a memory leak.
- Consider using this if you frequently create
Specialized
Namespace: global::StringWeaver.Specialized
PooledStringWeaver: Implements aStringWeaverthat sources its backing buffers fromArrayPool<char>.Shared(or your own passed implementation ofArrayPool<char>). This allows keeping GC pressure low while still using managed memory.- Consider using this if you frequently create and dispose of
StringWeavers with non-trivial capacities (e.g. a few dozen kB or more). - ⚠
PooledStringWeaverimplementsIDisposable(StringWeaverdoes not). Not disposing of instances ofPooledStringWeavercauses the buffer backing it to be lost to the pool, which is a memory leak and degrades performance of every other user of thatArrayPool<char>in your application.
- Consider using this if you frequently create and dispose of
WrappingStringWeaver: A truly zero-allocation wrapper for existing buffers that otherwise offer the full capabilities of the baseStringWeaverimplementation.- Use this any time you already have some memory you want to treat as a
charbuffer and want to avoid copying at all costs. - ⚠
WrappingStringWeaveronly pins memory for you when specifically asked to do so (and only when given achar[]orMemory<char>). Given aSpan<char>or a pointer (whether managed or unmanaged), it is the caller's responsibility to ensure the memory remains valid and accessible for the lifetime of theWrappingStringWeaverinstance. Changing the contents of the memory outside of theWrappingStringWeaverinstance should be done with caution but is generally safe (under the same constraints as the defaultStringWeaverimplementation, e.g.EnumerateIndicesOfref structs ensuring the buffer wasn't modified during enumeration). See this variant's wiki page for more details and usage examples. - While all constructors take buffers or pointers typed as
charin some form, you can reinterpret any memory ascharand use it withWrappingStringWeaver. Pinned memory reinterpreted usingUnsafe.As<TFrom, TTo>(ref TFrom)is a good example of this. - ⚠
WrappingStringWeaverimplementsIDisposable(StringWeaverdoes not). If you used one of the overloads that specify that the memory should be pinned, not disposing of the instance is a memory leak.
- Use this any time you already have some memory you want to treat as a
Inheritance
StringWeaver itself is not sealed because UnsafeStringWeaver inherits from it. As such, inheritance from StringWeaver is allowed for external types as well. There are only a select few members you must override to make your implementation function correctly, everything else (functionality-wise) is handled for you (and not virtual, for that matter):
Memory<char> FullMemory: Gets aMemory<char>instance over the entire backing storage (NOT just the used portion).void GrowCore(int requiredCapacity): WITHOUT checking whether it is required, expands the backing storage to at least the specified capacity (those checks are done for you before thisvirtualmethod is ever called).- ⚠
StringWeaver.ToString(the base version) issealed override. IBufferWriter<char>implementations as well asStreamandTextWritersupport through wrappers are both handled internally as well. Do not re-implement any of those.
Adding functionality on top of anything already handled for you is straightforward. All the base functionality can be used to compose your own methods or just use (Full)Memory/(Full)Span to access the backing storage directly. Start and End control which portion of the buffer is considered "used"; Length is computed from these.
⚠ The above statement is true for disposal. As mentioned, StringWeaver does not implement IDisposable, so derived types must do so themselves if they require it. Ensure cleanup for your own types is sound.
⚠ v2.0.0+ Breaking Changes
v2.0.0 saw the introduction of breaking changes to further reduce allocations for specific scenarios. If you aren't deriving from StringWeaver, you will very likely not be affected by most of these changes.
protected int Start { get; set; }andprotected int End { get; set; }: These properties delimit the used portion of the buffer. This allows derived types to implement functionality that would otherwise require shifting data around in the backing storage. It is discouraged for custom derived types to modify this directly. The methodsStringWeaverexposes are well-behaved with respect to this property, if its value is sane at call time.public int Length { get; }: This property no longer has a setter. It is now a computed property using the new propertyStringWeaver.End.protected internal virtual Memory<char> FullMemory { get; }: There has been no API change for this property, but the details of exactly what it is expected to return very much has. It must encompass the entirety of the backing memory of the current instance, regardless of the values ofStringWeaver.StartandStringWeaver.End. Before this, this expecation wasn't very explicit; trimming or similar operations kept the used portion of the backing memory aligned to index0in theMemory<char>returned by this property. This is no longer the case to reduce allocations where possible; however, the above change was made to facilitate this.protected void EnsureZeroAligned(): Aligns the used portion to index0in the backing storage for you usingStringWeaver.StartandStringWeaver.End.protected void ValidateRange(int index, int length): Validates a range against the used portion of the buffer, throwing anArgumentOutOfRangeExceptionfor you if it is not entirely within bounds.protected virtual void GrowCore(int requiredCapacity): Instead ofprotected virtual void Grow(int requiredCapacity), you now overrideGrowCore. Because increasingStringWeaver.Startvalues effectively reduce the available capacity, a new branch was added that attempts to satisfy the capacity requirement without actually needing to allocate usingEnsureZeroAligned.protected virtual void ClearCore(): Introduced to support derivedStringWeavertypes that need to do additional cleanup whenClearis called or where clearing is not just settingStringWeaver.StartandStringWeaver.Endto0.
On a less... annoying note, void ReplaceAll(ReadOnlySpan<char>, ReadOnlySpan<char>) (and the overloads taking an int index and int length, as well as several methods that delegate to this one) had a path optimized that allocated temporary storage for the replacement process. This memory is now sourced either from a stackalloc or from native memory.
Global configuration
The static class StringWeaverConfiguration exposes global configuration options for all StringWeaver variants where applicable. The options are usually strongly-typed and implemented in a thread-safe manner, however, altering options while the affected variants are in use will very likely lead to undefined behavior. I recommend very carefully thinking about what you're changing, why you're doing it and if it's really a good idea, then setting them on application startup and never touching the entire class again.
Contribution
Opening issues and submitting PRs are welcome. All changes must be appropriately covered by tests. Tests run exclusively under net10.0.
Support for netstandard2.0 must always be maintained. If possible, new functionality should be added to all target frameworks. New dependencies may be introduced after I vet the decision to do so.
Or get in touch on Discord @eyeoftheenemy
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.Memory (>= 10.0.5)
- PCRE.NET (>= 1.4.0)
- System.Buffers (>= 4.6.1)
- System.Memory (>= 4.6.3)
-
net6.0
- PCRE.NET (>= 1.4.0)
-
net7.0
- PCRE.NET (>= 1.4.0)
-
net8.0
- PCRE.NET (>= 1.4.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.