2024-08-04 C# 中 string 实用技巧级新手常见错误


文章内容参考视频 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() 方法效率都更快(仅供参考)。

image-20240804140201232

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() 方法效率都更快(仅供参考)。

image-20240804140754420

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 时,考虑使用字符代替,效率会更高。

image-20240804150101538

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() 后再比较,不仅速度最慢,而且还有额外的内存开销。

image-20240804154936655

​ [跳转 2.2-2 测试代码]

​ 而仅比较字符串相同时,则优先考虑 ==。

bool ans;

// Rank 1
ans = "Hello" == "Hello";

// Rank 2
ans = "Hello".Equals("Hello");
image-20240804161707964

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 进行优化。
  • 等等。
image-20240804153619256

注意: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” 分割,输出结果如下:

image-20240804142910681

​ 第二行没有输出期望的结果,是因为分割后字符串中有 “\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);
image-20240804144104758

​ 可以看到,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));
image-20240804145017303

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);
    }
}
  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蔗理苦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值