C#字符串长度全解析:码元、字符与字节长度的区别与应用
1. 从一次“字符截断”事故说起那天下午我正在调试一个处理多语言用户名的数据导出功能。逻辑很简单从数据库读取用户信息生成CSV文件。测试时一切正常直到一个日本用户的名字“山田 太郎”出现在列表里。导出的文件在其他系统导入时这个名字的后半部分被无情地截断了变成了乱码。问题出在哪里我用的明明是string.Length来判断长度并做截断啊。这个看似简单的“获取字符串长度”问题实际上埋着三个大坑字符长度、字节长度和特定编码如UTF-8下的字节长度。在C#里string.Length返回的是Unicode码元char的数量对于大部分英文字符这没问题一个char对应一个可视字符。但一旦遇到像“山田 太郎”里的“田”这样的字符或者更复杂的如“”这是一个需要两个char表示的补充平面字符string.Length告诉你的“长度”就和你在屏幕上看到的“字符数”以及存储或传输时需要的“字节数”完全不是一回事了。混淆这三者轻则导致UI显示错位、文本截断不美观重则会在网络传输、文件存储、数据库字段限制等场景下引发数据损坏、系统报错比如你搜索词里看到的“达梦 字段长度不够 报错字符串截断”、“指定的参数已超出有效值的范围”。今天我们就彻底把C#中字符串的这“三种长度”掰开揉碎讲清楚让你以后再也不踩这个坑。2. 核心概念拆解字符、码元、字节与编码在动手写代码之前必须把底层概念理清。这就像修车得先知道发动机、变速箱和底盘的区别。2.1 字符Character与码元Code Unit在C#中string本质上是一个char的只读集合。这里的char在.NET中是一个16位的值它表示一个UTF-16 码元。注意是“码元”不一定是完整的“字符”。基本多文种平面BMP字符例如英文字母‘A’、中文‘中’它们对应的Unicode码点可以用一个UTF-16码元表示。此时一个char对应一个可视字符string.Length等于字符数。补充平面字符例如“”U20BB7这是一个古汉字它的Unicode码点超过了UFFFF。在UTF-16编码中它需要用两个16位的码元来表示即一个代理项对。此时这个字符在string中占据两个char的位置string.Length返回2但对我们来说它只是一个“字符”。你可以用下面的代码直观感受string singleChar A; // 英文一个char string chineseChar 中; // 中文BMP字符一个char string surrogatePairChar ; // 补充平面字符两个char Console.WriteLine($‘A’ Length: {singleChar.Length}); // 输出 1 Console.WriteLine($‘中’ Length: {chineseChar.Length}); // 输出 1 Console.WriteLine($‘’ Length: {surrogatePairChar.Length}); // 输出 2所以string.Length告诉你的是码元char的数量而不是人类感知的字符数量。2.2 字节Byte与编码Encoding字符串在内存中以UTF-16码元序列形式存在。但当你要把它保存到文件、发送到网络或存入数据库尤其是非Unicode编码的数据库时它需要被转换成一连串的字节。这个转换规则就是字符编码。UTF-8变长编码一个字符可能用1到4个字节表示。英文数字是1字节大部分中文是3字节补充平面字符是4字节。它因兼容ASCII和空间效率高对英文内容而成为Web和存储的绝对主流这也是你搜索词中大量出现meta charsetutf-8的原因。UTF-16在.NET内部使用。BMP字符是2字节补充平面字符是4字节。GB2312/GBK中文传统编码一个中文字符通常占2字节。关键结论字符串的字节长度完全取决于你使用哪种编码。同一字符串“Hello 世界”用UTF-8、UTF-16和GBK编码后得到的字节数组长度是不同的。3. 实战如何获取三种不同的“长度”理解了理论我们来看C#中具体的获取方法。我会结合常见的使用场景和陷阱来讲解。3.1 获取码元长度string.Length这是最简单的也是最快的方法因为它直接返回内部char数组的长度。string text Hello 世界! ; int codeUnitLength text.Length; // 返回 11 // 分解H(1) e(1) l(1) l(1) o(1) 空格(1) 世(1) 界(1) !(1) 空格(1) (2) 11使用场景与坑场景快速进行字符串遍历、循环操作。例如简单的字符反转需注意代理项对、分配一个已知大小的char数组。大坑千万不要用它来做文本截断以适配UI显示或数据库字段长度这正是我开头踩的坑。如果你用text.Substring(0, 10)去截取上面的text你会得到“Hello 世界! ”最后一个“”字被从中间切开产生一个无效的代理项对导致乱码。3.2 获取字符数量字素簇感知的长度有时我们真的需要知道“看起来有几个字符”比如实现一个按“字符”限制的输入框。这需要用到StringInfo类。using System.Globalization; string text Hello 世界! ; TextElementEnumerator enumerator StringInfo.GetTextElementEnumerator(text); int characterCount 0; while (enumerator.MoveNext()) { characterCount; } Console.WriteLine($字符数量: {characterCount}); // 输出 10 // 分解H, e, l, l, o, 空格, 世, 界, !, 空格, 10个文本元素.NET 4.0 提供了更简洁的LengthInTextElements属性int charCount new StringInfo(text).LengthInTextElements; // 输出 10使用场景文本编辑器、聊天界面中光标位置的计算。实现符合用户直觉的字符串截断和长度限制。处理包含组合字符的文本如“c\u0327”显示为“ç”。3.3 获取字节长度指定编码这是网络编程、文件IO和数据库交互中最关键的一步。核心是使用System.Text.Encoding类的实例。3.3.1 获取UTF-8字节长度using System.Text; string text Hello 世界! ; Encoding utf8 Encoding.UTF8; // 方法1获取字节数组然后取长度最准确但产生了临时数组 byte[] bytes utf8.GetBytes(text); int byteLength bytes.Length; // 例如可能是 17 Console.WriteLine($UTF-8 字节长度 (通过GetBytes): {byteLength}); // 方法2使用GetByteCount避免分配字节数组推荐 int byteCount utf8.GetByteCount(text); Console.WriteLine($UTF-8 字节长度 (通过GetByteCount): {byteCount}); // 两种方法结果相同。为什么GetByteCount更优GetBytes需要实际执行编码操作并返回一个新的字节数组如果只关心长度这会产生不必要的内存分配垃圾回收压力。GetByteCount则只进行计算效率更高尤其是在频繁调用的热路径上。3.3.2 获取其他编码的字节长度方法完全一样只是换一个Encoding实例。Encoding gbk Encoding.GetEncoding(GBK); // 需要系统支持或注册编码 int gbkByteLength gbk.GetByteCount(text); // 中文字符在GBK下通常为2字节 Console.WriteLine($GBK 字节长度: {gbkByteLength}); Encoding unicode Encoding.Unicode; // 即 UTF-16LE int utf16ByteLength unicode.GetByteCount(text); Console.WriteLine($UTF-16 字节长度: {utf16ByteLength});3.4 综合对比与性能考量我们来一个完整的例子对比同一字符串的三种长度string demoText ABCDEF; // A,B,C,(代理项对),D,E,F Console.WriteLine($字符串: {demoText}); Console.WriteLine($string.Length (码元数): {demoText.Length}); // 输出 7 Console.WriteLine($StringInfo (字符数): {new StringInfo(demoText).LengthInTextElements}); // 输出 6 Console.WriteLine($UTF-8 字节数: {Encoding.UTF8.GetByteCount(demoText)}); // 输出 10 (A1B1C14D1E1F1) Console.WriteLine($UTF-16 字节数: {Encoding.Unicode.GetByteCount(demoText)}); // 输出 14 (每个码元2字节7*2) Console.WriteLine($ASCII 字节数 (无法编码的用‘?’替换): {Encoding.ASCII.GetByteCount(demoText)}); // 输出 7但‘’信息丢失注意GetByteCount是一个相对耗时的操作因为它需要遍历整个字符串并根据编码规则进行计算。对于超长字符串或性能敏感场景应避免在循环中反复调用。如果长度信息会被多次使用应先计算并缓存结果。4. 真实场景下的应用与避坑指南理论结合实践下面我们看看在具体开发中如何正确运用这些知识。4.1 场景一数据库字段长度校验这是最常见的坑。假设数据库有一个VARCHAR(10)字段使用的是UTF-8编码。你不能用string.Length来判断数据是否超长。public bool ValidateStringForDatabase(string input, int maxByteLength) { // 错误做法校验码元长度 // if (input.Length maxByteLength) return false; // 正确做法校验UTF-8字节长度 Encoding utf8 Encoding.UTF8; if (utf8.GetByteCount(input) maxByteLength) { // 超出限制可能需要截断或提示用户 return false; } return true; }进阶坑安全截断。当发现超长时直接按字节截断Substring会再次导致无效编码。必须使用编码感知的截断public string SafeTruncateForUtf8(string input, int maxByteLength) { Encoding utf8 Encoding.UTF8; if (utf8.GetByteCount(input) maxByteLength) return input; // 逐步减少字符直到字节长度符合要求 // 这是一个简单实现效率不高适用于不频繁调用的场景 for (int i input.Length - 1; i 0; i--) { string candidate input.Substring(0, i); if (utf8.GetByteCount(candidate) maxByteLength) { // 进一步检查确保截断点不在一个多字节字符的中间 // 更健壮的做法是使用Encoder.Convert或遍历编码 return candidate; } } return string.Empty; } // 生产环境建议使用更高效的算法或使用第三方库。4.2 场景二网络协议与API通信在定义网络协议或Web API如你搜索的C# WebAPI的数据格式时经常需要在报文头中指定后续字符串内容的字节长度。// 模拟构建一个协议包 [4字节长度][UTF-8字符串数据] public byte[] BuildPacket(string message) { Encoding utf8 Encoding.UTF8; byte[] dataBytes utf8.GetBytes(message); int dataLength dataBytes.Length; byte[] lengthBytes BitConverter.GetBytes(dataLength); // 将int转为4字节 byte[] packet new byte[4 dataLength]; Buffer.BlockCopy(lengthBytes, 0, packet, 0, 4); Buffer.BlockCopy(dataBytes, 0, packet, 4, dataLength); return packet; } // 解析包 public string ParsePacket(byte[] packet) { int dataLength BitConverter.ToInt32(packet, 0); // 前4字节是长度 // 注意这里假设packet长度正确实际需做边界检查 return Encoding.UTF8.GetString(packet, 4, dataLength); }这里的关键是写入的长度必须是编码后的字节长度而不是字符串的码元长度。4.3 场景三文件操作与编码声明当你用StreamWriter写文本文件或者像搜索热词里那样写HTML文件meta charsetutf-8时必须确保写入流的编码和声明的编码一致。// 正确明确指定UTF-8编码无BOM using (var writer new StreamWriter(output.html, false, new UTF8Encoding(false))) { writer.WriteLine(!doctype html); writer.WriteLine(html lang\zh-cn\); writer.WriteLine(headmeta charset\utf-8\); // ... 其余内容 }如果不指定编码StreamWriter默认会使用UTF-8 with BOM字节顺序标记。对于Web文件BOM可能不是必需的有时甚至会导致问题。new UTF8Encoding(false)参数false表示不包含BOM。4.4 场景四与外部系统或原生代码交互调用Windows API或其他通过P/Invoke交互的原生代码时参数常常要求是字节数组或指定编码的字符串。[DllImport(some.dll)] private static extern int SomeFunction(byte[] data, int length); public void CallNativeFunction(string message) { // 假设原生函数需要UTF-8编码的字节流 Encoding utf8 Encoding.UTF8; byte[] byteData utf8.GetBytes(message); int result SomeFunction(byteData, byteData.Length); // 传入的是字节长度 }这里传入的length必须是byteData.Length字节数而不是message.Length。5. 高级话题编码、性能与内存5.1 编码的选择与陷阱UTF-8 vs UTF-16在.NET内部用UTF-16与外部世界文件、网络交互时UTF-8是更通用、更节省空间对于混合文本的选择。这也是为什么JSON、XML等现代数据格式默认采用UTF-8。Encoding.Default慎用它获取的是系统当前的ANSI代码页在不同机器上可能不同中文Windows是GBK英文Windows是Windows-1252。用它编码的文本换台机器就可能乱码。对于需要持久化或跨系统交换的数据永远明确指定编码如UTF-8。BOM问题UTF-8的BOM是一个三字节的标记EF BB BF。对于纯文本文件或网络流BOM不是必须的甚至可能干扰解析例如PHP文件包含BOM会导致header已发送错误。使用new UTF8Encoding(false)来创建无BOM的编码器。5.2 性能优化技巧缓存Encoding实例Encoding.UTF8是一个静态属性返回的是缓存的单例可以直接使用。但对于自定义编码如Encoding.GetEncoding(GB18030)最好在类级别静态字段中缓存它避免每次调用都查找。private static readonly Encoding Gb18030 Encoding.GetEncoding(GB18030);重用字节数组在需要频繁编码/解码的高性能场景可以考虑重用字节数组使用Encoding.GetBytes(string, int, int, byte[], int)的重载避免重复分配。使用Span和Memory在.NET Core/.NET 5中可以利用SpanT和MemoryT进行零拷贝或堆栈分配操作进一步提升编码/解码性能。string text Hello World; Encoding utf8 Encoding.UTF8; int byteCount utf8.GetByteCount(text); Spanbyte buffer byteCount 256 ? stackalloc byte[byteCount] : new byte[byteCount]; int bytesWritten utf8.GetBytes(text, buffer); // 现在buffer中包含了编码后的数据无需额外分配数组。5.3 内存中的字符串表示理解这一点有助于调试和性能分析。一个string对象在内存中的大小不仅仅是字符数据。它包含对象头、长度信息和字符数组。对于包含大量非BMP字符代理项对的文本内存占用会是string.Length * 2字节每个char2字节加上对象开销。而当你用GetBytes将其转换为UTF-8字节数组后对于英文内容内存占用可能会减少但对于中文可能会增加因为UTF-8下一个中文通常3字节而UTF-16下是2字节。这不是一个需要时刻考虑的问题但在处理超大文本时选择合适的内存和序列化格式是有意义的。6. 常见错误排查与调试结合你的搜索热词我们看看一些典型错误“达梦 字段长度不够 报错字符串截断”这极大概率是用了string.Length去校验一个以字节为限制的数据库字段。解决方案就是改用Encoding.XXX.GetByteCount进行校验。“指定的参数已超出有效值的范围参数名index”如果在调用Substring或操作字符数组时出现此错误并且字符串可能包含代理项对那可能是因为你错误地假设了string.Length等于字符数导致索引计算错误。使用StringInfo类来按文本元素遍历才是安全的。“source file is not valid utf-8”尝试用UTF-8解码器去读取一个非UTF-8编码如GBK、带BOM的UTF-16的文件或者文件本身损坏。解决方法是先用二进制模式读取文件头部判断编码或用Encoding.GetEncoding尝试不同的编码并捕获DecoderFallbackException。try { string content File.ReadAllText(somefile.txt, Encoding.UTF8); } catch (DecoderFallbackException ex) { // 文件不是有效的UTF-8 // 尝试其他编码如 Encoding.Default string content File.ReadAllText(somefile.txt, Encoding.Default); }“c# 复制文件时 出现正由另一进程使用”虽然不直接相关但这也是常见IO错误。确保所有Stream、StreamReader/Writer都在using语句中以保证资源被正确释放。最后分享一个我自己的调试习惯在遇到字符串相关诡异问题时我会写一个小工具函数快速打印出字符串的所有“长度”和其十六进制表示这能立刻揭示问题本质。public static void DebugStringInfo(string s) { Console.WriteLine($字符串: ‘{s}‘); Console.WriteLine($string.Length: {s.Length}); Console.WriteLine($StringInfo.LengthInTextElements: {new StringInfo(s).LengthInTextElements}); Console.WriteLine($UTF-8 Bytes: {Encoding.UTF8.GetByteCount(s)}); Console.WriteLine($UTF-16 Bytes: {Encoding.Unicode.GetByteCount(s)}); Console.Write(UTF-16 Hex: ); foreach (char c in s) { Console.Write(${(int)c:X4} ); } Console.WriteLine(); }把这个函数扔进你的工具库下次再遇到字符串长度谜题时它会是你最好的侦探。

