ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C# String与Python str对照:字符串操作、比较与性能优化

C# String与Python str对照:字符串操作、比较与性能优化 如果你和我一样平时写脚本习惯用 Python被它的字符串操作惯坏了那么切到 Unity 写 C# 的头几天大概率会对着String类发懵。我最近在做 Unity 数据导出工具、UI 文本系统和配置表读写的活几乎天天跟字符串打交道索性把 C# String 和 Python str 的常用操作一页页对着抄整理成这份代码版笔记。C# 的 String 是引用类型、不可变以及竟然比较内容而不是对象引用这些都和 Python 的直觉不完全一样。这篇笔记会把两种语言的字符串 API 放在一起对照每段都有可直接跑的代码适合正在从 Python 转到 Unity / C# 的开发者也适合刚学 C# 字符串处理、想系统过一遍常用方法的同学。1. 先把 C# String 的底层规则说清楚不可变性、引用类型与字符串池1.1 不可变的两条规律你一改我就变但你我没变初次接触 C# 的 string最容易忽略的是它虽然是一个引用类型但又是不可变的。这里有两层意思第一你无法修改一个已有字符串的内容第二任何会“修改”字符串的操作本质都是生成一个新字符串对象再让变量指向它。看这样一段代码就能体会差异在哪里string a hello; string b a; a a world; Debug.Log(a); // hello world Debug.Log(b); // hello对应的 Python 代码a hello b a a a world print(a) # hello world print(b) # hello两种语言在这个例子里表现完全一样b没有被a的修改影响。区别在于底层C# 的 string 是引用类型b a时两个变量指向同一个对象但因为字符串不可变后面执行a a world时程序并没有把原来那个hello对象改掉而是新建了一个hello world让a指向它b依旧指向原来的hello。可以类比成一张已经写好的纸。这张纸不能擦掉重写你想改内容就得拿一张新纸重新抄一遍别人手上如果还有旧纸的复印件那它上面永远是旧内容。理解不可变性之后很多行为就能解释了字符串在多个变量之间共享是安全的因为没人能改它。字符串可以被当作哈希键缓存哈希值因为内容保证不变。每次拼接、替换、裁剪都会产生新的字符串对象在高频调用的地方这就是 GC 压力的来源。1.2 字符串池什么时候两个字符串是同一个对象C# 里还有一个 Python 开发者不太熟悉的机制字符串驻留池String Intern Pool。字符串字面量在编译期会统一放到一个池子里运行期间如果出现相同的字面量直接复用同一个对象。举个例子string x unity; string y unity; Debug.Log(x y); // True内容相等 Debug.Log(ReferenceEquals(x, y)); // True驻留池导致引用也相同如果字符串是运行时动态拼出来的它通常不会自动进入驻留池string prefix uni; string z prefix ty; Debug.Log(x z); // True内容是相等的 Debug.Log(ReferenceEquals(x, z)); // False引用不同这个 point 之所以重要是因为网上有大量说法叫“判断字符串相等要用 ReferenceEquals”这在 C# 里属于严重的误解。日常判断字符串内容是否相等都该用或Equals不要因为字符串是引用类型就担心比较的是引用。C# 编译器已经把 string 的重载为值比较行为上更接近 Python 字符串的。1.3 共享数据Substring 之外还有 Span 可以做切片知道字符串不可变以后我们再聊一个进阶点。Python 的切片s[1:4]会直接生成新字符串C# 的Substring通常也会生成新字符串。但在 .NET 的新版本里如果只是想临时读取一段字符而不修改它可以用AsSpan拿到只读内存视图不产生新的托管字符串对象string s abcdef; ReadOnlySpanchar slice s.AsSpan(1, 3); // slice 内容为 bcd但没有生成新的 string 对象这一招在 Unity 里能不能用取决于你使用的运行时和平台支持。Unity 2022 之后的很多主流平台已经能跑但在 il2cpp 和某些旧版本 Mono 上Span 支持依然可能踩坑。因此我把它当一个“方向性知识”写进笔记真正的项目里如果只是为了避免 GC优先还是用后面会说到的 StringBuilder 和 TextMeshPro 专用 API而不是到处搬 Span。2. 拼接与格式化从 到 ${} 再到 StringBuilderPython 这里怎么对标2.1 简单的 拼接C# 与 Python 都别在循环里用先看直觉写法。C# 支持直接用拼接字符串Python 也支持两个语言的简单场景几乎一致string playerName Alan; string level 8; string info Player: playerName , Level level;player_name Alan level 8 info Player: player_name , Level level问题出在循环拼接。C# 的最后会编译成string.Concat每次拼接都会创建一个新字符串对象。循环 N 次程序内部就要创建 N 个临时字符串不仅分配次数多而且每轮还要把前面已经拼接过的字符复制一遍整体开销接近 O(n^2)。Python 也一样字符串不可变意味着s x同样不断创建新对象。Python 的社区习惯是收集到一个list里最后用.join(list)一次拼完parts [] for chunk in chunks: parts.append(chunk) result .join(parts)C# 等项目收集完以后可以用string.Join也可以中途就丢给StringBuilder。两条路的共同点都是避免在循环内部反复产生中间字符串。2.2 string.Format、${} 与 Python 格式化全家桶C# 的格式化老版本最常见的是string.Formatstring info string.Format(Player: {0}, Level {1}, playerName, level);Python 里对应的是str.formatinfo Player: {0}, Level {1}.format(player_name, level)C# 6 之后引入了字符串插值${}功能上几乎等于 Python 的 f-stringstring info $Player: {playerName}, Level {level};info fPlayer: {player_name}, Level {level}从代码可读性角度我强烈建议新写的 C# 代码优先用${}它和 Python f-string 的阅读方式几乎相同占位符的位置就在数据旁边不容易发生参数错位。比较反直觉的是性能。string.Format接收的是object参数如果你把int、float这样的值类型传进去会发生装箱字符串插值在某些编译器版本上也会调用类似string.Format的路径。换句话说$HP: {hp} / {maxHp}每执行一次大概率也会产生一次字符串分配和值类型装箱。日常 UI 上没问题但如果放在每帧执行的 Update 里就要小心。2.3 StringBuilder用对场景才能省钱StringBuilder是 C# 里做大量拼接的常见方案。它内部维护一个可变字符缓冲Append 方法往缓冲里写只有最后 ToString 时才真正生成一个字符串对象。一个典型写法是StringBuilder sb new StringBuilder(128); for (int i 0; i 1000; i) { sb.Clear(); sb.Append(Frame ).Append(i).Append(: ); sb.Append(score ).Append(score); Debug.Log(sb.ToString()); }注意我在循环外面创建了 StringBuilder并通过 Clear 复用同一个缓冲这样每一轮循环只分配一个最终字符串对象避免了 1000 次临时拼接分配。Python 没有 StringBuilder但思路可以用io.StringIO来对标或者更日常地收集到 list 再 join。C# 里也有string.Join这种一次性拼接方式适合已知整个集合的场景string[] items { Sword, Shield, Potion }; string inventory string.Join(, , items);items [Sword, Shield, Potion] inventory , .join(items)真正需要 StringBuilder 的场景是拼接次数无法预先确定或者分布在复杂分支逻辑里。如果只是拼两三个字符串直接或插值就够强行引入 StringBuilder 反而降低代码可读性性能提升也可以忽略。3. 查找、截取、切分与替换C# 与 Python 的逐行代码对照3.1 Substring vs 切片索引怎么对齐C# 里做截取方法名是Substring接收起始索引和长度两个参数Python 直接使用切片语法s[start:end]终止下标是不包含的。两边的共同点是索引从 0 开始、区间左闭右开但是 C# 给的是“长度”Python 给的是“结束位置”这个差异最容易造成差一错误。看个对照string id 2024-000123; string code id.Substring(5); // 000123 string first id.Substring(0, 3); // 202id 2024-000123 code id[5:] # 000123 first id[:3] # 202如果要把 C# 的Substring(5, 3)翻译成 Python对应的是id[5:8]不是id[5:3]。因为 Python 的第二个位置是结束下标C# 的第二个参数却是截取长度。初学者最容易在Substring(5, 3)这个写法上犯错总以为它会返回从第 5 位开始 3 位结束的切片实际上它返回的是000这样长度为 3 的子串。3.2 IndexOf、Contains、StartsWith命中判断的常见写法查找子串的核心 APIC# 是IndexOf和ContainsPython 是find、index和in。先看代码string line unity skill attack indicators; int pos1 line.IndexOf(skill); // 6 int pos2 line.IndexOf(attack); // 12 bool hasIndicator line.Contains(indicator); // True bool starts line.StartsWith(unity); // True bool ends line.EndsWith(indicators); // Trueline unity skill attack indicators pos1 line.find(skill) # 6 pos2 line.find(attack) # 12 has_indicator indicator in line # True starts line.startswith(unity) # True ends line.endswith(indicators) # TrueIndexOf和 Python 的find在找不到子串时都返回-1这一点可以无缝迁移。Python 的index在找不到时会抛异常C# 的IndexOf不会喜欢显式处理错误的人其实更适应IndexOf。需要注意在较老的 Unity Mono 环境下Contains和StartsWith的默认行为可能受当前语言文化影响。我会在后面第 4 章专门讲这里先给一条实践规则做文件名、物品名、路径这类内部数据的匹配时最好显式指定StringComparison.OrdinalIgnoreCase或StringComparison.Ordinal。3.3 Split、Join、Trim分隔清洗字符串切分字符串是日常里最频繁的操作之一。C# 的Split跟 Python 的split看起来相似细节却不完全一样string csv a,b,,c; string[] parts csv.Split(,); // 结果[a, b, , c] string[] noEmpty csv.Split(new char[] { , }, StringSplitOptions.RemoveEmptyEntries); // 结果[a, b, c]csv a,b,,c parts csv.split(,) # 结果[a, b, , c] parts2 list(filter(None, csv.split(,))) # 结果[a, b, c]Python 的split(,)默认会保留空字符串只有无参split()才会按连续空白切分并自动去除空项。C# 里要达到类似效果得显式声明StringSplitOptions.RemoveEmptyEntries否则空项会原样保留。C# 里如果想按空白切分并去除空项可以这样写string line hello world ; string[] words line.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); // 结果[hello, world]这和 Python 的line.split()行为几乎一致分割符是所有空白字符连续空白不会产生空项。两侧修剪也一样string raw Hello World ; string trimmed raw.Trim(); // Hello World string startTrimmed raw.TrimStart(); // Hello World string endTrimmed raw.TrimEnd(); // Hello Worldraw Hello World trimmed raw.strip() # Hello World start_trimmed raw.lstrip() # Hello World end_trimmed raw.rstrip() # Hello WorldTrim 只处理首尾中间的空格不动这是两边共同的规则。3.4 Replace、Remove、Insert处理后的注意点替换操作C# 的Replace默认替换所有匹配项Python 的replace可以通过第三个参数限制替换次数string msg a-b-c-d; string all msg.Replace(-, _); // a_b_c_dmsg a-b-c-d all msg.replace(-, _) # a_b_c_d limited msg.replace(-, _, 2) # a_b_c-dC# 没有提供带次数的Replace如果想只替换第一次匹配需要自己处理。我在项目中写过这样一个辅助函数思路是利用IndexOf定位后手动裁开static string ReplaceFirst(string source, string oldValue, string newValue) { int pos source.IndexOf(oldValue, StringComparison.Ordinal); if (pos 0) return source; return source.Substring(0, pos) newValue source.Substring(pos oldValue.Length); }这个函数不长但能解决很多场景比如你只想替换配置文本里第一个占位符不希望把后面同名占位符一起改掉。Remove和Insert是 Python 没有直接对标的方法string t Hello World; string removed t.Remove(5, 6); // Hello string inserted t.Insert(6, Brave ); // Hello Brave WorldPython 实现同样效果会用切片t Hello World removed t[:5] t[11:] # Hello inserted t[:6] Brave t[6:] # Hello Brave World理解底层逻辑之后你会发现 C# 这些方法无非是围绕“生成新字符串”这一原则做的封装。4. 最容易翻车的字符串比较C# 的 、Equals 与 StringComparison4.1 C# 中 比较的是内容不是引用从 Python 转过来的人看到 C# 的 string 是引用类型容易先入为主地以为比较的是引用。实际上 C# 的string类型重载了运算符它比较的是字符串内容是否相等而不是堆上的内存地址。再看一遍string s1 abc; string s2 new StringBuilder(ab).Append(c).ToString(); Debug.Log(s1 s2); // True内容相等 Debug.Log(ReferenceEquals(s1, s2)); // False引用不同Python 里也有类似区分比较内容is比较身份。但 Python 的is几乎不应该用来判断字符串内容C# 里的ReferenceEquals同理。所以我强烈推荐日常代码一律使用或string.Equals判断字符串相等不要使用ReferenceEquals。字符串驻留池导致的“引用相同”是编译器优化产生的行为不是你可以依赖的规则。4.2 藏在默认比较规则里的“本地化”地雷C# 字符串比较比 Python 复杂是因为很多方法默认受“区域性比较规则”影响。比如在不同语言环境下字符排序规则、大小写转换规则会发生变化一个在中文环境下正常的匹配换到某些语言环境可能出现差异。最典型的例子是土耳其语中的字母I。在土耳其语文化环境里小写i对应的大写不是I而是İ因此在极端场景下i.ToUpper()在不同机器上可能返回不同结果。如果你的代码是服务器工具、编辑器工具、配置表校验这类和用户界面无关的内部逻辑就完全不需要跟随系统文化应该使用不变文化规则。4.3 统一使用 StringComparison.Ordinal 的实践C# 里凡是提供StringComparison参数重载的方法都值得你养成选一个参数的习惯string fileName Config_001; bool starts fileName.StartsWith(config, StringComparison.OrdinalIgnoreCase); bool equals fileName.Equals(CONFIG_001, StringComparison.OrdinalIgnoreCase); int pos fileName.IndexOf(001, StringComparison.Ordinal);这些写法特别适合 Unity 项目文件名匹配、玩家输入校验、物品 ID 查询、配置表 Key 比对基本上都该用Ordinal或OrdinalIgnoreCase。这样写的好处是跨平台结果一致不会因为 Android、iOS、Windows 的本地化设置不同导致同一个功能行为不同。Python 开发者看到这里会觉得有点繁琐因为 Python 的字符串比较默认按 Unicode 码点没有文化规则这种概念。但 C# 里多写这几个参数换来的是确定性这个代价在项目里是值得的。5. 放进 Unity 里实战文本更新、日志拼接与 GC 控制5.1 从 Update 里的文本更新说起Unity 游戏里最常见的字符串场景是 HUD 更新。很多新手会直接在 Update 里写// 不推荐每帧都可能拼接字符串 void Update() { hudText.text HP: hp / maxHp; }哪怕hp和maxHp不变这段代码每帧也会执行字符串拼接产生临时字符串对象最终触发 GC。第一次写没问题当场景里同时有十几个这样的 UI 文本时Profiler 里的 GC Alloc 会非常难看。我能给出的第一级优化是只在数据变化时才更新int lastHp -1; int lastMaxHp -1; void Update() { if (hp ! lastHp || maxHp ! lastMaxHp) { lastHp hp; lastMaxHp maxHp; hudText.text $HP: {hp} / {maxHp}; } }这一条不是字符串 API 层面的技巧却能直接砍掉大量无意义拼接。5.2 日志、路径与枚举 ToString隐藏的 GC 源头除了 Update 里的 UI另一个 GC 重灾区是日志和路径拼接。比如逐帧输出调试信息Debug.Log(Frame frame , state state.ToString());这里有两个问题的拼接产生临时字符串state.ToString()对枚举类型来说会产生装箱和一次字符串分配。枚举的ToString是通过反射找名字的调用成本比想象中高。优化方法有很多。团队里比较常用的是把枚举名提前缓存成数组private static readonly string[] StateNames { Patrol, Attack, Dead }; // 使用StateNames[(int)state]文件路径拼接也有类似问题。C# 里拼路径建议用Path.Combine不要用因为路径分隔符在不同平台下处理不一样。而 Asset 名或 Key 比较尽量用StringComparison.OrdinalIgnoreCase避免一次ToLowerInvariant()产生新字符串对象。5.3 TextMeshPro 的 SetText 与 StringBuilder 复用Unity 的 TextMeshPro 提供了一个更适合数值型 HUD 的接口SetText支持带占位符的模板并且内部会用预分配缓冲来格式化数字不需要每帧生成完整托管字符串tmpHpText.SetText(HP: {0} / {1}, hp, maxHp);这个 API 的实际零 GC 效果取决于 TMP 版本和使用姿势我自己实测下来它在高频数值刷新场景下比直接拼接或 string.Format 要好很多。注意{0}、{1}的占位符后面跟的参数是float或int调用时类型会被自动转换不需要你手动 ToString。如果我们需要组织一段较长的日志文本或导出数据用 StringBuilder 是另一个实用点StringBuilder sb new StringBuilder(256); sb.Clear(); sb.Append(timestamp).Append(timestamp); sb.Append(, player).Append(playerName); sb.Append(, event).Append(eventId); File.WriteAllText(path, sb.ToString());这里最值得注意的习惯是StringBuilder实例应该在循环外复用不要每次循环 new 一个。配合Clear()方法同一个对象可以服务整帧的大量拼接。6. 代码版速查手册C# String 与 Python str 常用方法对照表6.1 创建、判空与转换功能C#Python字符串字面量string s hello;s hello空字符串常量string.Empty判空string.IsNullOrEmpty(s)not s或s 判空或全空白string.IsNullOrWhiteSpace(s)not s.strip()数字转字符串num.ToString()str(num)字符转字符串ch.ToString()str(ch)特别提醒IsNullOrEmpty不是符串对象上的方法而是string类提供的静态方法。Python 里not s会把空字符串当作False但 C# 没有这种隐式布尔转换必须显式调用方法。6.2 查找、截取与裁剪功能C#Python长度s.Lengthlen(s)取单个字符/子串s[0]返回 chars[0]返回长度为 1 的 str截取s.Substring(1, 3)s[1:4]查找子串位置s.IndexOf(x)s.find(x)查找不存在IndexOf返回 -1find返回 -1index抛异常是否存在s.Contains(x)x in s前缀/后缀判断s.StartsWith(x)/s.EndsWith(x)s.startswith(x)/s.endswith(x)去除首尾空白s.Trim()s.strip()去除左侧/右侧空白s.TrimStart()/s.TrimEnd()s.lstrip()/s.rstrip()6.3 分割、拼接与替换功能C#Python按分隔符分割s.Split(,)s.split(,)按空白切且去空s.Split((char[])null, StringSplitOptions.RemoveEmptyEntries)s.split()数组拼接string.Join(, , arr), .join(arr)替换全部s.Replace(a, b)s.replace(a, b)限制次数替换需自己封装 ReplaceFirsts.replace(a, b, 2)删除指定区间s.Remove(0, 3)s[3:]或切片拼接插入s.Insert(0, prefix)prefix s这里最容易踩的坑是Split的空项处理。C# 默认保留空项Python 的split(,)也保留空项所以两者其实一致但 Python 的无参split()会去空这个行为需要你主动记下来。6.4 比较、格式化与性能工具功能C#Python内容相等判断s1 s2或s1.Equals(s2)s1 s2忽略大小写相等string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase)s1.casefold() s2.casefold()排序比较string.Compare(s1, s2, StringComparison.Ordinal)直接使用、格式化string.Format({0}-{1}, a, b){0}-{1}.format(a, b)插值语法${a}-{b}f{a}-{b}可变拼接StringBuilderlist.join(items)对比表看完以后你会发现两种语言的字符串包办能力其实差不多真正决定代码质量的不是某个 API 难学而是你是否掌握了 C# 不可变字符串背后的“更新即新建”原则。我在实际项目里的习惯是拿到一段字符串逻辑先问三个问题会不会高频执行、是否涉及本地化比较、是否能复用缓冲。这三个问题问完该用、Split还是StringBuilder基本就不会选错了。
返回列表