尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

多语言写法对比:从JavaScript箭头函数到MyBatis动态SQL的工程权衡

多语言写法对比:从JavaScript箭头函数到MyBatis动态SQL的工程权衡 1. 引言一种写法走天下真的够用吗先聊一个我在开发中经常遇到的场景同一个功能不同的人写出来的代码风格完全不同。比如同一句 SQL 动态查询有人只会用if标签拼条件有人却能用choose、trim或者foreach组合出更优雅的写法。再比如同一个函数有人习惯写普通函数有人坚持箭头函数有人一门心思用return返回错误有人在 Go 里用options模式管理配置参数。这些差异本身没有绝对的对错但在不同的业务场景下某一种写法可能明显优于另一种。如果一个人只掌握一种写法遇到复杂需求就会陷入“能跑但不好维护”的困境甚至写出有隐患的代码。这篇文章的出发点就是“不要局限于一种写法”。我会结合实际开发中最常见的四类场景JavaScript 中普通函数与箭头函数的写法差异MyBatis 的 mapper.xml 中动态 SQL 的多种写法Go 语言工程化中常见的代码组织写法C 交互题中的输入输出与函数封装写法。通过这四组对比希望大家建立一种“写法思维”面对需求时先想一想有没有更多解法再结合语义、性能、可读性和团队规范选出最合适的那一个。文章不是要告诉你“哪种写法最好”而是帮你把工具箱里的工具变多。2. 为什么说“不要局限于一种写法”2.1 写法的本质是工程权衡很多初学者喜欢背“最佳实践”比如“永远用箭头函数因为它简洁。”“MyBatis 动态 SQL 就用if够用了。”“Go 的错误处理就是if err ! nil没有别的写法。”“C 里就用cin和cout别用scanf。”这些说法都有一定道理但它们都只是某一场景下的经验不是放之四海而皆准的真理。写法的背后本质是工程权衡。拿 JavaScript 来说普通函数和箭头函数在this绑定上完全不同。箭头函数没有自己的this它会捕获定义时所在上下文的this。这在回调函数中非常方便但你如果想让一个对象方法使用动态this箭头函数就会出问题。再拿 C 交互题来说cin与scanf的差距在百万级输入数据上非常明显。你当然可以说“现代 C 已经通过ios::sync_with_stdio(false)优化了输入速度”但如果你在一个不支持关同步的评测环境里或者需要读取二进制数据scanf仍然是更稳妥的选择。2.2 场景决定写法单一写法的最大问题是它剥夺了你在具体场景中选择最优解的能力。举一个很简单的例子// 场景 A需要判断一个对象是否为空为空则返回默认值 String name user.getName(); if (name null) { name 默认用户; }这段代码写起来没问题但换个写法呢// 写法二使用三元表达式 String name user.getName() ! null ? user.getName() : 默认用户;再换一种// 写法三使用 OptionalJava 8 String name Optional.ofNullable(user.getName()).orElse(默认用户);三种写法都能实现同样的功能哪一种更好取决于团队规范、项目 JDK 版本、代码可读性要求等。如果你只会第一种虽然不会犯错但面对复杂分支逻辑时可能写出很长很冗余的代码。所以“不要局限于一种写法”并不是要求你追求花哨的语法技巧而是希望你在理解每种写法的语义、性能、可读性之后能做出合理的选型。2.3 对工程化意味着什么在多人协作的工程里写法的一致性和可维护性往往比“运行速度”更重要。一个能写出多种写法的开发者更有可能在代码评审中提出更合理的建议在接手历史代码时更快理解别人的思路在性能调优时找到瓶颈代码的替代写法在框架升级时平滑迁移到新语法。这些能力都需要你平时主动积累不同写法而不是停留在舒适区。3. JavaScript 写法对比普通函数与箭头函数先来看一个最常见的争议点普通函数 vs 箭头函数。3.1 最基本的形式差异普通函数的标准写法是function add(a, b) { return a b; }箭头函数则更简洁const add (a, b) a b;两者在简单计算场景下结果一致但它们的语义并不相同。箭头函数有几个典型特点没有自己的this没有arguments对象不能作为构造函数使用不能使用yield因此不能作为生成器函数。3.2this绑定的差异普通函数的this由调用方式决定而箭头函数的this由定义位置决定。来看一个实际项目中常见的场景class Counter { constructor() { this.count 0; } start() { setInterval(function () { // 这里 this 指向 window非严格模式或 undefined严格模式 this.count; console.log(this.count); }, 1000); } }这段代码会报错因为setInterval中的普通函数其this并不指向Counter实例。常规修法是用箭头函数class Counter { constructor() { this.count 0; } start() { setInterval(() { // 箭头函数捕获定义时的 this即 Counter 实例 this.count; console.log(this.count); }, 1000); } }箭头函数在这个场景下非常合适因为它定义在start方法内部this自然指向当前实例。但反过来如果把箭头函数当作对象方法使用可能会踩坑const user { name: 张三, greet: () { console.log(你好我是 ${this.name}); } }; user.greet(); // 输出你好我是 undefined原因就是箭头函数不会把this绑定到user对象而是向上层作用域查找this。这里如果改用普通函数就没有这个问题。3.3arguments的差异普通函数内部可以访问arguments对象拿到所有传入参数function sum() { let total 0; for (let i 0; i arguments.length; i) { total arguments[i]; } return total; } console.log(sum(1, 2, 3)); // 6箭头函数没有arguments如果确实需要类似功能可以用剩余参数const sum (...args) { let total 0; for (let i 0; i args.length; i) { total args[i]; } return total; }; console.log(sum(1, 2, 3)); // 6这两种写法在功能上等价但显式参数声明更利于阅读也避免了隐式依赖arguments的问题。3.4 使用建议综合来看我的个人建议是普通函数适合对象方法、构造函数、需要动态this的场景箭头函数适合回调、简单计算、不需要独立的this的场景在class方法中优先考虑箭头函数作为实例属性的写法但要评估兼容性和团队规范。下面给出一个综合示例// 文件路径src/write-style/function-arrow.js class EventHandler { constructor(name) { this.name name; // 箭头函数绑定实例适合做事件回调 this.handleClick () { console.log(${this.name} 被点击); }; } // 普通方法this 由调用者决定 describe() { console.log(当前处理器${this.name}); } } const handler new EventHandler(登录按钮); // 作为事件回调时箭头函数能保证 this 指向实例 document.getElementById(login-btn).addEventListener(click, handler.handleClick);如果这里你用了handler.handleClick的普通函数版本事件回调里的this会丢失原实例你可能还得手动bind一下document.getElementById(login-btn).addEventListener(click, handler.handleClick.bind(handler));所以你可以不用箭头函数但你必须知道“还有一种写法”能让你少写一行bind。4. MyBatis mapper.xml 中动态 SQL 的多种写法MyBatis 是 Java 后端开发中接触频率非常高的持久层框架它的 mapper.xml 里动态 SQL 是刚需。很多同学一写动态 SQL 就只会if但 MyBatis 其实提供了非常丰富的标签组合。4.1 最基础的if写法先看一条最基础的查询!-- 文件路径src/main/resources/mapper/UserMapper.xml -- select idselectByCondition resultTypecom.example.entity.User SELECT * FROM user WHERE 1 1 if testname ! null and name ! AND name #{name} /if if testage ! null AND age #{age} /if /select这是很多教程里最常见的写法。WHERE 1 1是为了避免后续动态条件拼接时出现“WHERE AND”的语法错误。它简单、直观适合条件数量少的场景。4.2 使用where标签优化但WHERE 1 1并不优雅在有些团队评审中会被批评。更好的写法是用where标签select idselectByCondition resultTypecom.example.entity.User SELECT * FROM user where if testname ! null and name ! AND name #{name} /if if testage ! null AND age #{age} /if /where /selectwhere标签会在子元素有内容时自动生成WHERE关键字并自动去掉开头的AND或OR。这样 SQL 更干净语义也更明确。4.3 多分支场景的choose写法假如业务规则是优先按用户名查询用户名为空时按邮箱查询邮箱也为空时查询所有禁用用户。这个需求用if写会非常别扭因为if是平铺条件不是互斥分支。此时应该使用choose、when、otherwiseselect idselectByPriority resultTypecom.example.entity.User SELECT * FROM user where choose when testname ! null and name ! AND name #{name} /when when testemail ! null and email ! AND email #{email} /when otherwise AND status 1 /otherwise /choose /where /selectchoose相当于 Java 中的switchwhen是caseotherwise是default。它能表达互斥逻辑而if是独立的多个条件判断可能同时生效。4.4 集合条件的foreach写法在批量查询中最常见的需求是select idselectByIds resultTypecom.example.entity.User SELECT * FROM user WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /selectforeach的几个属性含义collection传入的集合参数名称item循环变量名open、close拼接的前后缀separator元素之间的分隔符。这个写法在批量插入中也很常用insert idbatchInsert INSERT INTO user(name, age) VALUES foreach collectionlist itemuser separator, (#{user.name}, #{user.age}) /foreach /insert4.5 动态更新与set标签更新操作同样会遇到“只更新非空字段”的需求。错误的写法是update idupdateUser UPDATE user SET name #{name}, age #{age} WHERE id #{id} /update如果name为null这条 SQL 会把数据库里的name改成NULL这往往不是期望结果。更稳妥的是使用set标签update idupdateUser UPDATE user set if testname ! null name #{name}, /if if testage ! null age #{age}, /if /set WHERE id #{id} /updateset标签会自动去掉多余的末尾逗号同时配合if实现字段级动态更新。4.6 关于if test...的写法细节很多初学者写if时容易犯一个错误在 OGNL 表达式中混淆了字符串判断与数值判断。比如!-- 推荐写法先判空再判空字符串 -- if testname ! null and name ! AND name #{name} /if而对于数值类型不需要判断空字符串if testage ! null AND age #{age} /if常见错误是写成if testage ! null and age ! 这在实际运行时如果 age 是Integer类型OGNL 表达式可能不会报错但当你把参数从 JSON 反序列化时age可能是整数也可能是字符串判断逻辑就会变得混乱。因此建议根据参数类型来设计if条件不要无脑套同样的模板。4.7 多写法的选择建议场景推荐写法原因多条件可选查询whereifSQL 干净易维护多条件互斥查询choosewhenotherwise语义清晰接近 switch集合条件查询/插入foreach批量操作必须动态更新非空字段setif避免误更新为 NULL无任何条件时兜底where或显式 SQL防止全表更新/查询这里强调一点动态 SQL 的写法没有唯一标准但如果你只会if遇到互斥分支、批量操作、动态更新时代码会越来越难维护甚至可能出现 SQL 拼接错误。掌握更多标签就是掌握更多“武器”。5. Go 语言工程化写法从新手代码到工程代码Go 语言的语法非常简洁但正因为简洁写法的好坏在工程中长期维护时差异很大。下面从错误处理、配置管理、项目结构三个角度来对比不同写法。5.1 错误处理的多种写法初级写法直接忽略错误或层层嵌套// 文件路径internal/service/user_service.go func GetUserName(userID int64) string { user, err : userRepo.FindByID(userID) if err ! nil { return 未知用户 } return user.Name }这个写法的问题在于错误信息被吞掉了。如果FindByID失败是因为数据库超时这个函数返回的是“未知用户”上层无法判断到底发生了什么。工程化写法通常是包装错误func GetUserName(userID int64) (string, error) { user, err : userRepo.FindByID(userID) if err ! nil { return , fmt.Errorf(查询用户失败: %w, err) } return user.Name, nil }在更复杂的工程中还可以使用自定义错误类型或错误码var ErrUserNotFound errors.New(user not found) type AppError struct { Code int Message string Err error } func (e *AppError) Error() string { return e.Message } func (e *AppError) Unwrap() error { return e.Err }这里体现了错误处理的两种不同写法返回原始错误、包装错误、自定义错误类型。没有哪一种写法放之四海而皆准。简单的内部工具函数可以直接返回错误给外部使用的 API 接口就适合包装上下文。5.2 配置管理的多种写法Go 工程里常见的配置写法有几种。第一种是全局变量package config var ( AppName string Port int )这种方式读取方便但初始化顺序不明确测试时也不容易替换。第二种是结构体初始化type Config struct { AppName string Port int Debug bool } func NewDefaultConfig() *Config { return Config{ AppName: demo-app, Port: 8080, Debug: true, } }这种方式更符合工程化要求配置字段集中管理。第三种是函数式选项模式适合有很多可选配置的 SDKtype Option func(*Server) func WithPort(port int) Option { return func(s *Server) { s.port port } } func WithTimeout(timeout time.Duration) Option { return func(s *Server) { s.timeout timeout } } func NewServer(opts ...Option) *Server { s : Server{ port: 8080, timeout: 30 * time.Second, } for _, opt : range opts { opt(s) } return s }函数式选项模式的写法比“传一个巨大的配置结构体”更灵活调用方只需要传入关心的配置项也能在默认值基础上做增量修改。这种写法在开源项目中被大量使用比如 gRPC、Gin 等。它本质上是一种“把配置项变成函数参数”的工程化写法。5.3 项目结构的写法差异Go 项目的工程化写法还包括目录结构。常见结构有两种。一种是扁平结构小项目推荐demo/ ├── main.go ├── config.go └── service.go另一种是分层结构中大型项目推荐demo/ ├── cmd/ │ └── server/ │ └── main.go ├── internal/ │ ├── config/ │ ├── handler/ │ ├── service/ │ └── repository/ ├── pkg/ │ └── utils/ └── go.mod分层结构表达了清晰的依赖方向handler调用serviceservice调用repositorypkg存放可以被外部引用的工具包。这两种结构没有绝对的对错关键看团队规模和项目复杂度。如果你一上来就为一个小工具套用完整的分层结构反而可能增加维护成本但如果你在大型项目中仍然把所有代码写在一个main.go里后面会寸步难行。所以说掌握不同写法本质上是掌握不同规模的应对方案。6. C 交互题写法从输入输出到解题封装交互题是算法竞赛里比较有代表性的一类题型它对程序的输入输出时机、格式、效率都有要求。很多新人在交互题上栽跟头不是算法想不出来而是输入输出的写法不合适。6.1 输入输出效率对比先看最常用的写法#include iostream using namespace std; int main() { int n; cin n; int sum 0; for (int i 0; i n; i) { int x; cin x; sum x; } cout sum endl; return 0; }在数据量小的时候这套代码完全没问题。但在大数据量交互题中比如需要读取 10^6 个整数cin的默认同步模式会明显变慢。常见优化是取消同步#include iostream using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin n; long long sum 0; for (int i 0; i n; i) { int x; cin x; sum x; } cout sum \n; return 0; }ios::sync_with_stdio(false)可以关闭 C 标准流与 C 标准 I/O 的同步cin.tie(nullptr)可以取消cin与cout的绑定减少 flush 次数。这是竞赛中非常常见的写法。另一种写法是使用scanf和printf#include cstdio int main() { int n; scanf(%d, n); long long sum 0; for (int i 0; i n; i) { int x; scanf(%d, x); sum x; } printf(%lld\n, sum); return 0; }两种写法的效率在数据量大时会有差异但更重要的是如果你在交互题里需要逐行刷新输出比如每轮询问都要立即输出就要特别注意缓冲问题。6.2 交互题的输出刷新写法交互题要求你的程序与评测系统实时通信如果输出没有及时刷新评测程序可能会一直等待导致超时。常见写法是#include iostream #include vector using namespace std; int main() { int l 1, r 1000000; while (l r) { int mid (l r 1) / 2; cout ? mid endl; int ans; cin ans; if (ans 1) { l mid; } else { r mid - 1; } } cout ! l endl; return 0; }这里使用了endl它不仅输出换行还会刷新缓冲区。在某些交互题中这是必要的但如果你把endl换成\n会不会出错取决于评测环境是否需要强制刷新。有的代码为了保证可靠性会主动加上flushcout ? mid \n flush;这里的差异就体现了写法的意义在普通输出题中\n更高效在交互题中可能必须用endl或flush。你不知道这一点时一个看似无害的写法选择可能直接导致超时。6.3 解题逻辑的封装写法很多交互题的题解喜欢把逻辑写在main里。但对于较复杂的题更好的做法是把交互过程封装成函数。#include iostream using namespace std; // 交互询问函数 int ask(int x) { cout ? x endl; int res; cin res; return res; } int main() { int l 1, r 1000000; while (l r) { int mid (l r 1) / 2; if (ask(mid) 1) { l mid; } else { r mid - 1; } } cout ! l endl; return 0; }封装之后ask函数代表了“一次交互”逻辑更清晰也方便在多处询问时复用。这种思路本质上和工程代码中的“抽函数”是一致的。6.4 交互题写法的建议如果题目数据量大优先考虑scanf/printf或关闭同步的cin/cout如果是交互题输出后记得刷新缓冲区把“询问”和“回答”封装成独立函数便于理清交互流程不要在一个程序里混用cin和scanf在关闭同步后混用可能引发未定义行为。7. 如何训练自己的“多写法”能力到这里我们已经看了四种语言/框架下的不同写法。那么如何才能在实际工作中真正养成“不要局限于一种写法”的习惯呢我给出几条可行的方法。7.1 读源码时主动关注不同写法看开源项目时不要只关注功能逻辑也要关注代码的组织方式。比如同一个开源库里错误处理有哪几种风格配置初始化用了什么模式为什么这个函数用了普通函数而不是箭头函数动态 SQL 中为什么用where而不是WHERE 11带着这些问题去读源码比单纯复制代码更有价值。7.2 做“一题多解”练习这个方法对算法题尤其有效。一道题尝试用暴力法、二分法、双指针、动态规划分别实现你才能理解不同写法的适用边界。在日常开发中也可以这样先实现一个功能的“能跑版本”再尝试重构为更优雅的写法。比如一个嵌套很深的if能不能用switch或表驱动优化一个回调地狱能不能用Promise或async/await改写这都是在训练多写法能力。7.3 通过代码评审学习他人的写法代码评审是成本最低的学习场景。当别人在你的代码里提出“这里可以用choose代替多个if”“这里可以用函数式选项模式简化参数”时不要觉得被冒犯而要把建议记录下来研究一下为什么对方推荐这种写法。反过来你在评审别人的代码时也可以主动思考如果让我来写我会怎么写有没有另一种写法能减少分支或提升可读性7.4 保持“先列出候选写法再选型”的习惯在实际开发中我可以给你一个非常实用的建议动手写代码之前先在草稿或注释里列出两三种候选写法简单评估它们的优缺点然后再动手。比如你写一个动态查询接口脑子里先过一下条件分支是互斥的还是可叠加的是单条更新还是批量更新是否需要考虑性能这个页面最看重的是可读性还是执行效率这一系列思考不需要花很长时间但它能倒逼你从“唯一的写法”走向“综合选型”。8. 写在最后“不要局限于一种写法”并不是一句鼓励炫技的口号而是一种工程态度的提醒。在实际开发中代码不是写给自己看的还要给同事维护、给测试理解、给机器执行。每一种写法都有它的语义边界、性能特征和维护成本。你掌握的写法越多你在真实项目里的选择空间就越大越不容易写出“能跑但难维护”的代码。希望这篇文章能给你一些启发。下次再写 JavaScript 函数、MyBatis 动态 SQL、Go 配置管理或者 C 交互题时可以先停下来想一想除了我平时习惯的那种写法还有没有更好的选择如果你有其他“同场景不同写法”的经验或踩坑经历欢迎在评论区分享。本文中的示例代码都是实际可运行的也建议你本地打开 IDE 或编译器把每一种写法都敲一遍感受它们之间的差异。
返回列表