【仓颉语言入门 · 第二篇 第 3 课】
【仓颉语言入门 · 第二篇 第 3 课】命令行参数解析让程序听懂命令std.argopt上一课结尾说过我们的程序一直是哑巴——所有输入要么写死在代码里要么靠改文件。真实世界的工具第一件事就是听懂命令行tool.exe -i input.log -o out.txt --verbose这一串是什么-i后面跟的是文件路径--verbose是个开关中间还有位置参数——靠if (args[0] -i)一层层手工劈代码又长又容易错。这一课的标准库std.argopt把这件事变成声明式你只管声明我认识哪些参数解析、配对、校验、收集一次完成。学完顺手给第 2 课的日志工具装上真正的命令行入口。本文所有代码、输出与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行。目录第二篇导航第一篇《仓颉语言入门》共 30 课已完结环境搭建 → 语法 → 函数 → 类型系统 → 工程化 → 并发 → 两场实战。第二篇在更高的起点上继续共17 课、4 个模块模块课次内容一、补遗篇0106正则、时间日期、命令行参数、随机数、加密哈希、二进制处理二、数据库篇0710std.database.sql 入门与 CRUD、预编译与事务、手写 DAO、综合小实战三、进阶硬核篇1114反射、宏与 deriving、C 互操作 FFI、并发容器与对象池四、实战项目篇1517HTTP 服务端、多线程日志分析器、简易爬虫正则表达式std.regex时间日期处理std.time命令行参数解析本文随机数与概率std.random加密与哈希std.crypto二进制数据处理std.binary数据库入门std.database.sql 与 CRUD预编译、事务与防注入手写 DAO 数据访问层数据库综合小实战反射在运行时打量类型宏与 deriving让编译器帮你写代码与 C 互操作FFI并发容器与对象池实战三HTTP 服务端实战四多线程日志分析器实战五简易爬虫一、为什么需要参数解析先看我们一贯的做法。第 29 课的命令行小工具输入路径是写死在代码里的let path data/todo.txt // 想换个文件改代码、重新编译想灵活一点就得手工劈main的参数数组main(args: ArrayString): Int64 { // 手工劈参数一堆 if还要自己管下标越界、值配对、开关判断…… var i 0 while (i args.size) { if (args[i] -i i 1 args.size) { let path args[i 1] i 2 } // -o 呢--verbose 呢-i 后面忘给值呢不认识的参数呢 } 0 }这一坨还没写完就已经能预见它的下场每种新选项都要加一个 if带值选项要自己看下一个元素开关和带值选项要分别处理用户写错了还得自己想报错文案。而真实命令行比你想象的更野-v -i a.log和-vi a.log是一个意思--inputa.log和--input a.log也是一个意思——手工劈参数要把这些姿势全都照顾到不现实。std.argopt的思路是声明式先用ArgumentSpec列一张我认识哪些参数的规格表再把参数数组整个交给parseArguments它负责配对、识别各种写法、收集位置参数不认识的东西直接抛异常你 catch 住打印用法就行。声明一次姿势全管。先认全四个主角后面逐一展开类型/函数角色ArgumentSpec一条参数规格叫什么名、带不带值ArgumentMode取值模式开关 / 必带值 / 值可带可不带parseArguments解析入口吃进参数数组和规格表吐出结果ParsedArguments解析结果选项值表 位置参数列表二、第一行参数代码声明规格交给解析器2.1 main 的参数从哪来先补一个前置事实第 1 课实测过这里正式讲仓颉的入口有两种写法带参数的版本是main(args: ArrayString)。args里没有程序名args[0]直接就是第一个参数——这和 C/Java 的argv[0]是程序名不一样main(args: ArrayString): Int64 { println(收到 ${args.size} 个参数) 0 }用 cjpm 传参--之后的内容会原样交给程序cjpm run -- hello.txt收到 1 个参数实测注意Windows 下给cjpm run --传中文参数会报IllegalArgumentException: Invalid unicode scalar value.第 1 课踩过的坑编码链路问题。本课示例一律用 ASCII 参数中文内容建议走配置文件或环境变量第 24 课之后还会再遇到这个话题。2.2 最小可用例子直接上代码——一个指定输入文件 可开详细模式的小程序import std.argopt.* main(args: ArrayString): Int64 { // 1. 先声明我认识哪些参数-i/--input 必带值-v/--verbose 是开关 let specs [ ArgumentSpec.Full(input, ri, ArgumentMode.RequiredValue), ArgumentSpec.Full(verbose, rv, ArgumentMode.NoValue) ] // 2. 把命令行参数交给解析器 let pa parseArguments(args, specs) // 3. 按名字取值get 返回 OptionString没给就是 None if (let Some(p) - pa.options.get(input)) { println(输入文件${p}) } if (pa.options.get(verbose).isSome()) { println(详细模式开) } // 4. 位置参数不带横线的 for (n in pa.nonOptions) { println(位置参数${n}) } 0 }编译运行在 cjpm 工程里cjpm run ---idata/app.log-vextra.txt输入文件data/app.log 详细模式开 位置参数extra.txt逐行拆解ArgumentSpec.Full(input, ri, ArgumentMode.RequiredValue)一条完整规格——长选项叫--input短选项叫-iri是 Rune 字面量单个字符必须带值。三种取值模式下一节细讲。parseArguments(args, specs)两个位置参数——参数数组 规格数组返回ParsedArguments。pa.options.get(input)options是一张选项名 → 值的表get返回OptionString——第 9 课的老朋友可能没有用 Option 显式表达。if (let Some(p) - ...)解包。pa.options.get(verbose).isSome()开关型选项的判断方式——出现了就是 Some没出现就是 None值本身是空串判断有没有不看值看键。pa.nonOptions所有不带横线的位置参数原样收集成ArrayString。注意第 2 步开始“-i 后面跟谁”、用户写没写 -v这些配对逻辑全部消失——这就是声明式的价值。三、三种取值模式ArgumentModeArgumentMode是一个枚举只有三个成员对应三种这个选项怎么取值模式含义典型场景NoValue纯开关出现即生效--verbose详细输出、--force强制执行RequiredValue必须带值缺值报错-i 文件路径、-o 输出文件OptionalValue值可带可不带--level不给值用默认级别给了用指定级别一次看清三种模式的行为这回不经过命令行直接把参数数组硬编码传给解析器方便逐场景对照import std.argopt.* main(): Int64 { let specs [ ArgumentSpec.Full(debug, rd, ArgumentMode.NoValue), ArgumentSpec.Full(output, ro, ArgumentMode.RequiredValue), ArgumentSpec.Long(level, ArgumentMode.OptionalValue) ] // NoValue不给 -dget 回 None let r1 parseArguments([-o, out.txt], specs) println(没开 debug${r1.options.get(debug)}) println(output${r1.options.get(output)}) // 给了 -d键出现值是空串 let r2 parseArguments([-d, -o, out.txt], specs) println(开了 debug${r2.options.get(debug)}) // OptionalValue--level3 有值--level 不带值 let r3 parseArguments([--level3], specs) println(level3${r3.options.get(level)}) let r4 parseArguments([--level], specs) println(level 不带值${r4.options.get(level)}) 0 }没开 debugNone outputSome(out.txt) 开了 debugSome() level3Some(3) level 不带值Some()三个实测结论都是容易想错的地方开关没开是None开了是Some()。Some()看着奇怪其实是Some()——开关型选项出现时值是空串。所以判断开关永远用isSome()别去比较值。OptionalValue不带值时也是空串Some()带值时才是真正的值。想区分给了--level但没给值和压根没给值——前者是Some()后者是None。业务上常见写法var level 1 if (let Some(v) - pa.options.get(level)) { if (v ! ) { level Int64.parse(v) // 给了值才覆盖默认 } }RequiredValue缺值不是返回 None而是直接抛异常——严格模式下一节马上讲报错文案。另外注意规格表里混进了新面孔ArgumentSpec.Long(level, ...)——只声明长选项、不配短选项。四种声明写法下一节集齐。四、ArgumentSpec 的四种写法ArgumentSpec有四个静态构造入口覆盖全部声明需求写法签名效果FullFull(长名, 短字符, 模式)长短两个名字都认LongLong(长名, 模式)只认长选项ShortShort(短字符, 模式)只认短选项NonOptionsNonOptions(回调)位置参数的集中处理入口4.1 Full 的双键登记import std.argopt.* main(): Int64 { let specs [ ArgumentSpec.Full(input, ri, ArgumentMode.RequiredValue), ArgumentSpec.Long(output, ArgumentMode.RequiredValue), ArgumentSpec.Short(rv, ArgumentMode.NoValue) ] let pa parseArguments([-i, a.log, --output, b.log, -v], specs) // Full短名、长名各自是一个键都能取到 println(用 i 取${pa.options.get(i)}) println(用 input 取${pa.options.get(input)}) // Long只有长名一个键 println(output${pa.options.get(output)}) // Short只有短名一个键 println(v${pa.options.get(v)}) 0 }用 i 取Some(a.log) 用 input 取Some(a.log) outputSome(b.log) vSome()Full声明的选项短名和长名在结果表里是两个独立的键都指向同一个值。这个设计利弊参半好处是你用哪个名字取都行坏处是遍历结果表时会看到i、input各出现一次遍历顺序不保证别依赖顺序以及判断用户到底用的短名还是长名做不到——解析器不区分。按名取值时习惯统一用长名。4.2 NonOptions位置参数的回调前面pa.nonOptions是事后拿位置参数ArgumentSpec.NonOptions提供事中入口——解析时回调收到全部位置参数适合集中登记import std.argopt.* main(args: ArrayString): Int64 { let specs [ ArgumentSpec.Full(verbose, rv, ArgumentMode.NoValue), // NonOptions收到全部位置参数的回调在这里集中处理 ArgumentSpec.NonOptions({ files: ArrayString println(待处理文件 ${files.size} 个) for (f in files) { println( ${f}) } }) ] let pa parseArguments(args, specs) // 回调之外nonOptions 属性里同样拿得到 for (f in pa.nonOptions) { println(nonOptions 里也有${f}) } 0 }运行cjpm run -- -v a.txt b.png待处理文件 2 个 a.txt b.png nonOptions 里也有a.txt nonOptions 里也有b.png回调参数是ArrayStringlambda 参数记得写类型标注第 13 课的规矩。回调内外两份数据一致用哪个都行——但回调里不能修改外部的 var 变量比如在回调里累加计数会编译报错报错文案见第九节 FAQ要做统计就在回调外对着pa.nonOptions做。五、解析器的五条规则parseArguments认哪些姿势五条规则全部实测。还是硬编码参数数组一个show辅助函数逐场景打印import std.argopt.* func show(tag: String, argv: ArrayString, specs: ArrayArgumentSpec): Unit { println( ${tag} ) let pa parseArguments(argv, specs) println( i ${pa.options.get(i)}v ${pa.options.get(v)}) print( 位置参数) for (n in pa.nonOptions) { print(${n} ) } println() } main(): Int64 { let specs [ ArgumentSpec.Full(input, ri, ArgumentMode.RequiredValue), ArgumentSpec.Full(verbose, rv, ArgumentMode.NoValue) ] // 规则一长选项可以用等号连值 show(等号形式, [--inputa.log], specs) // 规则二-- 之后的全是位置参数横线不再特殊 show(双横线分隔, [-i, a.log, --, -v, x.txt], specs) // 规则三短选项可以连写带值的排最后 show(短选项连写, [-vi, a.log], specs) // 规则四短选项的值可以直接贴在选项后面 show(短选项值连写, [-ia.log], specs) // 规则五同名选项重复出现后者覆盖前者 show(重复覆盖, [-i, a.log, -i, b.log], specs) 0 } 等号形式 i Some(a.log)v None 位置参数 双横线分隔 i Some(a.log)v None 位置参数-v x.txt 短选项连写 i Some(a.log)v Some() 位置参数 短选项值连写 i Some(a.log)v None 位置参数 重复覆盖 i Some(b.log)v None 位置参数五条规则汇总#写法行为1--inputa.log等号连值与--input a.log等效2--单独出现之后所有元素原样进位置参数-v也不再当选项3-vi a.log短选项连写v 是开关最后一位 i 接后面的 a.log 当值4-ia.log短选项值连写i 的值就是紧贴着的 a.log5-i a -i b同名重复后者覆盖前者想收集全部同名值得自己用位置参数或多个不同名选项规则二值得单独说一句--是命令行的老约定用来保护长得像选项的参数——比如要删一个名叫-v的文件rm -- -v。解析器内建了这个约定不用你操心。六、错误处理ArgumentParseExceptionparseArguments是严格模式不认识的东西不装看不见直接抛ArgumentParseException。两种典型报错实测文案如下import std.argopt.* func tryParse(tag: String, argv: ArrayString, specs: ArrayArgumentSpec): Unit { println( ${tag} ) try { let pa parseArguments(argv, specs) println( 解析成功i ${pa.options.get(i)}) } catch (e: ArgumentParseException) { println( 参数错误${e.message}) } } main(): Int64 { let specs [ArgumentSpec.Full(input, ri, ArgumentMode.RequiredValue)] tryParse(未知选项, [-z], specs) tryParse(必带值选项缺值, [-i], specs) tryParse(正常, [-i, ok.log], specs) 0 } 未知选项 参数错误Unknown option: z. 必带值选项缺值 参数错误Missing option value for i. 正常 解析成功i Some(ok.log)两条文案的规律场景报错文案规格表里没有的选项Unknown option: z.末尾带句号RequiredValue选项没拿到值Missing option value for i.末尾带句号为什么设计成直接抛而不是把-z当位置参数放过命令行工具的第一原则是fail fast用户打错参数最负责的做法是立刻停下来告诉他哪里错了而不是揣着错误参数继续跑。所以标准姿势是try-catch包住解析catch里打印e.message 用法说明返回非 0 退出码——下一节实战就是这个套路。七、CIDE 实操给日志巡检装上命令行入口第 2 课的日志巡检工具日志路径是写死的。现在用std.argopt给它装上真正的命令行入口需求-l/--level 级别要过滤的日志级别必填-i/--input 路径日志文件路径选填缺省用data/app.log-v/--verbose开关打开时额外打印读了多少条、命中多少条。7.1 数据文件沿用第 2 课的data/app.log放在工程根目录2026-10-09 08:30:15 [INFO] auth - 用户 user01 从 192.168.1.23 登录 2026-10-09 08:31:02 [ERROR] db - 连接超时: 192.168.1.10:5432 2026-10-09 08:31:45 [WARN] auth - 密码重试过多: user07 2026-10-09 08:32:20 [INFO] api - 请求 /api/orders 耗时 42ms 2026-10-09 08:33:05 [ERROR] api - 调用支付网关失败: 10.0.0.8 2026-10-09 08:34:10 [INFO] auth - 用户 13812345678 修改了手机号 139988877667.2 完整代码src/main.cj全文文件读取是第 22 课的老三样按行过滤用的是第 6 课的字符串方法import std.argopt.* import std.fs.* import std.unicode.* main(args: ArrayString): Int64 { // 1. 参数规格-i/--input 日志路径默认 data/app.log、-l/--level 过滤级别、-v/--verbose 详细输出 let specs [ ArgumentSpec.Full(input, ri, ArgumentMode.RequiredValue), ArgumentSpec.Full(level, rl, ArgumentMode.RequiredValue), ArgumentSpec.Full(verbose, rv, ArgumentMode.NoValue) ] try { // 2. 解析 let pa parseArguments(args, specs) // 3. 过滤级别是必填项 var level if (let Some(lv) - pa.options.get(level)) { level lv } else { println(缺少过滤级别-l INFO|WARN|ERROR) return 1 } // 4. 日志路径给了 -i 就用否则用默认 var path data/app.log if (let Some(p) - pa.options.get(input)) { path p } let verbose pa.options.get(verbose).isSome() if (verbose) { println(日志路径${path}) println(过滤级别${level}) println(----) } // 5. 读文件、按级别过滤第 2 课的老三样 let raw String.fromUtf8(File.readFrom(path)) let tag [${level}] var total 0 var hit 0 for (line0 in raw.split(\n)) { let line line0.trim() if (line ) { continue } total 1 if (line.contains(tag)) { hit 1 println(line) } } if (verbose) { println(----) println(共 ${total} 条日志其中 ${level} 级 ${hit} 条) } } catch (e: ArgumentParseException) { // 6. 参数本身不合法缺值、不认识的选项 println(参数错误${e.message}) println(用法logtool -l 级别 [-i 日志路径] [-v]) return 1 } catch (e: Exception) { // 7. 运行期错误如文件不存在 println(运行出错${e.message}) return 1 } 0 }7.3 五种运行场景全部实测cwd 为工程根目录cjpm run ---lERROR-v日志路径data/app.log 过滤级别ERROR ---- 2026-10-09 08:31:02 [ERROR] db - 连接超时: 192.168.1.10:5432 2026-10-09 08:33:05 [ERROR] api - 调用支付网关失败: 10.0.0.8 ---- 共 6 条日志其中 ERROR 级 2 条不带-v只出结果cjpm run ---lWARN2026-10-09 08:31:45 [WARN] auth - 密码重试过多: user07什么都不传——业务层校验拦住-l是必填项cjpm run --缺少过滤级别-l INFO|WARN|ERROR-l后面忘给值——解析层抛异常catch 里打印用法cjpm run ---l参数错误Missing option value for l. 用法logtool -l 级别 [-i 日志路径] [-v]指定一个不存在的文件——第二个 catch 接住运行期错误报错文案里的绝对路径随运行目录而变cjpm run ---lERROR-inofile.log运行出错The file C:\...\s2argopt\nofile.log does not exist or permission denied!7.4 逐段看懂规格声明三个Full长短名成对给——-l与--level等价用户爱用哪个用哪个。level用RequiredValue解析层保证写了 -l 就一定有值-l 到底写没写是业务层的事所以还有 3 号段的手工校验。双层校验解析层的异常管参数语法缺值、未知选项业务层的 if 管参数语义必填没填。两层各司其职报错文案也不同——语法错给用法语义错给提示这是命令行工具的基本修养。双 catchArgumentParseException管参数问题Exception兜底其他运行期错误文件不存在就是它接住的。catch 的顺序无关紧要两个类型不重叠。默认值套路var path data/app.log先给默认if (let Some(p) - ...)有覆盖——这是选填参数的标准写法比OptionalValue模式更直白推荐。对比第 2 课的写死版本想换日志文件不用改代码了想只看 ERROR 不用注释 INFO 的处理逻辑了——工具的可复用一大半来自参数化。第 29 课的待办小工具等你做完本课练习就可以用同样的方式升级它。八、常用 API 速查API说明ArgumentSpec.Full(长名: String, 短字符: Rune, 模式: ArgumentMode)长短双名选项结果表里短名、长名各登记一个键ArgumentSpec.Long(长名: String, 模式: ArgumentMode)只认长选项ArgumentSpec.Short(短字符: Rune, 模式: ArgumentMode)只认短选项ArgumentSpec.NonOptions(回调: (ArrayString) - Unit)位置参数集中处理回调回调内不能改外部 varArgumentMode.NoValue开关出现即生效值登记为空串ArgumentMode.RequiredValue必带值缺值抛ArgumentParseExceptionArgumentMode.OptionalValue值可带可不带不带值登记为空串parseArguments(args: ArrayString, specs: ArrayArgumentSpec)解析入口返回ParsedArguments不认识的东西直接抛异常ParsedArguments.optionsReadOnlyMapString, String选项名 → 值含短名、长名两套键ParsedArguments.nonOptionsArrayString全部位置参数options.get(名)OptionString没出现是 None判断开关用.isSome()ArgumentParseException.message报错详情Unknown option: z./Missing option value for i.解析器五条姿势全部内建无需配置--名值等号连值--之后全部当位置参数短选项可连写-vi a.log带值的排最后短选项值可连写-ia.log同名选项后者覆盖前者。九、常见问题 FAQQ1网上资料里的ArgOpt类ArgOpt(i:o:v, ...)那种能用吗能用但别用——它已被标记废弃1.2.0 编译时会给警告实测文案warning: class ArgOpt is deprecated. Use global function public func parseArguments(args: ArrayString, specs: ArrayArgumentSpec): ParsedArguments instead.警告里已经把替代方案写得明明白白parseArgumentsArgumentSpec也就是本课教的这套。Q2开关型选项的值为什么是空串判断开没开怎么办解析器的登记策略选项出现就登记键没值就登记空串。所以Some()即Some()代表出现了None代表没出现。判断永远用isSome()不要去比较值是不是空串——那是OptionalValue区分给了没值/没给时才需要的事。Q3在NonOptions回调里累加计数报lambda capturing mutable variables needs to be called directly这是仓颉 lambda 的通用规则第 13 课讲过lambda 捕获可变变量var有严格限制只有定义后立刻直接调用的 lambda 才能捕获 var。传给NonOptions的回调是被解析器稍后调用的捕不了 var。实测报错error: lambda capturing mutable variables needs to be called directly note: sum is mutable解法回调里只做打印/登记这类只读的事统计逻辑放在回调外对着pa.nonOptions做见练习 4。Q4args里为什么没有程序名想拿程序名怎么办仓颉的main(args: ArrayString)设计成纯参数args[0]就是第一个用户参数和 C/Java 的argv[0]是程序名不同。一般工具用不到程序名真需要时用固定字符串打印即可报错用法里的logtool就是手写的。Q5给程序传中文参数为什么报IllegalArgumentException: Invalid unicode scalar value.第 1 课实测过的 Windows 编码链路问题cjpm run -- 中文参数在传参过程中会把中文打碎成非法字节序列。规避方式命令行参数一律用 ASCII路径、级别名都可以做到中文内容放进文件让程序去读。这个坑与std.argopt无关是平台链路问题但实测确实会撞上提前绕开。十、课后练习练习原则照旧每题只考本课知识点题面给全提示先想清楚规格表再动手。前 4 题必做第 5 题选做。1.必做–help 用法开关。很多工具都有-h/--help给了就打印用法没给就正常干活。用Full(help, rh, ArgumentMode.NoValue)isSome()实现。提示import std.argopt.*判断用pa.options.get(help).isSome()。import std.argopt.* main(args: ArrayString): Int64 { let specs [ArgumentSpec.Full(help, rh, ArgumentMode.NoValue)] let pa parseArguments(args, specs) if (pa.options.get(help).isSome()) { println(用法tool [-h]) println( -h, --help 显示本帮助) } else { println(开始干活) } 0 }期望输出两次运行cjpm run -- -h → 用法tool [-h] -h, --help 显示本帮助 cjpm run -- → 开始干活2.必做问候工具。实现-n/--name 名字给了就打印你好名字没给就打印请用 -n 告诉我你的名字。提示RequiredValueif (let Some(n) - pa.options.get(name))验证时参数用英文Windows 中文参数坑见 FAQ Q5。期望输出两次运行第二次不带参数cjpm run -- -n Cangjie → 你好Cangjie cjpm run -- → 请用 -n 告诉我你的名字3.必做读程序写输出。规格表如下-i/--input必带值-v/--verbose开关let specs [ ArgumentSpec.Full(input, ri, ArgumentMode.RequiredValue), ArgumentSpec.Full(verbose, rv, ArgumentMode.NoValue) ]分别用三组参数解析后打印i、v两个键的值①[-ia.log]②[-v, -i, b.log]③[--inputc.log, x.txt]第③组还要打印位置参数个数。先在纸上写答案再运行验证——考的是第五节的五条规则。期望输出① iSome(a.log) vNone ② iSome(b.log) vSome() ③ iSome(c.log) vNone 位置参数 1 个4.必做位置参数求和。命令行传来若干个整数如cjpm run -- 10 20 30求总和。提示ArgumentSpec.NonOptions回调里打印收到几个数求和别在回调里做回调捕不了 var见 FAQ Q3在回调外对pa.nonOptions循环Int64.parse转整数记得import std.convert.*。期望输出收到位置参数 3 个 数字个数3 总和605.选做缺值的友好提示。-o/--output是RequiredValue。用户只写了-o忘给值时解析器抛的Missing option value for o.对普通用户不够友好——catch 住它额外打印一行人话提示。提示第六节的try-catch套路e.message拿原始文案。期望输出两次运行cjpm run -- -o result.txt → 输出到result.txt cjpm run -- -o → 参数错误Missing option value for o. 提示选项 o 需要一个值例如 -o result.txt下节预告程序听话之后下一个需求是随机抽奖、模拟、生成测试数据、随机打乱——都绕不开随机数。下一课随机数与概率std.random讲清楚三件事怎么取一个随机整数/浮点、怎么从一半概率到按权重抽奖控制概率分布、以及为什么默认的随机数每次运行都不一样种子的秘密。顺手给本课的 logtool 加一个随机抽查 N 条日志的功能。系列说明本系列基于 Windows 平台 CIDE 仓颉 SDK1.2.0编写所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入以你本地版本为准欢迎评论区交流。 工具下载本系列全程使用的仓颉 IDE ——CIDE免费开源、社区版GitCode 仓库 / 安装包下载https://gitcode.com/wp_upala/cide打开页面后进入发行版Releases两种包任选其一安装版下载CIDE-版本-x64-Setup.exe双击安装适合日常长期使用免安装版Portable下载CIDE-版本-x64-Portable.zip解压到任意目录即用不写注册表、不留安装痕迹拷到 U 盘也能在别的电脑直接运行包内附《使用说明.txt》。适合先试用、或在受限电脑上学习本系列课程。仓颉 SDK 请前往仓颉编程语言官网下载https://cangjie-lang.cn

