面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

计算机基础阅读 05月27日 10:50

XML 中的 CDATA 是什么?什么时候需要用 CDATA?

CDATA(Character Data)是 XML 里的一个特殊标记,告诉解析器"这段内容别解析,原样保留"。当你需要在 XML 中放代码、HTML 片段或包含大量 <、>、& 的文本时,CDATA 省去逐个转义的麻烦。基本语法<code> <![CDATA[ if (x < 10 && y > 5) { return "ok"; } ]]></code><![CDATA[ 和 ]]> 之间的内容,XML 解析器不会尝试解析标签或实体引用,全部当作原始文本处理。什么时候需要 CDATA嵌入代码:JavaScript、SQL、CSS 里大量使用 <、>、&&,不用 CDATA 就得写成 <、>、&&,可读性极差。<script> <![CDATA[ function check() { if (count < 10 && status === "active") { return true; } } ]]></script>嵌入 HTML 片段:RSS feed 里经常包含 HTML 内容,CDATA 是标准做法。<description> <![CDATA[ <p>这是一段<strong>HTML</strong>内容</p> ]]></description>嵌入 SQL 查询:MyBatis、Hibernate 的 XML 映射文件里写 SQL,比较运算符必须转义或用 CDATA。<select id="findActive"> <![CDATA[ SELECT * FROM users WHERE age > 18 AND score >= 60 ]]></select>CDATA 的限制不能嵌套:CDATA 内部不能出现 ]]>,因为解析器会把第一个 ]]> 当作 CDATA 结束标记。如果内容里确实需要 ]]>,得拆成两个 CDATA 节:]]]><![CDATA[>。大小写敏感:必须是 CDATA,写成 cdata 或 Cdata 都不对。空白保留:CDATA 里的换行和缩进会原样保留,包括你不想保留的。格式化 XML 时注意别误改 CDATA 内的空白。不能做部分转义:CDATA 是全有或全无的——整个内容都不解析。如果只需要转义个别字符,用实体引用 < > 更精确。CDATA vs 实体引用| 特性 | CDATA | 实体引用 ||------|-------|----------|| 语法 | <![CDATA[...]]> | < > & || 适用范围 | 大段文本 | 单个字符 || 可读性 | 高,原文可读 | 低,需要还原 || 灵活性 | 低,整个块不解析 | 高,精确控制 |经验法则:超过 3 个特殊字符就用 CDATA,少于 3 个用实体引用。常见误区CDATA 不是数据类型:CDATA 只是告诉解析器别解析,它不改变数据的含义。解析后 <![CDATA[hello]]> 和 hello 是等价的——应用程序拿到的是同样的字符串。CDATA 不影响验证:XSD 验证时,CDATA 内的内容同样会被检查是否符合类型约束。CDATA 只跳过解析,不跳过验证。浏览器中的 CDATA:XHTML 里曾经用 //<![CDATA[ 包裹 JavaScript,但 HTML5 不需要——<script> 标签的内容本身就不被当作 XML 解析。这个用法已经过时了。
计算机基础阅读 05月27日 10:49

XML 和 JSON 有什么区别?什么时候该用 XML?

XML 和 JSON 是两种最常用的数据交换格式,但它们的定位不同:XML 是标记语言,擅长表达文档结构;JSON 是数据格式,擅长表达结构化数据。现代 Web 开发 90% 的场景用 JSON,但 XML 在特定领域仍然不可替代。核心区别| 特性 | XML | JSON ||------|-----|------|| 定位 | 标记语言,面向文档 | 数据格式,面向数据 || 语法 | 标签闭合 <name>值</name> | 键值对 "name": "值" || 数据类型 | 无内置类型,都是字符串 | string、number、boolean、null、array、object || 注释 | 支持 <!-- --> | 不支持 || 命名空间 | 支持,避免标签冲突 | 不支持 || 验证 | DTD / XSD 成熟方案 | JSON Schema(较新,工具链不完善) || 冗余度 | 高(开闭标签重复) | 低 || 解析速度 | 慢(DOM/SAX) | 快(原生支持) |一句话概括:XML 能做文档,JSON 只能做数据;JSON 传输快,XML 验证强。什么时候必须用 XML包含文档内容:XML 的标签能表达语义和层级(标题、段落、列表),JSON 的键值对做不到。Office 文档(docx、xlsx)底层是 XML,RSS/Atom feed 也是 XML——这些场景需要混合内容和结构。需要严格验证:XSD 可以定义精确的类型约束(值范围、正则模式、枚举),JSON Schema 功能弱得多。金融、医疗、政府数据交换标准(如 HL7、FHIR 的 XML 格式)依赖 XSD 验证。命名空间:多个词汇表组合时,命名空间避免标签冲突。SOAP、XHTML、SVG 都用命名空间。JSON 没有这个能力。遗留系统集成:企业里大量旧系统只认 XML。SAP、Oracle、银行接口——你不想用也得用。JSON 的优势场景Web API:RESTful API 用 JSON 是事实标准。体积小、解析快、前端原生支持,没有理由用 XML。配置文件:package.json、tsconfig.json、.eslintrc——开发工具链已经全面倒向 JSON(以及 JSON 超集如 JSON5、YAML)。移动端和低带宽场景:JSON 比 XML 小 30-50%,解析快 2-3 倍。移动网络下这个差距很实际。NoSQL 数据库:MongoDB、CouchDB 存储 JSON 文档,查询天然适配。同一数据的格式对比<!-- XML --><book id="1" category="web"> <title>XML Guide</title> <price>39.95</price> <tags> <tag>XML</tag> <tag>Programming</tag> </tags></book>{ "id": 1, "category": "web", "title": "XML Guide", "price": 39.95, "tags": ["XML", "Programming"]}XML 版本 156 字节,JSON 版本 98 字节。数据量大的时候,这个差距更明显。格式互转XML 和 JSON 互转不是无损的——XML 的属性、命名空间、混合内容在 JSON 里没有对应概念:XML 属性(<book id="1">)转 JSON 时变成普通字段,丢失"这是属性"的语义XML 混合内容(<p>文字<b>加粗</b>继续</p>)转 JSON 需要特殊处理JSON 的数组类型转 XML 时只能用重复标签模拟所以别指望"先写 XML 再转 JSON"或反过来——两种格式的数据模型不同,互转会丢信息。选型决策简单判断:如果数据是给人读的文档,用 XML;如果是给程序消费的结构化数据,用 JSON。如果两者都涉及(比如带格式的富文本数据),考虑用 JSON 做传输、XML 做存储,或者直接用 Markdown + JSON 元数据的组合方案。
计算机基础阅读 05月27日 10:49

XML Schema 和 DTD 有什么区别?XSD 为什么取代了 DTD?

XML Schema(XSD)和 DTD 都用来定义 XML 文档的结构和约束,但能力差距很大。XSD 是 DTD 的现代替代方案——基于 XML 语法、支持数据类型、支持命名空间、可扩展可继承。DTD 语法简单但功能有限,新项目几乎不再使用。核心区别| 特性 | XML Schema (XSD) | DTD ||------|------------------|-----|| 语法 | XML 格式,可用 XML 解析器处理 | 自有语法,不是 XML || 数据类型 | 丰富内置类型(string、int、date、boolean 等) | 只有字符串,没有类型区分 || 命名空间 | 原生支持 | 不支持 || 类型继承 | 支持 extension 和 restriction | 不支持 || 可重用性 | 支持类型导入和引用 | 难以复用 || 约束精度 | 可定义值范围、正则模式、枚举 | 只能定义元素出现次数 |简单说:XSD 能做的 DTD 做不了(类型约束、命名空间),DTD 能做的 XSD 都能做且做得更好。XSD 基础结构XSD 本身是 XML 文档,根元素是 <xs:schema>:<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="book" type="BookType"/> <xs:complexType name="BookType"> <xs:sequence> <xs:element name="title" type="xs:string"/> <xs:element name="price" type="xs:decimal"/> <xs:element name="publishDate" type="xs:date"/> </xs:sequence> <xs:attribute name="id" type="xs:string" use="required"/> </xs:complexType></xs:schema>complexType 定义包含子元素或属性的复杂类型,simpleType 定义带约束的简单类型。XSD 的约束能力XSD 比 DTD 强的地方在于精确约束:<!-- 值范围约束 --><xs:simpleType name="AgeType"> <xs:restriction base="xs:integer"> <xs:minInclusive value="0"/> <xs:maxInclusive value="120"/> </xs:restriction></xs:simpleType><!-- 正则约束 --><xs:simpleType name="EmailType"> <xs:restriction base="xs:string"> <xs:pattern value="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"/> </xs:restriction></xs:simpleType><!-- 枚举约束 --><xs:simpleType name="StatusType"> <xs:restriction base="xs:string"> <xs:enumeration value="active"/> <xs:enumeration value="inactive"/> </xs:restriction></xs:simpleType>DTD 只能声明元素存在和出现次数,无法约束值的格式和范围。类型继承和扩展XSD 支持两种继承方式:extension:在基础类型上添加新元素或属性restriction:在基础类型上收紧约束<!-- 扩展:在 PersonType 上加 department --><xs:complexType name="EmployeeType"> <xs:complexContent> <xs:extension base="PersonType"> <xs:sequence> <xs:element name="department" type="xs:string"/> </xs:sequence> </xs:extension> </xs:complexContent></xs:complexType>这是 DTD 完全做不到的——DTD 没有类型体系,每个元素定义都是独立的。在 XML 中引用 XSD<book xmlns="http://www.example.com/books" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.com/books books.xsd"> <title>XML Guide</title> <price>49.99</price> <publishDate>2024-01-15</publishDate></book>schemaLocation 属性成对出现:命名空间 URI + XSD 文件路径。解析器会根据 XSD 验证文档内容。什么时候还在用 DTD?DTD 在 2025 年基本只剩这些场景:维护遗留系统(改 DTD 风险比替换小)HTML5 的 DOCTYPE 声明(严格说是简化版 DTD)简单的配置文件验证(不值得写 XSD 的场景)新项目用 XSD,没有理由选 DTD。如果嫌 XSD 太啰嗦,可以考虑 RelaxNG——更简洁的替代方案。
服务端阅读 05月27日 10:48

MariaDB 窗口函数怎么用?排名、累计和同比计算详解

窗口函数是 SQL 里做数据分析最好用的工具——不用窗口函数,计算排名、累计、同比这些需求得写各种子查询和自连接,代码又长又慢。MariaDB 从 10.2 开始支持窗口函数,基本覆盖了 SQL 标准的核心功能。语法结构每个窗口函数都遵循同一套语法:函数名(表达式) OVER ( PARTITION BY 分组字段 ORDER BY 排序字段 ROWS/RANGE 窗口范围)OVER 子句定义了"窗口"——函数在这个范围内计算。PARTITION BY 把数据分组,每组独立计算;ORDER BY 决定组内排序;ROWS/RANGE 进一步约束参与计算的行范围。排名函数:ROWNUMBER、RANK、DENSERANK三个排名函数的区别在处理并列值时的行为:ROW_NUMBER:严格递增,1-2-3-4,不管值是否相同RANK:并列同名,跳号,1-1-3-4DENSE_RANK:并列同名,不跳号,1-1-2-3典型场景——每个部门薪资前三名:SELECT * FROM ( SELECT name, department, salary, DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk FROM employees) t WHERE rnk <= 3;用 DENSE_RANK 而不是 RANK,因为如果前三名有并列,RANK 会跳号,导致实际返回的记录少于 3 条。聚合函数做累计和移动平均窗口函数让 SUM/AVG/COUNT 不再只是"一组一个数",而是逐行累计:-- 累计销售额SELECT order_date, amount, SUM(amount) OVER (ORDER BY order_date) AS running_totalFROM orders;-- 7 天移动平均SELECT order_date, amount, AVG(amount) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS avg_7dFROM orders;ROWS BETWEEN … AND … 定义了参与计算的行范围。6 PRECEDING AND CURRENT ROW 表示当前行和前 6 行,总共 7 行做平均。LAG 和 LEAD:访问前后行LAG 取前 N 行的值,LEAD 取后 N 行的值。算环比增长率靠它们:SELECT month, revenue, LAG(revenue, 1) OVER (ORDER BY month) AS prev_month, ROUND((revenue - LAG(revenue, 1) OVER (ORDER BY month)) / LAG(revenue, 1) OVER (ORDER BY month) * 100, 2) AS growth_pctFROM monthly_sales;LAG 的第二个参数是偏移量,第三个参数是默认值(缺省返回 NULL)。算同比就改成 LAG(revenue, 12),往前取 12 个月。FIRSTVALUE 和 LASTVALUE 的坑FIRSTVALUE 取分组内第一个值,LASTVALUE 取最后一个值。但 LAST_VALUE 有个常见陷阱——默认窗口范围是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,不是整个分区。所以如果你想取部门最低薪资,必须显式指定窗口范围:-- 正确写法:指定完整窗口范围SELECT name, department, salary, LAST_VALUE(salary) OVER ( PARTITION BY department ORDER BY salary DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS lowest_in_deptFROM employees;不加 ROWS BETWEEN … UNBOUNDED FOLLOWING,LAST_VALUE 每行返回的值都不一样——因为窗口只到当前行为止。ROWS 和 RANGE 的区别ROWS:按物理行号计算,窗口大小固定RANGE:按逻辑值范围计算,同一排序值的行作为一个整体-- ROWS:固定 3 行窗口SUM(amount) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)-- RANGE:同一天的行一起算SUM(amount) OVER (ORDER BY date RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW)RANGE 适合按时间窗口聚合,同一时间点的所有行会被包含在同一个窗口内。ROWS 更精确,适合固定行数的滑动窗口。
服务端阅读 05月27日 10:46

Serverless 监控和调试怎么做?实战方案详解

Serverless 的监控比传统应用难得多——函数生命周期短,日志转瞬即逝,一次请求可能跨越十几个函数,调用链路像黑盒。传统 APM 工具虽然支持 Serverless,但配置复杂,且按函数收费的定价模式在大量函数场景下很贵。先搞清楚要监控什么:冷启动频率和耗时、函数执行时间、错误率和错误类型、并发数和限流情况、调用链路和依赖关系。这五项覆盖了 Serverless 最核心的可观测性需求。日志:结构化是前提Serverless 函数的日志不是写在本地文件上的,而是打到 CloudWatch / 日志服务。如果还是 print("something happened") 这种格式,在几千条日志里找到你要的那条无异于大海捞针。必须做两件事:JSON 格式输出:每条日志包含 traceid、functionname、timestamp、level、message,方便用 CloudWatch Insights 或 Loki 过滤查询请求级 trace ID:在 API Gateway 层生成,一路透传到下游所有函数,这样可以用一条查询把整个请求链路的日志串起来分布式追踪:看清调用链路一个 API 请求从 API Gateway → Lambda A → SQS → Lambda B → DynamoDB,光看日志拼不出完整链路。分布式追踪就是解决这个问题的。AWS X-Ray 是最直接的选择——和 Lambda 原生集成,开启即可用,但功能有限。OpenTelemetry 更灵活,支持多后端(Jaeger、Zipkin、Datadog),适合多云或混合架构。关键配置点:确保所有函数都开启 tracing在函数间传递 trace context(W3C Trace Context 标准的 traceparent header)设置合理的采样率——全量追踪成本高,1% 采样又可能漏掉关键错误本地调试:模拟环境还是直接上云?本地跑 Serverless 函数有两种思路:模拟派:用 SAM CLI 的 sam local invoke 或 Serverless Framework 的 offline 插件,在本地模拟 API Gateway、DynamoDB 等服务。好处是快,坏处是模拟环境总跟线上有差异——IAM 策略、VPC 配置、环境变量都可能不一样。直接上云派:写完代码直接部署到 dev 环境,用真实云服务测试。好处是环境一致,坏处是部署慢、费钱。实际项目中两者结合:单元测试本地跑,集成测试部署到 dev 环境。别花太多时间折腾本地模拟——线上环境的差异问题,本地模拟永远解决不了。错误处理和告警Serverless 函数报错后实例就被回收了,现场信息不会保留。所以错误处理必须做到两点:捕获所有异常:在函数入口包一层 try-catch,把错误信息连同上下文(请求参数、环境变量、调用栈)写入日志和错误追踪服务(Sentry、Rollbar)。裸奔的 Lambda 一旦崩溃,你连它为什么崩溃都不知道。设置合理的告警:CloudWatch Alarm 按 Lambda 的 ErrorRate 和 Duration 设阈值,错误率超过 5% 或 P99 延迟超过 2 秒就触发告警。别等到用户投诉才发现问题。
服务端阅读 05月27日 10:45

Serverless 架构有什么缺点?冷启动、调试和成本问题怎么解?

用了两年 Serverless,踩了不少坑。冷启动延迟、调试困难、成本失控……这些问题在 PPT 里不会被提及,但上线后每一个都会咬你。下面按实际影响程度,逐一拆解 Serverless 架构的主要限制和应对策略。冷启动:最让人头疼的延迟函数一段时间没被调用后,运行环境会被回收。下次请求进来,平台得重新分配资源、加载运行时、初始化代码——这个过程就是冷启动。AWS Lambda 冷启动通常 200ms-5s,Java 等重运行时更慢,可能 10 秒以上。应对方法:预留实例:花钱保活,适合对延迟敏感的核心接口轻量运行时:Go、Rust 比 Java/Node 冷启动快一个数量级定时心跳:每 5 分钟调一次函数防止回收(治标不治本,还浪费钱)SnapStart:AWS 提供的快照恢复功能,Java 冷启动从 10 秒降到 200ms执行时间和资源的天花板AWS Lambda 最长执行 15 分钟,内存上限 10GB,/tmp 最多 10GB。视频转码、大数据批处理这类长任务直接超出限制。解法不是硬塞进 Lambda,而是换架构:长任务用 AWS Fargate / Azure Container Apps,本质是 Serverless 容器,没有执行时间限制批处理用 AWS Step Functions 编排多个 Lambda,每个处理一部分大内存任务用 AWS Lambda 的 10GB 配置,但成本很高状态管理的天然缺陷Serverless 函数是无状态的——每次调用可能落在不同的实例上,实例间不能共享内存。这意味着:不能用全局变量缓存数据(下次请求可能不是同一个实例)WebSocket 长连接需要借助 API Gateway + DynamoDB 维护用户会话必须存外部存储(Redis、DynamoDB)这不是"限制"而是"设计约束"——接受无状态,把状态外置到专用服务,反而让架构更清晰。调试和可观测性是硬伤本地跑得好好的,部署到云端就出问题——环境差异、IAM 权限、网络配置都可能不一样。传统应用的断点调试在 Serverless 里基本不可行。实际可用的调试手段:本地模拟:用 Serverless Framework 的 local invoke 或 AWS SAM 的 sam local,模拟云环境结构化日志:每个请求带 trace ID,用 CloudWatch/Loki 按请求链路追踪X-Ray/Jaeger:分布式追踪,看清函数间的调用链和耗时预发布环境:和线上配置一致,上线前跑一遍集成测试厂商锁定:被平台绑定的隐性成本AWS Lambda 用了 API Gateway + DynamoDB + Step Functions,整套架构深度绑定 AWS。要迁到阿里云函数计算,代码、配置、基础设施全得重写。降低锁定风险的策略:业务逻辑和基础设施解耦:核心代码不依赖云 SDK,通过适配层调用云服务用 Terraform/CDK 管理基础设施:换平台只改配置,不改业务代码优先选开放标准:容器镜像部署比原生函数更容易迁移现实一点:锁定是不可避免的,关键是评估迁移成本是否在可接受范围内成本:看起来便宜,算起来不一定Serverless 按调用量计费,低流量场景确实便宜。但高并发或被攻击时,费用可能远超预期——DDoS 攻击不只是安全风险,还是财务风险。成本控制手段:设置并发上限和账单告警:AWS Lambda 支持账户级并发限制,防止失控对比预留实例和按需付费:稳定流量下预留实例更划算关注冷启动的间接成本:预留实例花钱,冷启动浪费请求时间,找到平衡点监控每次调用的成本:CloudWatch 可以按函数统计费用,及时发现异常
服务端阅读 05月27日 10:43

Swift 类型转换怎么做?is、as、as?、as! 有什么区别?

Swift 用 is、as、as?、as! 四个操作符做类型检查和转换。is 检查"是不是这个类型",返回 Bool;as 向上转型(子类→父类),永远成功,编译器自动推断时甚至不用写;as? 向下转型(父类→子类),成功返回可选值,失败返回 nil,安全;as! 强制向下转型,失败直接 crash,危险。核心规则:向上转型用 as(安全,编译器保证),向下转型优先 as?(安全,失败返回 nil),只在逻辑上100%确定时才用 as!。实际开发中 as? 用得最多——处理 JSON 解析、UITableViewCell 复用、协议类型的向下转型,几乎都是 as?。Any 和 AnyObject 是 Swift 的两种"万能类型":Any 可以表示任何类型(包括值类型和引用类型),AnyObject 只能表示类类型。类型转换在处理 [Any] 数组时最常见——网络请求返回的 JSON 经常被解析为 [String: Any],需要逐个 as? 转换。追问as、as?、as! 分别在什么场景用?as:向上转型(子类→父类)和桥接 Objective-C 类型(String as NSString),编译器保证安全。as?:向下转型不确定是否成功时,比如 JSON 解析 value as? String、UITableViewCell 复用 cell as? CustomCell,日常开发最常用。as!:逻辑上100%确定类型时,比如 storyboard 里已知 cell 类型,或者单元测试中断言类型。和 try! 一样,as! 基本只在"失败就是 bug"的场景用。Any 和 AnyObject 有什么区别?Any 可以表示任何类型——class、struct、enum、闭包、元组都行。AnyObject 只能表示类类型,本质是 Objective-C 的 id 类型。从 Objective-C 桥接过来的 API 返回 AnyObject,纯 Swift 代码用 Any。实际项目中尽量少用这两个——频繁用 Any 说明设计有问题,应该用协议或泛型代替。protocol 类型的 as? 转型有什么坑?把协议类型向下转型为具体类型时,只有协议标记了 @objc 或者是 class-only 协议才能 as?。纯 Swift 协议(没有 class 约束)的 existential 不能直接 as? 转为具体类型——编译器会报错。解法:把协议改为 @objc protocol,或者用泛型/关联类型替代类型转换。为什么 Swift 不推荐频繁用类型转换?频繁用 is/as? 说明你在绕过类型系统,通常意味着设计有问题。正确做法是用多态——定义协议方法让子类各自实现,调用方不需要知道具体类型。如果代码里一堆 if let x = y as? A { ... } else if let x = y as? B { ... },应该重构为协议 + 多态调用。写段代码class MediaItem { var name: String; init(name: String) { self.name = name } }class Movie: MediaItem { var director: String; init(name: String, director: String) { self.director = director; super.init(name: name) } }class Song: MediaItem { var artist: String; init(name: String, artist: String) { self.artist = artist; super.init(name: name) } }let library: [MediaItem] = [ Movie(name: "Casablanca", director: "Curtiz"), Song(name: "Blue Suede Shoes", artist: "Elvis"), Movie(name: "Citizen Kane", director: "Welles")]// as? 安全转型for item in library { if let movie = item as? Movie { print("Movie: \(movie.name) by \(movie.director)") } else if let song = item as? Song { print("Song: \(song.name) by \(song.artist)") }}// switch + is/asfor item in library { switch item { case is Movie: print("\(item.name) is a movie") case let song as Song: print("\(song.name) by \(song.artist)") default: break }}
服务端阅读 05月27日 10:42

Swift Result 类型怎么用?和 throws 有什么区别?

Result 是 Swift 5 引入的枚举,用两个 case 表示操作结果:.success 携带成功值,.failure 携带错误。它把错误从控制流(do-catch)变成了值——你可以像处理普通枚举一样 switch 它、map 它、存起来以后再处理。Result 最大的价值在异步回调。completion handler 里没法用 throws 传播错误(因为回调函数本身不是 throws 的),而 Result 作为返回值天然适合这种场景:func fetch(id: String, completion: @escaping (Result<User, Error>) -> Void)。async/await 出现后,新代码直接 async throws 更清晰,Result 主要用于兼容旧接口。Result 的常用方法:map 转换成功值(失败原样传递),flatMap 链式转换(返回新的 Result),get 把 Result 变回 throws(try result.get() 可以在 do-catch 中使用)。Result { try someThrowingFunc() } 初始化器可以方便地把 throws 函数包装成 Result。追问Result 和 throws 怎么选?同步代码用 throws + do-catch 更直观,也是 Swift 惯用风格。异步回调用 Result,因为 completion handler 里的 throws 外层 catch 不到。async/await 之后,新代码统一用 async throws,Result 退居兼容层。简单说:能 throws 就 throws,必须回调就用 Result。Result 的 map 和 flatMap 有什么区别?map 接收 (Success) -> NewValue 闭包,只转换成功值,失败原样传递,返回 Result。flatMap 接收 (Success) -> Result,整个 Result 替换,适合链式调用另一个可能失败的操作。类比:map 是"成功的话转换一下",flatMap 是"成功的话再做一次可能失败的操作"。Result 的 Failure 类型有什么坑?泛型约束 Failure: Error,但不同 Result 的 Failure 类型不同时不能直接组合。比如 Result 和 Result 做 flatMap 链会报类型不匹配。解法一:统一用 Error 作为 Failure 类型(牺牲类型精确性);解法二:用 mapError 统一错误类型;解法三:自定义 AppError 枚举把所有错误 case 收拢。Result 怎么和 async/await 配合?用 try await someAsyncFunc() 直接替代 Result 回调。如果必须兼容旧的 Result 接口,可以用 let result = await Result { try await someFunc() } 包装。反过来,把 Result 回调转 async/await 可以用 withCheckedContinuation 桥接。Result 和 Optional 有什么区别?什么时候用哪个?Optional 表示"有值或没有",没有失败原因;Result 表示"成功或失败",失败时携带错误信息。读配置文件、查缓存——只关心有没有值,用 Optional;网络请求、文件读写——需要知道失败原因,用 Result。try? 可以把 throws 函数降级为 Optional,但丢失了错误详情。写段代码enum APIError: Error { case invalidURL case badStatus(Int) case decodingFailed}func fetchUser(id: String) -> Result<User, APIError> { guard let url = URL(string: "https://api.example.com/users/\(id)") else { return .failure(.invalidURL) } return .success(User(name: "Test"))}// 链式调用let result = fetchUser(id: "42") .map { $0.name } .flatMap { name in fetchUser(id: name) }// Result 转 throwsdo { let user = try fetchUser(id: "42").get()} catch { print(error)}// 包装 throws 为 Resultlet result2 = Result { try JSONDecoder().decode(User.self, from: data) }
服务端阅读 05月27日 10:40

Swift 内存管理怎么做?ARC 和循环引用详解

Swift 用 ARC(自动引用计数)管理内存。每次创建类实例,ARC 分配内存并将引用计数置为 1;每多一个强引用指向它,计数 +1;引用离开作用域或被赋新值,计数 -1;计数归零,ARC 立刻释放内存。ARC 只管引用类型(class),值类型(struct/enum)不存在引用计数。循环引用是 ARC 最大的坑:两个实例互相强引用,计数永远不归零,内存泄漏。两种经典场景——类属性互相引用,和闭包捕获 self。类属性互引用的解法:把一边改成 weak 或 unowned。weak 必须是可选型 var,引用的对象释放后自动变 nil,安全;unowned 不是可选型,对象释放后访问会 crash,但性能更好。选哪个看生命周期——如果被引用对象可能先死,用 weak;如果确定被引用对象活得比自己久,用 unowned。闭包捕获 self 导致循环引用更隐蔽:闭包强引用了 self,self 又持有闭包。解法是捕获列表 [weak self] 或 [unowned self],在闭包内用 guard let self = self 解包。实际项目中 90% 用 weak self,因为闭包执行时 self 可能已释放。追问weak 和 unowned 有什么区别?怎么选?weak 修饰的属性在对象释放后自动置 nil,必须是可选型 var,访问安全;unowned 不会自动置 nil,对象释放后访问触发野指针 crash。选择标准:被引用对象可能先于自己释放用 weak(比如 delegate 模式),确定对方活得比自己久用 unowned(比如 Customer 持有 CreditCard,CreditCard 无主引用 Customer)。拿不准就用 weak,安全第一。weak 引用的底层实现是什么?Runtime 维护一张 weak 表(哈希表),key 是对象地址,value 是指向该对象的所有 weak 指针数组。对象 dealloc 时,Runtime 遍历 weak 表找到对应的指针数组,逐个置 nil,然后从表中删除。这也是为什么 weak 访问需要加锁——多线程可能同时在读 weak 指针和修改 weak 表。weak-strong dance 是什么?为什么需要?闭包中 [weak self] 后,self 变成可选型,每次用都要解包。如果闭包执行期间 self 被释放,中途解包失败会导致逻辑断裂。weak-strong dance 的做法:在闭包开头 guard let self = self else { return },把 weak 引用提升为局部强引用。这样闭包执行期间 self 不会被释放,逻辑连贯,执行结束后局部强引用消失,不影响释放。怎么检测循环引用?Xcode 的 Memory Graph(Debug Memory Graph 按钮)可以直接看对象引用关系,找到循环引用链。Instruments 的 Leaks 工具可以自动检测泄漏,切换到 Cycles & Roots 视图能看到循环引用的图形化展示。日常开发中,在 deinit 里打个 log,如果控制器退出后没触发,大概率有循环引用。ARC 和 MRC 有什么区别?ARC 做了什么优化?MRC 手动写 retain/release/autorelease,ARC 由编译器自动插入这些调用。ARC 的优化:编译器会省略不必要的 retain/release——比如函数返回值直接传递给调用方,中间不需要 retain 再 release(快速路径优化)。ARC 不等于垃圾回收,它在编译期确定,没有运行时停顿。写段代码class ViewController: UIViewController { var onComplete: (() -> Void)? func setup() { // 循环引用:闭包捕获 self,self 持有闭包 onComplete = { self.dismiss(animated: true) // ⚠️ 循环引用 } // 解法:weak self + guard onComplete = { [weak self] in guard let self = self else { return } self.dismiss(animated: true) // ✅ 安全 } }}// delegate 用 weak 防止循环引用class ListView: UIView { weak var delegate: ListViewDelegate?}
服务端阅读 05月27日 10:38

Swift 错误处理怎么做?throws、try、catch 详解

Swift 的错误处理核心就四个关键字:Error 定义错误、throws 声明可能失败、throw 抛出错误、try+do-catch 捕获处理。整个流程编译器强制检查——throws 函数必须被 try 调用,try 必须在 do-catch 或另一个 throws 函数中,不可能"忘了处理错误"。这点和 Java 的 checked exception 思路类似,但 Swift 更轻量。try 三种形式各有用途:try(标准,需 do-catch)、try?(失败返回 nil,丢弃错误详情)、try!(断言不会失败,否则 crash)。日常 80% 场景用 try? 就够了——只关心成功还是失败,不需要区分原因。需要根据错误类型走不同分支时才用完整 do-catch。try! 只在逻辑上不可能失败的路径或单元测试中用。错误沿调用链自动向上传播:A throws → B 调用 A 也必须 throws → C 调用 B 也必须处理,直到某个调用方用 do-catch 消化。传播链编译器强制检查,不存在运行时才发现错误没处理的情况。defer 无论是否抛错都执行,用于资源清理,多个 defer 按栈序(后进先出)执行。rethrows 专用于高阶函数——函数本身不抛错,但传入闭包如果是 throws 的,调用方也必须 try。标准库 map、filter、forEach 都用了 rethrows:传普通闭包不强制 try,传 throws 闭包才需要。Swift 5 的 Result 把错误从控制流变成值,可以像处理枚举一样处理成功和失败,特别适合异步回调。async/await 普及后新代码用 async throws 更直观,Result 主要兼容旧接口。Swift 错误处理不是传统"异常"——不会 unwinding 调用栈,happy path 几乎零开销。追问try? 和 try! 有什么区别?什么场景用哪个?try? 失败返回 nil;try! 失败直接 crash。绝大多数场景用 try?,比如读配置文件读不到就用默认值。try! 只在逻辑上不可能失败时用,比如解析 Bundle 内的 JSON——这都能失败说明资源缺失,是开发阶段就该暴露的 bug,不应静默处理。rethrows 和 throws 有什么区别?throws 表示函数自身会抛错;rethrows 表示函数本身不抛错,但透传闭包参数的错误。关键区别:rethrows 函数传入不抛错的闭包时,调用方不需要 try。Array.map 用 rethrows,所以 arr.map { $0 + 1 } 不需要 try,arr.map { try throwingFunc($0) } 才需要。Result 和 do-catch 怎么选?同步用 do-catch;异步回调用 Result,因为 completion handler 里抛出的错误外层 catch 不到。async/await 之后新代码直接 async throws 更清晰。Result(catching: { try func() }) 可以把 throws 函数包装成 Result,方便在回调中传递。defer 有什么常见坑?多个 defer 后进先出。坑一:在循环里写 defer 以为每次迭代都执行,实际等函数作用域结束。坑二:defer 不等异步操作完成,需要异步清理得用其他机制。正确做法:打开资源后立刻写 defer 关闭。Swift 错误处理和 Java 异常有什么本质区别?Swift 的错误不会 unwinding 调用栈,happy path 零开销。Java checked 异常强制 catch 或声明,代码侵入性强;Swift 更轻量,try? 提供了快速降级路径。Swift 没有 finally,用 defer 替代,且 defer 不限于 do-catch 块,任何作用域都能用。写段代码enum APIError: Error { case invalidURL case badStatus(Int)}func fetchUser(id: String) throws -> User { guard let url = URL(string: "https://api.example.com/users/\(id)") else { throw APIError.invalidURL } let (data, resp) = try URLSession.shared.data(from: url) guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else { throw APIError.badStatus((resp as? HTTPURLResponse)?.statusCode ?? -1) } return try JSONDecoder().decode(User.self, from: data)}do { let user = try fetchUser(id: "42")} catch APIError.badStatus(let code) { print("HTTP \(code)")} catch { print(error)}let maybe = try? fetchUser(id: "42") // User?