相关新闻

涂胶显影机(Track)各职级技术岗人才综合评估结论及录用判定标准

涂胶显影机(Track)各职级技术岗人才综合评估结论及录用判定标准

一、总则(终审核心逻辑) 本标准为Track技术招聘最终终审唯一依据,完全贯通公司整套Track招聘体系:各职级14维面试打分卡、两轮分层技术面试、HR分层分级面试、面试官权责体系、职级薪酬体系、法务竞业合规审核与竞业储备体系。 本制度固化三方评审权重,明确技术、HR、法…

2026/7/29 9:07:11阅读更多 →
STM32驱动OLED实战:从I2C通信到动态界面与性能优化

STM32驱动OLED实战:从I2C通信到动态界面与性能优化

1. 从点亮到炫技:为什么STM32驱动OLED是嵌入式入门的必修课如果你刚开始玩STM32,点亮一个LED灯可能是你的第一个“Hello World”。但很快你就会发现,那个闪烁的小灯带来的成就感,远不如在一块小小的OLED屏幕上看到自己绘制的图形、…

2026/7/29 9:05:10阅读更多 →
从零构建电子足球机器人:STM32与PID控制实战指南

从零构建电子足球机器人:STM32与PID控制实战指南

1. 项目概述:从“踢球”到“造球”的思维跃迁 “电子足球”这个名字,乍一听可能让人联想到FIFA、实况这类电子游戏。但今天要聊的,完全不是一回事。这是一个典型的创客比赛项目,它的核心不是操控屏幕里的虚拟球员,而是…

