diff --git a/src/System.Memory/src/System/ReadOnlySpan.Portable.cs b/src/System.Memory/src/System/ReadOnlySpan.Portable.cs index 7cf3dde9014a..c8ddb1d43b78 100644 --- a/src/System.Memory/src/System/ReadOnlySpan.Portable.cs +++ b/src/System.Memory/src/System/ReadOnlySpan.Portable.cs @@ -193,6 +193,17 @@ public override string ToString() { if (typeof(T) == typeof(char)) { + // If this wraps a string and represents the full length of the string, just return the wrapped string. + if (_byteOffset == MemoryExtensions.StringAdjustment) + { + object obj = Unsafe.As(_pinnable); // minimize chances the compilers will optimize away the 'is' check + if (obj is string s && _length == s.Length) + { + return s; + } + } + + // Otherwise, copy the data to a new string. unsafe { fixed (char* src = &Unsafe.As(ref DangerousGetPinnableReference())) diff --git a/src/System.Memory/tests/ReadOnlySpan/ToString.cs b/src/System.Memory/tests/ReadOnlySpan/ToString.cs index c8326428f6a6..30fafd0f5a46 100644 --- a/src/System.Memory/tests/ReadOnlySpan/ToString.cs +++ b/src/System.Memory/tests/ReadOnlySpan/ToString.cs @@ -49,5 +49,23 @@ public static unsafe void ToStringForSpanOfString() var span = new ReadOnlySpan(a); Assert.Equal("System.ReadOnlySpan[3]", span.ToString()); } + + [Fact] + public static void ToString_SpanOverString() + { + string orig = "hello world"; + Assert.Equal(orig, orig.AsReadOnlySpan().ToString()); + Assert.Equal(orig.Substring(0, 5), orig.AsReadOnlySpan(0, 5).ToString()); + Assert.Equal(orig.Substring(5), orig.AsReadOnlySpan(5).ToString()); + Assert.Equal(orig.Substring(1, 3), orig.AsReadOnlySpan(1, 3).ToString()); + } + + [SkipOnTargetFramework(~TargetFrameworkMonikers.NetFramework, "Optimization only applies to portable span.")] + [Fact] + public static void ToString_SpanOverFullString_ReturnsOriginal() + { + string orig = "hello world"; + Assert.Same(orig, orig.AsReadOnlySpan().ToString()); + } } }