# ZString: A Zero Allocation StringBuilder for .NET and Unity

> ZString replaces String.Format, String.Concat and StringBuilder in hot paths by writing values straight into a rented buffer. It is a Cysharp library for .NET and Unity developers who profile allocations, and its trade-offs are real.

**Cysharp/ZString** — Zero Allocation StringBuilder for .NET and Unity.

- Repository: https://github.com/Cysharp/ZString
- Stars: 2,808 · Forks: 193
- Language: C#
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/cysharp-zstring

## The allocation problem ZString targets in C# string building

Ordinary string concatenation in C# is not free. The README walks through what the compiler emits for `"x:" + x + " y:" + y + " z:" + z`: it becomes `String.Concat(new []{ "x:", x.ToString(), " y:", y.ToString(), " z:", z.ToString() })`. That means a params array allocation plus a separate `ToString()` allocation for every value. `string.Format` takes a different route into the same trap: it resolves to `String.Format(string, object, object, object)`, so each struct argument is boxed into an object before formatting begins.

The audience for ZString is narrow but well defined. It is for .NET and Unity developers whose profiler shows string operations in a frame loop, a request handler, or a logging path, and who already know which call sites matter. It is not a general-purpose convenience library for people who have never looked at an allocation profile. The namespace is `Cysharp.Text`.

## How the struct builder and rented buffer avoid boxing

The mechanism has three parts. First, the builder is a struct (`Utf16ValueStringBuilder` or `Utf8ValueStringBuilder`), so creating it does not allocate an object on the heap. Second, the write buffer is rented from `ThreadStatic` storage or from `ArrayPool`, and `Dispose()` returns it to the pool. Third, the append methods are generic: `Append<T>(T value)` writes the value directly into the buffer instead of calling `value.ToString()` and concatenating the result.

The generic surface extends to formatting. `AppendFormat<T1,...,T16>(string format, T1 arg1, ..., T16 arg16)` and the static `Format<T1,..,T16>` take up to sixteen typed arguments, which is what avoids the boxing that `string.Format` incurs. `Concat<T1,..,T16>` does the same for concatenation. Both `Span<char>` (Utf16) and `Span<byte>` (Utf8) output are supported, so a Utf8 builder can write encoded bytes to a stream or an `IBufferWriter<byte>` without an intermediate string.

The README is explicit that the final string is the only allocation in the simple cases, and that going further requires a stringless consumer. It names Unity TextMeshPro's `SetCharArray` as the example: if the output target can accept a character buffer, the inner buffer can be handed over directly and nothing is allocated at all. That is the design's real boundary. ZString does not make allocation disappear; it moves the allocation to the point where a `string` is genuinely required.

## Installing ZString from NuGet and a first real use

For .NET Core the README gives a single NuGet command. Run it in the Package Manager console and the `ZString` package is added to the project; the namespace to import is `Cysharp.Text`.

```bash
PM> Install-Package ZString
```

For Unity the route is different. The README points to the releases page and says to download `ZString.Unity.unitypackage`, then import it into the project. There is no UPM or git URL documented in the README, so the package file is the path the project itself describes.

Once referenced, the smallest useful step is to replace a `string.Format` call in a hot path with `ZString.Format`. The README's example uses numeric format strings, which is the part people usually miss: the format item syntax is the familiar one, including `{1:000}` and `{2:P}`.

```csharp
using Cysharp.Text;

_ = ZString.Concat(x, y, z);
_ = ZString.Format("x:{0}, y:{1:000}, z:{2:P}", x, y, z);
_ = ZString.Join(',', x, y, z);
```

When you need to build incrementally rather than in one call, create a builder in a `using` block, append, and materialize the string at the end. The `Dispose` in the using block is what returns the rented buffer.

```csharp
using (var sb = ZString.CreateStringBuilder())
{
    sb.Append("foo");
    sb.AppendLine(42);
    sb.AppendFormat("{0} {1:.###}", "bar", 123.456789);
    var str = sb.ToString();
}
```

For Utf8 output the README shows `ZString.CreateUtf8StringBuilder()` with `AppendFormat`, then `WriteToAsync(stream)`, `CopyTo(bufferWriter)` or `TryCopyTo(dest, out var written)`. That last group is where the design pays off most clearly, because the bytes go to the destination without a string ever existing.

## Prepared formats and the thread-static buffer constraint

Two details in the README deserve more attention than they get. The first is `PrepareUtf16<int, int>("x:{0}, y:{1:000}")` and its Utf8 counterpart. These parse the format template once and return a `Utf16PreparedFormat` or `Utf8PreparedFormat` value. The README compares the intended usage to `RegexOptions.Compiled` and says the return value should be stored in a field. Calling `PrepareUtf16` inside the formatting loop defeats the purpose entirely, and nothing in the API stops you from doing that.

The second is the `notNested` parameter on `CreateStringBuilder(bool notNested)` and `CreateUtf8StringBuilder(bool notNested)`. Passing true selects a thread-static buffer, which the README describes as faster but requiring immediate return. The consequence is a real constraint: you cannot hold that builder across an `await`, hand it to another thread, or nest one inside another. The default (no argument) is the safer choice, and the faster path is opt-in precisely because it is easier to misuse. Anyone who reads only the benchmark graph and not the parameter documentation will pick the wrong one.

