Skip to content

Add net11 StringBuilder Rune members - #589

Merged
SimonCropp merged 1 commit into
mainfrom
stringbuilder-rune
Sep 10, 2026
Merged

Add net11 StringBuilder Rune members#589
SimonCropp merged 1 commit into
mainfrom
stringbuilder-rune

Conversation

@SimonCropp

Copy link
Copy Markdown
Owner

Adds the StringBuilder Rune members that a polyfill can actually reach:

  • EnumerateRunes(), and the StringBuilderRuneEnumerator it returns
  • GetRuneAt(int) and TryGetRuneAt(int, out Rune)
  • Replace(Rune, Rune) and Replace(Rune, Rune, int, int)

API count 1067 to 1072. All of it needs Rune, so it applies from netcoreapp3.0.

Append(Rune) and Insert(int, Rune) are deliberately excluded

StringBuilder already has Append(object) and Insert(int, object), which are applicable to a Rune, and an instance method always beats an extension method. An extension taking a Rune can therefore never bind. Confirmed on net8 with an extension that appends a marker: the marker never appears for Append and Insert, and does appear for Replace.

This is not a gap in practice. sb.Append(rune) already compiles on earlier target frameworks and appends the same characters, because Append(object) calls Rune.ToString(). The only difference is a boxing allocation. Adding the members would have produced entries in api_list for methods that can never be called.

Replace has no applicable instance overload, since Rune converts to neither char nor string, so those two do bind.

StringBuilderRuneEnumerator

It is a top level type in System.Text rather than nested in StringBuilder, so it can be recreated in the normal way, with a TypeForwardedTo on net11.

It delegates to StringRuneEnumerator over a snapshot of the content, which gives the same replacement character behaviour for unpaired surrogates for free. The snapshot means changes made while enumerating are not observed, which is recorded as a //Note:.

Semantics

Taken from net11 rather than assumed:

  • an unpaired surrogate enumerates as U+FFFD
  • GetRuneAt at the index of a high surrogate returns the whole scalar
  • GetRuneAt on a low surrogate, or on an unpaired surrogate, throws ArgumentException
  • GetRuneAt with an out of range index throws ArgumentOutOfRangeException
  • TryGetRuneAt returns false for those same surrogates, but still throws for an out of range index rather than returning false
  • Replace may change the length, since the two runes can differ in UTF-16 length

Verification

  • Solution builds clean in Release. Consume builds clean in Debug across all 22 target frameworks.
  • Tests green on net11.0 (1670), net462 (1636), net8.0 (1667) and net10.0 (1670), plus PublicTests, EmbeddedTests, UnsafeTests, NoRefsTests and NoExtrasTests.
  • The same assertions run against the BCL on net11 and against the polyfill on netcoreapp3.0 through net10.

Adds the Rune members that can be reached from a polyfill:

* EnumerateRunes(), and the StringBuilderRuneEnumerator it returns
* GetRuneAt(int) and TryGetRuneAt(int, out Rune)
* Replace(Rune, Rune) and Replace(Rune, Rune, int, int)

Append(Rune) and Insert(int, Rune) are deliberately not included.
StringBuilder already has Append(object) and Insert(int, object), which
are applicable to a Rune, and an instance method always beats an
extension method, so an extension taking a Rune could never bind. Those
two calls already compile on earlier target frameworks and produce the
same characters through the object overloads, at the cost of boxing.

StringBuilderRuneEnumerator is a top level type rather than nested in
StringBuilder, so it can be recreated. It delegates to
StringRuneEnumerator over a snapshot of the content, which gives the
same replacement character behaviour for unpaired surrogates.

Semantics were taken from net11:

* an unpaired surrogate enumerates as U+FFFD
* GetRuneAt on a low surrogate, or on an unpaired surrogate, throws
  ArgumentException, and an out of range index throws
  ArgumentOutOfRangeException
* TryGetRuneAt returns false for those same surrogates, but still throws
  for an out of range index

All of this needs Rune, so it applies from netcoreapp3.0.
@SimonCropp SimonCropp added this to the 11.3.0 milestone Sep 10, 2026
@SimonCropp
SimonCropp merged commit ddd9360 into main Sep 10, 2026
4 of 6 checks passed
@SimonCropp
SimonCropp deleted the stringbuilder-rune branch September 10, 2026 01:46
This was referenced Sep 10, 2026
This was referenced Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant