文章目录
- 1 方法重载
- 1.1 string.Split()
- 1.2 string.Indexof()
- 2 方法对比
- 2.1 Contains
- 2.2 Equals
- 2.3 字符串差值
- 3 StringBuilder
- 4 换行符
- 4.1 推荐做法
- 4.2 换行符混合问题
- 5 文件路径分隔
- 5.1 推荐做法
- 6 测试代码
- 6.1 "OnlySplit()" vs "SplitWithTrim()"
- 6.2 "OnlyIndexOf()" vs "IndexOfWithToLower()"
- 6.3 "Contains()" vs "IndexOf()" vs "Any()"
- 6.4 "Equals()" vs "Compare()" vs "ToLower()"
- 6.5 "Equals()" vs "=="
- 6.6 "$" vs "StringFormat()"
文章内容参考视频 C#中字符串类的一些实用技巧及新手常犯错误,感谢 十月的寒流。
1 方法重载
1.1 string.Split()
var message = "Hello, C# String!";
var splited = Array.Empty<string>();
// 1.以 " " 分隔字符串
splited = message.Split(" "); // ["Hello,", "", "C#", "String!"]
// 2.以 " " 分隔字符串,结果数最多为 2
splited = message.Split(" ", 2); // ["Hello,", " C# String!"]
// 3.以 " " 分隔字符串,结果数最多为 2,并对移除每个结果开头和结尾的空格
splited = message.Split(" ", 2, StringSplitOptions.TrimEntries); // ["Hello,", "C# String!"]
// splited = message.Split(" ", 2).Select(x => x.Trim()).ToArray(); 该方法效率比上面低
[跳转 1.1 测试代码]
示例 3 运行效率对比,可见在 .NET 7.0 和 .NET 8.0 环境下,使用带操作选项参数的 Split() 方法效率都更快(仅供参考)。
1.2 string.Indexof()
var message = "Hello, C# String!";
var index = -1;
// 1.查找第一个 ' '
index = message.IndexOf(' '); // 6
// 2.从下标 7 开始,查找第一个 ' '
index = message.IndexOf(' ', 7); // 7
// 3.查找第一个 's',并且不区分大小写
index = message.IndexOf('s', StringComparison.OrdinalIgnoreCase); // 11
// index = message.ToLower().IndexOf('s'); 该方法效率比上面低
[跳转 1.2 测试代码]
示例 3 运行效率对比,可见在 .NET 7.0 和 .NET 8.0 环境下,使用带操作选项参数的 IndexOf() 方法效率都更快(仅供参考)。
2 方法对比
2.1 Contains
判断字符串中是否包含某个子串时,优先使用 Contains() 方法。
bool ans;
// Rank 1
ans = "Hello, C# String!".Contains('r');
// Rank 2
ans = "Hello, C# String!".IndexOf('r') >= 0;
// Rank 3
ans = "Hello, C# String!".Any(c => c == 'r');
[跳转 2.1 测试代码]
可以看出,.NET 7.0 下,Contains() 效率领先其他方法很多,而 .NET 8.0 下,Contains() 和 IndexOf() 相差无几。
原因是,IndexOf() 返回的结果包含额外信息,而 Any 方法的算法更具有通用性,因此效率不高。
额外地,当匹配的字符串长度仅为 1 时,考虑使用字符代替,效率会更高。
2.2 Equals
需要考虑大小写比较字符串时,优先考虑使用 Equals() 方法,而不是 CompareTo(),或者将字符串 ToLower() 后再 == 比较。
bool ans;
// Rank 1
ans = "Hello".Equals("hello", StringComparison.OrdinalIgnoreCase);
// Rank 2
ans = String.Compare("Hello", "hello", StringComparison.OrdinalIgnoreCase) == 0;
// Rank 3
ans = "Hello".ToLower() == "hello";
[跳转 2.2-1 测试代码]
可以看出,Equals() 方法效率领先许多。而使用 ToLower() 后再比较,不仅速度最慢,而且还有额外的内存开销。
[跳转 2.2-2 测试代码]
而仅比较字符串相同时,则优先考虑 ==。
bool ans;
// Rank 1
ans = "Hello" == "Hello";
// Rank 2
ans = "Hello".Equals("Hello");
2.3 字符串差值
优先考虑使用内插字符串 $“”,而不是字符串格式化 string.Format() 方法。
int _a = 10;
char _b = 'X';
double _c = 3.14;
string _d = "hello";
string ans;
// Rank 1
ans = $"[{_a}, {_b}, {_c}, {_d}]";
// Rank 2
ans = String.Format("[{0}, {1}, {2}, {3}]", _a, _b, _c, _d);
[跳转 2.3 测试代码]
原因:对于内插字符串,C# 编译器会进行底层优化。例如:
- 若为常量,会直接替换为常量值。
- 替换为 StringBuilder 进行优化。
- 等等。
注意:StringBuilder.AppendFormat(…) 方法效率也比 StringBuilder.Append(内插字符串) 效率低。
3 StringBuilder
StringBuilder 实用用法如下:
// 1.初始提供一个字符串,减少一次扩容
var sb = new StringBuilder("Hello"); // "Hello"
// 2.替换 "ll" 为 "LL"
sb.Replace("ll", "LL"); // "HeLLo"
// 3.从下标 1 开始,向后移除 3 个长度
sb.Remove(1, 3); // "Ho"
// 4.将 "ell" 插入下标 1 的位置
sb.Insert(1, "ell"); // "Hello"
// 5. 重复添加 1 次 ' ' 字符
sb.Append(' ', 1); // "Hello "
// 6.内容末尾添加换行符
sb.AppendLine("World!"); // "Hello World!\r\n"
4 换行符
在不同操作系统中,文本的换行符不同。Linux 下换行符是 “\n”,而 Windows 下换行符是 “\r\n”。下面模拟不同系统下,因换行符不同而带来的影响(使用 C#10 顶级语句):
using System.Text;
string ToString(string[] splited) { // 将字符串数组转换为字符串
if (splited.Length == 0) return "[]";
var sb = new StringBuilder($"[{splited[0]}");
for (var i = 1; i < splited.Length; i++) { sb.Append($", \"{splited[i]}\""); }
sb.Append(']');
return sb.ToString();
}
var messages = new[] { "Hello", "World", "from", "C#" }; // 准备拼接的字符数组
var joinWithN = string.Join("\n", messages); // 用 "\n" 拼接
var joinWithRn = string.Join("\r\n", messages); // 用 "\r\n" 拼接
var splitWithN = joinWithN.Split('\n'); // 用 "\n" 拼接后,用 "\n" 分割
var splitWithRn = joinWithRn.Split('\n'); // 用 "\r\n" 拼接后,用 "\n" 分割
// 输出结果
Console.WriteLine("splitWithN: " + ToString(splitWithN));
Console.WriteLine("splitWithRn: " + ToString(splitWithRn));
首先,我们声明一组待拼接的字符数组,然后分别使用 “\n” 和 “\r\n” 拼接,模拟不同操作系统下的换行符写入。
之后,我们统一使用 “\n” 分割,输出结果如下:
第二行没有输出期望的结果,是因为分割后字符串中有 “\r”,因此回车到行首,覆盖了很多本应打印的内容。
4.1 推荐做法
使用 Environment.NewLine 作为换行符,其会获取环境下定义的换行符字符串,而不是写死 “\n” 或 “\r\n”。
var messages = new[] { "Hello", "World", "from", "C#" }; // 准备拼接的字符数组
var joinWithNewLine = string.Join(Environment.NewLine, messages);
var splitWithNewLine = joinWithNewLine.Split(Environment.NewLine);
// 输出结果
Console.WriteLine("splitWithNewLine: " + ToString(splitWithNewLine));
4.2 换行符混合问题
当不清除字符串中的换行符是 “\n” 还是 “\r\n”,或者二者都有,此时解决方案是:使用 string.ReplaceLineEndings() 方法。
var lines = "Hello\r\nWorld\nfrom\nC#";
var newLines = lines.ReplaceLineEndings(); // 将所有换行符替换为 Environment.NewLine
// 输出结果
Console.WriteLine("lines: " + lines.Length);
Console.WriteLine("newLines: " + newLines.Length);
可以看到,newLines 长度增加了 2,因为将 2 个 “\n” 替换为 “\r\n”。
5 文件路径分隔
在不同操作系统中,文件路径的分隔符也不同。Linux 下是 “/”(斜杠),而 Windows 下是 “\”(反斜杠)。因此,产生的问题类似换行符。
5.1 推荐做法
使用 Path.Combine() 方法。该方法会自动忽略多余的路径分隔符。
var paths1 = new[] { "Hello", "World", "from", "C#" };
var paths2 = new[] { "Hello", "World", "from\\", "C#" }; // 含有多余的路径分隔符
Console.WriteLine(Path.Combine(paths1));
Console.WriteLine(Path.Combine(paths2));
6 测试代码
6.1 “OnlySplit()” vs “SplitWithTrim()”
测试代码:[返回文章]
namespace Learning.BenchmarkTest;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
[MemoryDiagnoser(false)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
public class StringSplitTest
{
public string message = "Hello, C# String!";
[Benchmark(Baseline = true)]
public string[] OnlySplit() {
return message.Split(" ", 2, StringSplitOptions.TrimEntries);
}
[Benchmark]
public string[] SplitWithTrim() {
return message.Split(" ", 2).Select(s => s.Trim()).ToArray();
}
}
6.2 “OnlyIndexOf()” vs “IndexOfWithToLower()”
测试代码:[返回文章]
namespace Learning.BenchmarkTest;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
[MemoryDiagnoser(false)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
public class StringIndexOfTest
{
public string message = "Hello, C# String!";
[Benchmark(Baseline = true)]
public int OnlyIndexOf() {
return message.IndexOf('s', StringComparison.OrdinalIgnoreCase);
}
[Benchmark]
public int IndexOfWithToLower() {
return message.ToLower().IndexOf('s');
}
}
6.3 “Contains()” vs “IndexOf()” vs “Any()”
测试代码:[返回文章]
namespace Learning.BenchmarkTest;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
[MemoryDiagnoser(false)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
public class StringContainsTest
{
private const string _testStr = "Hello, C# String!";
private const char _testChar = 'r';
[Benchmark(Baseline = true)]
public bool Contains() {
return _testStr.Contains(_testChar);
}
[Benchmark]
public bool IndexOf() {
return _testStr.IndexOf(_testChar) >= 0;
}
[Benchmark]
public bool Any() {
return _testStr.Any(c => c == _testChar);
}
}
6.4 “Equals()” vs “Compare()” vs “ToLower()”
测试代码:[返回文章]
namespace Learning.BenchmarkTest;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
[MemoryDiagnoser(false)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
public class StringEqualsIgnoreCaseTest
{
private const string _testStr = "Hello";
private const string _compareStr = "hello";
[Benchmark(Baseline = true)]
public bool Equals() {
return _testStr.Equals(_compareStr, StringComparison.OrdinalIgnoreCase);
}
[Benchmark]
public bool Compare() {
return String.Compare(_testStr, _compareStr, StringComparison.OrdinalIgnoreCase) == 0;
}
[Benchmark]
public bool ToLower() {
return _testStr.ToLower() == _compareStr;
}
}
6.5 “Equals()” vs “==”
测试代码:[返回文章]
namespace Learning.BenchmarkTest;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
[MemoryDiagnoser(false)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
public class StringEqualsTest
{
private const string _testStr = "Hello";
private const string _compareStr = "Hello";
[Benchmark(Baseline = true)]
public bool Equals() {
return _testStr.Equals(_compareStr);
}
[Benchmark]
public bool Sign() {
return _testStr == _compareStr;
}
}
6.6 “$” vs “StringFormat()”
测试代码:[返回文章]
namespace Learning.BenchmarkTest;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
[MemoryDiagnoser(false)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
public class StringFormatTest
{
private int _a = 10;
private char _b = 'X';
private double _c = 3.14;
private string _d = "hello";
[Benchmark(Baseline = true)]
public string Interpolation() {
return $"[{_a}, {_b}, {_c}, {_d}]";
}
[Benchmark]
public string StringFormat() {
return String.Format("[{0}, {1}, {2}, {3}]", _a, _b, _c, _d);
}
}