2026/7/29 9:05:10阅读更多 →
Frida内存分析进阶:手把手实现hexdump工具与逆向实战

Frida内存分析进阶:手把手实现hexdump工具与逆向实战

1. 项目概述:为什么内存分析是逆向工程的“眼睛” 在逆向工程的世界里,我们常常把目标应用比作一个黑盒。你看到的是它的输入和输出,但中间的处理逻辑、数据流转、状态变化,都隐藏在厚厚的代码和内存屏障之后。Frida,作…

2026/7/29 10:27:30阅读更多 →
测试工程师的成长地图:从初级到资深,这五年学什么

测试工程师的成长地图:从初级到资深,这五年学什么

系列专栏:测试工程师每日一博 Day 18 上一篇:[Day 17 测试工程师的代码评审:除了功能正确性之外还该看什么] 这一篇第一次跳出"工程细节",回到"人"。17 天讲完工具、流程、横切、闭环、总调用之后,该回答一个被很多测试工程师反复问的问题 —— 「这条路…

2026/7/29 10:27:30阅读更多 →
本地部署大模型实战:Llama 2-7B在RTX 3060上的高效运行方案

本地部署大模型实战:Llama 2-7B在RTX 3060上的高效运行方案

1. 为什么选择本地搭建大模型? 去年我在团队内部做技术分享时,发现超过80%的开发者对大模型还停留在"只能通过API调用"的认知阶段。实际上,随着模型量化技术和消费级硬件的发展,现在完全可以在本地笔记本上运行7B参数规…