相关新闻

摄像头采集核心与 D3D11 渲染集成说明

摄像头采集核心与 D3D11 渲染集成说明

1. 工程关系 04_Camera_D3D11_Preview 没有复制摄像头采集代码,也没有引用外部摄像头仓库。 它复用同一 AV_Atomic_Playground 工程中 02_Video_Capture 提供的 video_capture_core 静态库。 02_Video_Capture └── video_capture_core├── media_foundation_capture.cp…

2026/10/13 15:06:39 阅读更多 →
星哥带你玩飞牛NAS-36:给NAS装个老游戏ROM ,浏览器直接开玩

星哥带你玩飞牛NAS-36:给NAS装个老游戏ROM ,浏览器直接开玩

星哥带你玩飞牛NAS-36:给NAS装个老游戏ROM ,浏览器直接开玩 导读: 谁的硬盘里没存过几个 GBA、FC 的 ROM 呢?文件堆了一堆,封面没几张,每次想玩都得翻半天目录。今天介绍一个开源项目 RomM,把 …

2026/10/13 15:06:39 阅读更多 →
Windows进程管理实战:任务管理器操作与API原理全解析

Windows进程管理实战:任务管理器操作与API原理全解析

简介:这份实验指导文档围绕 Windows 任务管理器的进程管理展开,适合操作系统课程实验教学、期末复习或自学巩固。文档取材自经典实验教材,以“背景知识—实验目的—工具/准备—实验内容与步骤—知识点”为主线,先介绍任务管理器“…