## Where ZString is the wrong tool

ZString is a struct with a rented buffer and an explicit `Dispose`. That combination is a lifetime obligation, and the library does not enforce it. A builder that is not disposed leaks a pooled array back to nothing; the `ArrayPool` never sees it again. In code with multiple early returns or exception paths, that is easy to get wrong, and the failure is silent rather than loud.

The `notNested` mode narrows the safe usage further. Any code path that awaits between creation and disposal is a candidate for a corrupted thread-static buffer, and async string building is common in exactly the server-side code that cares about allocations. For those paths, plain `StringBuilder` or the interpolated string handler machinery in modern C# is the lower-risk option, and the allocation difference may not matter if the call is not in a tight loop.

There is also the question of whether you are measuring at all. ZString's benefit is proportional to call frequency. Replacing a handful of `string.Format` calls in startup code adds a dependency and a disposal discipline for no measurable gain. The README's own comparison is against concatenation, `string.Format` and `StringBuilder`, which is the right frame: this is a micro-optimization tool, and it should be introduced where a profiler already points.

## ZString versus StringBuilder and the interpolated string handlers

The most direct comparison is with `System.Text.StringBuilder`, and the difference is not speed so much as what gets allocated. `new StringBuilder()` allocates the builder object itself, and `.ToString()` allocates the result. ZString's struct builder removes the first allocation, and the rented buffer means the internal storage is reused across calls rather than grown and collected. For a call site that runs thousands of times per second, that is the whole point.

The other alternative is the compiler's own string interpolation, which in recent C# versions can lower to an interpolated string handler and avoid the intermediate `string` and the boxing. The approaches overlap in intent. The practical difference is that ZString exposes the buffer: `AsSpan()`, `AsMemory()`, `AsArraySegment()` and `TryCopyTo` let you route the characters to a consumer that never needed a string. Interpolation handlers do not give you that handle, and neither does `StringBuilder` in the same way. If your destination is a Unity `TextMeshPro` component, an `IBufferWriter<byte>` or a `Stream`, ZString's buffer access is the reason to prefer it. If your destination is a `string` and the call is not hot, the built-in options are simpler and have no disposal contract.

## Licence, release cadence and the cost of upgrading

ZString is MIT licensed, which permits commercial and closed-source use with the usual requirement to retain the copyright and permission notice. The repository carries an `opensource.snk` file and a `Directory.Build.props`, which is consistent with the signed-assembly practice common to Cysharp projects, though the README does not discuss strong naming or its implications for consumers.

The release history is worth reading before you plan an upgrade. Version 2.6.0 was published on 2024-03-25, 2.5.1 on 2023-09-21 and 2.5.0 on 2022-10-26. That is a slow cadence, with roughly a year between minor releases. The repository itself is not archived and the last push was on 2026-08-04, so work continues between releases, but the published versions move infrequently. For a library whose API surface is small and whose value is in avoiding allocations, a slow release cadence is less alarming than it would be for something tracking a fast-moving specification. The upgrade cost is mainly the disposal and `notNested` discipline described above, which does not change between versions. Pin the version in your package manifest and read the release notes before moving, since the README does not document a rollback procedure.

## Conclusion

Adopt ZString if you are chasing per-frame or per-request allocations in a .NET or Unity codebase, especially where TextMeshPro or Utf8 output is involved. Do not adopt it as a blanket replacement for StringBuilder in cold paths, and do not use it for string building that crosses an async boundary inside a using block. Before committing, verify two things in your own code: that every Utf16ValueStringBuilder and Utf8ValueStringBuilder is disposed on all paths, and that any PrepareUtf16 or PrepareUtf8 value is stored in a static or instance field rather than rebuilt per call.

## FAQ

### What is ZString?

ZString is a zero allocation StringBuilder for .NET Core and Unity, published by Cysharp under the MIT licence. It provides a struct builder that rents its write buffer from ThreadStatic storage or ArrayPool, and generic Append, Format and Concat methods that avoid boxing and ToString allocations.

### How do I install ZString from NuGet?

For .NET Core the README gives the command `PM> Install-Package ZString` in the Package Manager console. The namespace to import is Cysharp.Text. Unity users download ZString.Unity.unitypackage from the releases page instead.

### How is ZString different from StringBuilder?

The README's comparison is that `new StringBuilder()` allocates the builder object and `.ToString()` allocates the result, while ZString uses a struct builder and a rented buffer. ZString also exposes the written buffer through AsSpan, AsMemory and TryCopyTo so output can go to a stringless consumer.

### Does ZString work with Unity TextMeshPro?

Yes. The README states that ZString is integrated with Unity TextMeshPro to avoid string allocation, and shows `tmp.SetTextFormat("Position: {0}, {1}, {2}", x, y, z)` and `tmp.SetText(sb)` as examples. It cites TextMeshPro's SetCharArray as the stringless API that makes completely zero allocation possible.

## Sources

- [Cysharp/ZString on GitHub](https://github.com/Cysharp/ZString)
- [Issues](https://github.com/Cysharp/ZString/issues)
- [License: MIT](https://github.com/Cysharp/ZString/blob/master/LICENSE)
- [README](https://github.com/Cysharp/ZString/blob/master/README.md)
- [Releases](https://github.com/Cysharp/ZString/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cysharp-zstring
