Skip to content

Commit 7abf649

Browse files
First letter to upper case: retarget net10.0, source-generated regex, tests that can fail
Retarget both projects from net6.0 to net10.0 and move the packages to current versions: BenchmarkDotNet and BenchmarkDotNet.Annotations 0.15.8, Microsoft.NET.Test.Sdk 18.10.1, MSTest.TestAdapter and MSTest.TestFramework 4.4.0 (from 2.2.8), coverlet.collector 10.0.1. No source change was needed for the MSTest major jump: this file has no Assert.ThrowsException call site. FirstCharToUpperRegex moves from Regex.Replace(input, "^[a-z]", ...) to a [GeneratedRegex("^\p{Ll}")] partial method. The ASCII class silently passed through any non-ASCII first letter that every other method here capitalizes. FirstCharToUpperUnsafeCode keeps its body and gains an XML doc comment and an inline comment saying what it really does: it mutates the instance it was given, so an interned literal changes for every reference to it. It is demonstration code. The unit tests could not fail before. All eight shared one interned literal, so the unsafe test upper-cased the input of every test that ran after it, and every assertion only checked char.IsUpper on the first character. The assertions are now AreEqual on the whole expected string, the unsafe tests build their own non-interned instances, and two tests are added: one proving the mutation reaches a second variable, one covering a non-ASCII first letter. Idiom lift: file-scoped namespaces, and the test class field becomes private readonly FirstLetterToUpperMethods _upperCase = new(). Build 0 warnings 0 errors, 10 of 10 tests passing on SDK 10.0.302, runtime 10.0.10.
1 parent e413527 commit 7abf649

4 files changed

Lines changed: 218 additions & 188 deletions

File tree

‎strings-csharp/FirstLetterToUpper/FirstLetterToUpper/FirstLetterToUpper.csproj‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net6.0</TargetFramework>
5+
<TargetFramework>net10.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
13-
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.2" />
12+
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
13+
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.15.8" />
1414
</ItemGroup>
1515

1616
</Project>
Lines changed: 114 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,156 @@
1-
using BenchmarkDotNet.Attributes;
1+
using BenchmarkDotNet.Attributes;
22
using System.Text.RegularExpressions;
33

4-
namespace FirstLetterToUpper
4+
namespace FirstLetterToUpper;
5+
6+
[MemoryDiagnoser]
7+
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
8+
[RankColumn]
9+
public partial class FirstLetterToUpperMethods
510
{
6-
[MemoryDiagnoser]
7-
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
8-
[RankColumn]
9-
public class FirstLetterToUpperMethods
11+
public IEnumerable<object[]> SampleStrings()
12+
{
13+
yield return new object[] { GenerateRandomString(2000)};
14+
}
15+
16+
[Benchmark]
17+
[ArgumentsSource(nameof(SampleStrings))]
18+
public string FirstCharSubstring(string input)
1019
{
11-
public IEnumerable<object[]> SampleStrings()
20+
if (string.IsNullOrEmpty(input))
1221
{
13-
yield return new object[] { GenerateRandomString(2000)};
22+
return string.Empty;
1423
}
1524

16-
[Benchmark]
17-
[ArgumentsSource(nameof(SampleStrings))]
18-
public string FirstCharSubstring(string input)
19-
{
20-
if (string.IsNullOrEmpty(input))
21-
{
22-
return string.Empty;
23-
}
25+
return $"{input[0].ToString().ToUpper()}{input.Substring(1)}";
26+
}
2427

25-
return $"{input[0].ToString().ToUpper()}{input.Substring(1)}";
28+
[Benchmark]
29+
[ArgumentsSource(nameof(SampleStrings))]
30+
public string FirstCharToUpper(string input)
31+
{
32+
if (string.IsNullOrEmpty(input))
33+
{
34+
return string.Empty;
2635
}
2736

28-
[Benchmark]
29-
[ArgumentsSource(nameof(SampleStrings))]
30-
public string FirstCharToUpper(string input)
31-
{
32-
if (string.IsNullOrEmpty(input))
33-
{
34-
return string.Empty;
35-
}
37+
return $"{char.ToUpper(input[0])}{input[1..]}";
38+
}
3639

37-
return $"{char.ToUpper(input[0])}{input[1..]}";
40+
[Benchmark]
41+
[ArgumentsSource(nameof(SampleStrings))]
42+
public string FirstCharToCharArray(string input)
43+
{
44+
if (string.IsNullOrEmpty(input))
45+
{
46+
return string.Empty;
3847
}
3948

40-
[Benchmark]
41-
[ArgumentsSource(nameof(SampleStrings))]
42-
public string FirstCharToCharArray(string input)
43-
{
44-
if (string.IsNullOrEmpty(input))
45-
{
46-
return string.Empty;
47-
}
49+
var stringArray = input.ToCharArray();
4850

49-
var stringArray = input.ToCharArray();
51+
if (char.IsLower(stringArray[0]))
52+
{
53+
stringArray[0] = char.ToUpper(stringArray[0]);
54+
}
5055

51-
if (char.IsLower(stringArray[0]))
52-
{
53-
stringArray[0] = char.ToUpper(stringArray[0]);
54-
}
56+
return new string(stringArray);
57+
}
5558

56-
return new string(stringArray);
59+
[Benchmark]
60+
[ArgumentsSource(nameof(SampleStrings))]
61+
public string FirstCharToUpperAsSpan(string input)
62+
{
63+
if (string.IsNullOrEmpty(input))
64+
{
65+
return string.Empty;
5766
}
5867

59-
[Benchmark]
60-
[ArgumentsSource(nameof(SampleStrings))]
61-
public string FirstCharToUpperAsSpan(string input)
62-
{
63-
if (string.IsNullOrEmpty(input))
64-
{
65-
return string.Empty;
66-
}
68+
Span<char> destination = stackalloc char[1];
6769

68-
Span<char> destination = stackalloc char[1];
70+
input.AsSpan(0, 1).ToUpperInvariant(destination);
6971

70-
input.AsSpan(0, 1).ToUpperInvariant(destination);
72+
return $"{destination}{input.AsSpan(1)}";
73+
}
7174

72-
return $"{destination}{input.AsSpan(1)}";
73-
}
75+
[GeneratedRegex("^\\p{Ll}")]
76+
private static partial Regex FirstLowerLetter();
7477

75-
[Benchmark]
76-
[ArgumentsSource(nameof(SampleStrings))]
77-
public string FirstCharToUpperRegex(string input)
78+
[Benchmark]
79+
[ArgumentsSource(nameof(SampleStrings))]
80+
public string FirstCharToUpperRegex(string input)
81+
{
82+
if (string.IsNullOrEmpty(input))
7883
{
79-
if (string.IsNullOrEmpty(input))
80-
{
81-
return string.Empty;
82-
}
83-
84-
return Regex.Replace(input, "^[a-z]", c => c.Value.ToUpper());
84+
return string.Empty;
8585
}
8686

87-
[Benchmark]
88-
[ArgumentsSource(nameof(SampleStrings))]
89-
public string FirstCharToUpperLinq(string input)
90-
{
91-
if (string.IsNullOrEmpty(input))
92-
{
93-
return string.Empty;
94-
}
87+
return FirstLowerLetter().Replace(input, c => c.Value.ToUpper());
88+
}
9589

96-
return $"{input.FirstOrDefault().ToString().ToUpper()}{input.Substring(1)}";
90+
[Benchmark]
91+
[ArgumentsSource(nameof(SampleStrings))]
92+
public string FirstCharToUpperLinq(string input)
93+
{
94+
if (string.IsNullOrEmpty(input))
95+
{
96+
return string.Empty;
9797
}
9898

99+
return $"{input.FirstOrDefault().ToString().ToUpper()}{input.Substring(1)}";
100+
}
99101

100-
[Benchmark]
101-
[ArgumentsSource(nameof(SampleStrings))]
102-
public string FirstCharToUpperStringCreate(string input)
103-
{
104-
if (string.IsNullOrEmpty(input))
105-
{
106-
return string.Empty;
107-
}
108102

109-
return string.Create(input.Length, input, static (Span<char> chars, string str) =>
110-
{
111-
chars[0] = char.ToUpperInvariant(str[0]);
112-
str.AsSpan(1).CopyTo(chars[1..]);
113-
});
103+
[Benchmark]
104+
[ArgumentsSource(nameof(SampleStrings))]
105+
public string FirstCharToUpperStringCreate(string input)
106+
{
107+
if (string.IsNullOrEmpty(input))
108+
{
109+
return string.Empty;
114110
}
115111

116-
[Benchmark]
117-
[ArgumentsSource(nameof(SampleStrings))]
118-
public string FirstCharToUpperUnsafeCode(string input)
112+
return string.Create(input.Length, input, static (Span<char> chars, string str) =>
119113
{
120-
if (string.IsNullOrEmpty(input))
121-
{
122-
return string.Empty;
123-
}
114+
chars[0] = char.ToUpperInvariant(str[0]);
115+
str.AsSpan(1).CopyTo(chars[1..]);
116+
});
117+
}
118+
119+
/// <summary>
120+
/// Demonstration code only. This method does not build a new string: it pins the string it was
121+
/// given and overwrites its first character in place, so every other variable, field or
122+
/// dictionary key pointing at the same instance changes with it. Identical string literals in an
123+
/// assembly share one interned instance, which makes the damage reach code this method never saw.
124+
/// Do not use it in production.
125+
/// </summary>
126+
[Benchmark]
127+
[ArgumentsSource(nameof(SampleStrings))]
128+
public string FirstCharToUpperUnsafeCode(string input)
129+
{
130+
if (string.IsNullOrEmpty(input))
131+
{
132+
return string.Empty;
133+
}
124134

125-
unsafe
135+
unsafe
136+
{
137+
// Mutates the instance itself. If input is an interned literal, every reference to
138+
// that literal now reads the modified value. See the XML comment above.
139+
fixed (char* p = input)
126140
{
127-
fixed (char* p = input)
128-
{
129-
*p = char.ToUpper(*p);
130-
}
141+
*p = char.ToUpper(*p);
131142
}
132-
133-
return input;
134143
}
135144

136-
private string GenerateRandomString(int size)
137-
{
138-
var random = new Random();
145+
return input;
146+
}
139147

140-
var charOptions = "abcdefghijklmnopqrstuvwxyz";
148+
private string GenerateRandomString(int size)
149+
{
150+
var random = new Random();
141151

142-
return new string(Enumerable.Repeat(charOptions, size).Select(s => s[random.Next(s.Length)]).ToArray()).ToLower();
143-
}
152+
var charOptions = "abcdefghijklmnopqrstuvwxyz";
153+
154+
return new string(Enumerable.Repeat(charOptions, size).Select(s => s[random.Next(s.Length)]).ToArray()).ToLower();
144155
}
145156
}

‎strings-csharp/FirstLetterToUpper/FirstLetterToUpperTests/FirstLetterToUpperTests.csproj‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77

88
<IsPackable>false</IsPackable>
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
13-
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
14-
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
15-
<PackageReference Include="coverlet.collector" Version="3.1.2" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
13+
<PackageReference Include="MSTest.TestAdapter" Version="4.4.0" />
14+
<PackageReference Include="MSTest.TestFramework" Version="4.4.0" />
15+
<PackageReference Include="coverlet.collector" Version="10.0.1" />
1616
</ItemGroup>
1717

1818
<ItemGroup>

0 commit comments

Comments
 (0)