@@ -121,88 +121,8 @@ public static string Concat(params object?[] args)
121121 return result ;
122122 }
123123
124- public static string Concat < T > ( IEnumerable < T > values )
125- {
126- ArgumentNullException . ThrowIfNull ( values ) ;
127-
128- if ( typeof ( T ) == typeof ( char ) )
129- {
130- // Special-case T==char, as we can handle that case much more efficiently,
131- // and string.Concat(IEnumerable<char>) can be used as an efficient
132- // enumerable-based equivalent of new string(char[]).
133- using ( IEnumerator < char > en = Unsafe . As < IEnumerable < char > > ( values ) . GetEnumerator ( ) )
134- {
135- if ( ! en . MoveNext ( ) )
136- {
137- // There weren't any chars. Return the empty string.
138- return Empty ;
139- }
140-
141- char c = en . Current ; // save the first char
142-
143- if ( ! en . MoveNext ( ) )
144- {
145- // There was only one char. Return a string from it directly.
146- return CreateFromChar ( c ) ;
147- }
148-
149- // Create the StringBuilder, add the chars we've already enumerated,
150- // add the rest, and then get the resulting string.
151- var result = new ValueStringBuilder ( stackalloc char [ StackallocCharBufferSizeLimit ] ) ;
152- result . Append ( c ) ; // first value
153- do
154- {
155- c = en . Current ;
156- result . Append ( c ) ;
157- }
158- while ( en . MoveNext ( ) ) ;
159- return result . ToString ( ) ;
160- }
161- }
162- else
163- {
164- using ( IEnumerator < T > en = values . GetEnumerator ( ) )
165- {
166- if ( ! en . MoveNext ( ) )
167- return string . Empty ;
168-
169- // We called MoveNext once, so this will be the first item
170- T currentValue = en . Current ;
171-
172- // Call ToString before calling MoveNext again, since
173- // we want to stay consistent with the below loop
174- // Everything should be called in the order
175- // MoveNext-Current-ToString, unless further optimizations
176- // can be made, to avoid breaking changes
177- string ? firstString = currentValue ? . ToString ( ) ;
178-
179- // If there's only 1 item, simply call ToString on that
180- if ( ! en . MoveNext ( ) )
181- {
182- // We have to handle the case of either currentValue
183- // or its ToString being null
184- return firstString ?? string . Empty ;
185- }
186-
187- var result = new ValueStringBuilder ( stackalloc char [ StackallocCharBufferSizeLimit ] ) ;
188-
189- result . Append ( firstString ) ;
190-
191- do
192- {
193- currentValue = en . Current ;
194-
195- if ( currentValue != null )
196- {
197- result . Append ( currentValue . ToString ( ) ) ;
198- }
199- }
200- while ( en . MoveNext ( ) ) ;
201-
202- return result . ToString ( ) ;
203- }
204- }
205- }
124+ public static string Concat < T > ( IEnumerable < T > values ) =>
125+ JoinCore ( ReadOnlySpan < char > . Empty , values ) ;
206126
207127 public static string Concat ( IEnumerable < string ? > values )
208128 {
@@ -891,48 +811,102 @@ private static string JoinCore<T>(ReadOnlySpan<char> separator, IEnumerable<T> v
891811 ThrowHelper . ThrowArgumentNullException ( ExceptionArgument . values ) ;
892812 }
893813
894- using ( IEnumerator < T > en = values . GetEnumerator ( ) )
814+ using ( IEnumerator < T > e = values . GetEnumerator ( ) )
895815 {
896- if ( ! en . MoveNext ( ) )
816+ if ( ! e . MoveNext ( ) )
897817 {
818+ // If the enumerator is empty, just return an empty string.
898819 return Empty ;
899820 }
900821
901- // We called MoveNext once, so this will be the first item
902- T currentValue = en . Current ;
903-
904- // Call ToString before calling MoveNext again, since
905- // we want to stay consistent with the below loop
906- // Everything should be called in the order
907- // MoveNext-Current-ToString, unless further optimizations
908- // can be made, to avoid breaking changes
909- string ? firstString = currentValue ? . ToString ( ) ;
910-
911- // If there's only 1 item, simply call ToString on that
912- if ( ! en . MoveNext ( ) )
822+ if ( typeof ( T ) == typeof ( char ) )
913823 {
914- // We have to handle the case of either currentValue
915- // or its ToString being null
916- return firstString ?? Empty ;
917- }
824+ // Special-case T==char, as we can handle that case much more efficiently,
825+ // and string.Concat(IEnumerable<char>) can be used as an efficient
826+ // enumerable-based equivalent of new string(char[]).
918827
919- var result = new ValueStringBuilder ( stackalloc char [ StackallocCharBufferSizeLimit ] ) ;
828+ IEnumerator < char > en = Unsafe . As < IEnumerator < char > > ( e ) ;
920829
921- result . Append ( firstString ) ;
830+ char c = en . Current ; // save the first value
831+ if ( ! en . MoveNext ( ) )
832+ {
833+ // There was only one char. Return a string from it directly.
834+ return CreateFromChar ( c ) ;
835+ }
922836
923- do
837+ // Create the builder, add the char we already enumerated,
838+ // add the rest, and then get the resulting string.
839+ var result = new ValueStringBuilder ( stackalloc char [ StackallocCharBufferSizeLimit ] ) ;
840+ result . Append ( c ) ; // first value
841+ do
842+ {
843+ if ( ! separator . IsEmpty )
844+ {
845+ result . Append ( separator ) ;
846+ }
847+
848+ c = en . Current ;
849+ result . Append ( c ) ;
850+ }
851+ while ( en . MoveNext ( ) ) ;
852+ return result . ToString ( ) ;
853+ }
854+ else if ( typeof ( T ) . IsValueType && default ( T ) is ISpanFormattable )
924855 {
925- currentValue = en . Current ;
856+ // Special-case value types that are ISpanFormattable, as we can implement those to avoid
857+ // all string allocations for the individual values. We only do this for value types because
858+ // a) value types are much more likely to implement ISpanFormattable, and b) we can use
859+ // DefaultInterpolatedStringHandler to do all the heavy lifting, and it's more efficient
860+ // for value types as the checks it does for interface implementations are all elided.
861+
862+ T value = e . Current ; // save the first value
863+ if ( ! e . MoveNext ( ) )
864+ {
865+ // There was only one value. Return a string from it directly.
866+ return value ! . ToString ( ) ?? Empty ;
867+ }
926868
927- result . Append ( separator ) ;
928- if ( currentValue != null )
869+ var result = new DefaultInterpolatedStringHandler ( 0 , 0 , CultureInfo . CurrentCulture , stackalloc char [ StackallocCharBufferSizeLimit ] ) ;
870+ result . AppendFormatted ( value ) ; // first value
871+ do
929872 {
930- result . Append ( currentValue . ToString ( ) ) ;
873+ if ( ! separator . IsEmpty )
874+ {
875+ result . AppendFormatted ( separator ) ;
876+ }
877+
878+ result . AppendFormatted ( e . Current ) ;
931879 }
880+ while ( e . MoveNext ( ) ) ;
881+ return result . ToStringAndClear ( ) ;
932882 }
933- while ( en . MoveNext ( ) ) ;
883+ else
884+ {
885+ // For all other Ts, fall back to calling ToString on each and appending the resulting
886+ // string to a builder.
934887
935- return result . ToString ( ) ;
888+ string ? firstString = e . Current ? . ToString ( ) ; // save the first value
889+ if ( ! e . MoveNext ( ) )
890+ {
891+ return firstString ?? Empty ;
892+ }
893+
894+ var result = new ValueStringBuilder ( stackalloc char [ StackallocCharBufferSizeLimit ] ) ;
895+
896+ result . Append ( firstString ) ;
897+ do
898+ {
899+ if ( ! separator . IsEmpty )
900+ {
901+ result . Append ( separator ) ;
902+ }
903+
904+ result . Append ( e . Current ? . ToString ( ) ) ;
905+ }
906+ while ( e . MoveNext ( ) ) ;
907+
908+ return result . ToString ( ) ;
909+ }
936910 }
937911 }
938912
@@ -965,6 +939,11 @@ private static string JoinCore(ReadOnlySpan<char> separator, ReadOnlySpan<string
965939 }
966940 }
967941
942+ if ( totalLength == 0 )
943+ {
944+ return Empty ;
945+ }
946+
968947 // Copy each of the strings into the result buffer, interleaving with the separator.
969948 string result = FastAllocateString ( totalLength ) ;
970949 int copiedLength = 0 ;
0 commit comments