Add the net9 StringBuilder params ReadOnlySpan overloads - #605
Merged
Conversation
Six members: AppendFormat with and without a provider, and AppendJoin over ReadOnlySpan<object> and ReadOnlySpan<string> with a char or a string separator. These only pay off for a caller that already has a span. Verified on net8 before writing anything: a loose argument list binds to the BCL params array overload and the extension is never reached, because an applicable type member is preferred over any extension. An explicit ReadOnlySpan does reach it. So the polyfill adds the explicit span form without changing what a loose argument list compiles to. Both facts are noted on each member, since the second is not obvious. The CompositeFormat overloads added in net8 are not included. CompositeFormat is itself a net8 type that parses and caches a format string, so it would have to be recreated first. API count 1155 -> 1161.
This was referenced Sep 10, 2026
This was referenced Sep 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six members, all net9:
AppendFormatwith and without a provider, andAppendJoinoverReadOnlySpan<object?>andReadOnlySpan<string?>with acharor astringseparator.The question that had to be answered first: can these ever be called?
The BCL already has
AppendFormat(string, params object?[])on every target. Extension members are only considered when no applicable type member exists — so if a loose argument list always binds to the array overload, a span extension would be dead code and not worth shipping.Tested on net8.0 with a marker before writing anything:
sb.AppendFormat("{0}{1}", 1, 2)params object[]sb.AppendFormat(fmt, someReadOnlySpan)So it is not dead code — it is reached exactly when the caller already has a span, which is the case the API exists for. And it is not a source break either: a loose argument list still compiles to the array overload, exactly as it does without Polyfill, producing identical text.
Both facts are
//Note:d on each member. The second one especially, because "I called it and my allocation didn't go away" is otherwise a confusing result.On net9.0 and later, C# 13's params-collection rules prefer the span overload for a loose list, so the same source allocates less there. That difference is the BCL's, not the polyfill's.
Implementation and the allocation note
Each copies the span to an array and defers to the array overload, which allocates precisely what the span form exists to avoid. That is noted. It cannot be avoided without reimplementing composite formatting, and the "deliverability over perf" rule covers it — the value here is that code written against net9 compiles and behaves correctly on older targets.
A related group deliberately left out
AppendFormat(IFormatProvider, CompositeFormat, ...)— five overloads — is a net8 addition and is not included.CompositeFormatis itself a net8 type that parses and caches a format string, so it would have to be recreated before any of those overloads could exist. That is a separate and much larger piece of work.Tests
Six tests, on every target framework, so net9.0 and later check the BCL and everything below checks the polyfill. Each span call is asserted against the corresponding array overload rather than a hand-written string, so the oracle is an API that exists unchanged everywhere. Covered:
nullelements,nullseparators, empty and single-element spans, and a provider that demonstrably changes the output (de-DEvs invariant for{0:N2}).There is also a regression guard for loose argument lists, since those bind to the array overload below net9.0 and the span overload from net9.0 — the test pins that both produce the same text.
Verification
Solution clean in Release, Consume clean across all 22 TFMs — including net461 and netstandard2.0, where
AppendJoin's array form is itself a Polyfill extension rather than a type member, so those targets were the ambiguity risk. Tests green on net11.0 (1741), net10.0 (1741), net9.0 (1741), net8.0 (1738), net462 (1687), plus PublicTests, EmbeddedTests, UnsafeTests, NoRefsTests and NoExtrasTests.API count 1155 → 1161.