2026/10/13 15:06:39 阅读更多 →

最新新闻

选择排序与插入排序:手写背后的底层直觉与工程权衡

选择排序与插入排序:手写背后的底层直觉与工程权衡

1. 为什么今天还要手写选择排序和插入排序?——从“能跑通”到“真理解”的分水岭很多人看到“算法-6-选择排序插入排序”这个标题,第一反应是:这都2024年了,Python里一个sorted()、JavaScript里一个.sort()就搞定的事&#xff0c…

2026/10/13 15:56:01 阅读更多 →
FFmpeg 3.4.2 离线包部署指南:解压、环境配置与常见转码命令详解

FFmpeg 3.4.2 离线包部署指南:解压、环境配置与常见转码命令详解

简介:这是一份FFmpeg 3.4.2稳定版的Windows预编译库资源,面向需要在应用程序中集成音视频处理能力的C/C开发者。包内同时提供32位与64位动态库,涵盖DLL运行库、配套导入库及头文件,开发者可基于这些文件快速完成环境配置&#xff…

2026/10/13 15:56:01 阅读更多 →
LiteLLM实践:多模型API统一接入与代理网关

LiteLLM实践:多模型API统一接入与代理网关

最近在做大模型应用落地,手里同时握着好几家主流模型的 API Key,结果发现最痛苦的其实不是效果调优,而是工程侧怎么把这么多接口统一管理起来。各家调用的报文格式不一样、鉴权方式不一样、返回结构也不一样,模型一多,…