2026/7/29 10:27:30阅读更多 →
MicroPython运算符与表达式:从基础概念到嵌入式开发实战

MicroPython运算符与表达式:从基础概念到嵌入式开发实战

1. 项目概述:从“加减乘除”到程序逻辑的构建“MicroPython运算符和表达式 - 1.2.3”这个标题,乍一看像是一本编程教材的章节索引,或者某个在线教程的进度条。对于任何一位准备踏入MicroPython世界,尤其是想在ESP32、RP2040这类微…

2026/7/29 10:27:30阅读更多 →
Text Generation WebUI:从零开始掌握大语言模型应用

Text Generation WebUI:从零开始掌握大语言模型应用

1. 项目概述Text Generation WebUI(又称oobabooga)是一个开源的文本生成Web界面,它让普通用户也能轻松使用各种大型语言模型(LLM)进行文本创作、对话交互等任务。这个项目最初由开发者oobabooga创建,目的是…

2026/7/29 10:27:30阅读更多 →
UniApp原生插件开发与离线打包调试实战指南

UniApp原生插件开发与离线打包调试实战指南

1. 项目概述:为什么需要掌握UniApp原生插件与离线打包? 如果你正在用UniApp开发跨平台应用,并且已经走到了需要调用手机硬件(比如蓝牙、NFC、特定传感器)或者集成第三方SDK(比如人脸识别、地图导航&#x…

2026/7/29 10:25:29阅读更多 →
覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

🔹 工具基础介绍 OpenClaw 是开源生态中一款实用性较强的本地智能工具,凭借本地离线运行、可视化图形操作和任务自动化三大核心特性,赢得了众多用户的青睐。与普通在线对话AI工具不同,它属于能够直接操控本机软硬件的智能数字员工…

2026/7/29 9:47:45阅读更多 →
伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

所谓液压伺服阀体的精密激光焊接,是用激光束对阀座壳体(通常为不锈钢或铝合金)进行密封焊接,使阀体在21-35MPa的高压液压油或压缩气体中长期运行而不发生介质泄漏。液压伺服阀是高端液压系统的"大脑"。从航空航天飞行控…

2026/7/29 7:00:19阅读更多 →
D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南 【免费下载链接】d2dx D2DX is a complete solution to make Diablo II run well on modern PCs, with high fps and better resolutions. 项目地址: https://gitcode.com/gh_mirrors/d2/d2dx 你是否还在…

2026/7/29 7:58:51阅读更多 →
28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“!

28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“!

28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“! 在构建复杂的 Agent 系统时,我们经常会遇到这样的场景:Agent 正在执行一个多步骤的任务,比如“下单购买商品”,但执行到一半时,我们…

2026/7/29 0:01:46阅读更多 →
自律同行,突破无界!NANK南卡正式官宣曾舜晞成为品牌代言人

自律同行,突破无界!NANK南卡正式官宣曾舜晞成为品牌代言人

近日,国际专注开放式技术研发的声学品牌Nank南卡,正式官宣实力艺人曾舜晞担任品牌代言人。消息一经发出便轰动全网。为什么耳机品牌不选择流量明星、老牌歌手?而且是选择曾舜晞?让我们一起来探索一下!比起短期的流量&a…

2026/7/29 0:01:46阅读更多 →
【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

一、本文介绍 🔥本文在RT-DETR多模态融合目标检测中引入RLAB残差线性注意力模块,可在不同模态特征交互阶段进行多次残差细化,使可见光、红外等特征在尺度、语义和空间位置上更好对齐;随后将细化特征与解码器输出拼接并生成Q、K、V,通过线性注意力自适应强化关键通道、目…

2026/7/29 0:01:46阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/28 20:22:24阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/29 4:31:51阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/28 2:35:58阅读更多 →