2026/10/13 15:56:01 阅读更多 →
C#对接ONVIF摄像机:从发现设备到RTSP取流与PTZ云台控制的完整链路

C#对接ONVIF摄像机:从发现设备到RTSP取流与PTZ云台控制的完整链路

简介:面向需要在C#项目中集成Onvif协议的开发者,这份Demo程序提供了一套亲测可用的摄像机对接方案,涵盖RTSP视频流地址获取、PTZ云台控制、预置位设置与调用,并借助VLC 3.0.4.0实现视频流播放,同时附带获取RTSP地址的W…

2026/10/13 15:56:01 阅读更多 →
信贷决策建模:特征工程、评分卡与可解释性实战解析

信贷决策建模:特征工程、评分卡与可解释性实战解析

简介:2020年数学建模C题企业信贷风险预测的完整解答文档,面向备战数模竞赛、希望掌握机器学习风控建模的读者。文档围绕“量化分析—特征工程—模型构建—评估优化”主线展开:先将企业信贷违约风险转化为可量化评分,再从进项与销项…

2026/10/13 15:56:01 阅读更多 →
智能体运营蓝图:从系统打通到互联企业的实践路线

智能体运营蓝图:从系统打通到互联企业的实践路线

我做了不少企业数字化项目的咨询,一个感受特别深:很多公司系统上了不少——CRM、ERP、MES、OA,一个都不少,但业务跑起来还是靠人在几个系统之间来回搬运信息。说要"互联",最后连了个寂寞。这两年智能体&…

2026/10/13 15:55:00 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →