【IT行业杂学】“两种正义”交汇之时:潜藏在软件中的标准分裂真相
Back to Top
为了覆盖更广泛的受众,这篇文章已从日语翻译而来。
您可以在这里找到原始版本。
引言:行业潜藏的“两种标准”问题
#在软件开发的世界中,经常存在规格或行为分为两派的“标准分裂”现象。
人们常会问“哪一种是正确的?”,但实际上这只是因为各自有其历史或技术背景罢了。
此次将介绍一些与这种“两种标准”相关的例子。
数组从“0”还是从“1”开始?~使用的语言改变的“常识”~
#根据编程语言的不同,数组的索引起点主要分为三种模式。
● 以“0”开始的语言
#当今主流语言多数将数组索引从“0”开始,这是因为与硬件的亲和性更高。
代表性的语言示例:
- C / C++
- Java / Kotlin
- Python / JavaScript
- C# / Go / Rust
- Swift / Ruby / Perl
在这些语言中,数组 a
的第一个元素通过 a[0]
访问。
这是基于向数组的基址加上偏移量(0、1、2……)来引用的低级设计思想。
例如,假设数组 a
如下连续地存储在内存中:
索引 | 内存地址(示例) |
---|---|
a[0] |
0x1000 |
a[1] |
0x1004 |
a[2] |
0x1008 |
在此处重要的是:
a[n]
的本质是通过“基址 + 第 n 个偏移”来访问的这一思想。
也就是说,a[0]
意味着“从首部的第 0 个元素(= 就是首部本身)”,
对于以指针运算为基础的 C 语言等来说,这是极其自然的设计。
另外,在机器码层面上,由于把 0 用作“基准”更容易处理,因此从 0 开始在效率上也更有优势。
● 以“1”开始的语言
#在强调数学一致性的语言中,为了与自然数对应,采用从“1”开始。
代表性的语言示例:
- FORTRAN / R / COBOL
- Lua / MATLAB / Julia
- Smalltalk
在数学领域,数组(或数列)的下标通常从“1”开始:
- 矩阵:
- 求和符号:
- 斐波那契数列:
这是基于“在‘数数’的概念中,第一个被计为第 1 个”的直观表达。
对于 FORTRAN、R 等以科学技术计算、公式处理为主的语言来说,从 1 开始更为自然。
例如,在 R 中 x[1]
表示第一个元素。
这与数学直觉(第一个是第 1 个)一致,在矩阵或数列的表示上很自然。
● 可以自由指定起始下标的语言
#也存在可以灵活指定下标范围的语言。
这种设计既能支持从 0 开始,也能支持从 1 开始。
典型语言示例:
- Pascal(可以指定范围,如
array[1..10]
) - Ada(可以显式定义
array(0..9)
或array(1..10)
) - Fortran(可以指定起始,如
dimension(0:9)
) - VB.NET(通过
Option Base
来切换是否从 0 开始)
这种灵活性有助于实现针对不同用途的下标设计。
● 起始下标的选择对设计的影响
#- 从“0”开始:重视内存效率和低级运算优化
- 从“1”开始:重视数学自然性和公式可读性
- 自由设置型:可实现高灵活性和高抽象度的设计
并非哪一种“正确”,而是根据用途和上下文做出合适的选择这一点才重要。
在 Visual Basic (VB) 中,数组下标的规范因版本而异。尤其是像 Dim a(10)
这样的声明,根据语言或设置的不同,解释也不同,需要注意。
- VB6 及以前中,
Dim a(10)
会生成一个索引 0~10、共 11 个元素的数组。此外,指定Option Base 1
后,下标起始位置会改为1
。 - VB.NET中,
Option Base
会被忽略,并始终从 0 开始。也就是说,Dim a(10)
表示索引0~10
的 11 个元素的数组。
由此可见,C 或 Python 中指定的是“元素个数”,而 VB 中指定的是“最大索引”,两者根本不同。
示例:Dim a(10)
- VB.NET →
a(0)
~a(10)
共 11 个元素 - C / Python → 下标 0~9,共“元素个数为 10”
即使看似相同的语法,也有可能因为语言不同而意义相反,因此在处理数组大小时尤其要注意。
● 数组下标差异:总结
#- 从“0”开始是基于硬件结构和指针偏移的高效实现思想
- 从“1”开始则保留了强调数学自然性和可读性的公式文化遗产
并非哪一种“正确”,而是根据目的和背景做出合适的选择。
0 开始 vs 1 开始映照出“我们所观察世界的坐标轴位于何处”的差异。
字节序:大端(Big Endian) vs 小端(Little Endian)
#字节序(Endianess) 指的是在将多字节数据(例如:16 位、32 位、64 位整数等)存储到内存时,哪个字节先放置的顺序差异。
● 大端(Big Endian)
#- 定义:将最高有效字节(MSB:Most Significant Byte)优先存储(放在较低的内存地址)
- 采用示例:网络通信(TCP/IP 标准)、部分 RISC 架构(SPARC、早期 PowerPC 等)
示例:存储 0x12345678 时(按地址从小到大排列)
Big Endian: [0x12][0x34][0x56][0x78]
- 理念:类似人类十进制的书写方式,先读高位,注重直观的可读性
- 背景:也有一些指令集架构设计为“操作码 → 操作数”的顺序
作为采用大端的 CPU,Motorola 6809(1978 年)是较早的例子。之后更为广泛使用的 68000 系(1979 年)也沿用了同样的设计,并搭载于 Apple Macintosh 等众多商用机上。
● 小端(Little Endian)
#- 定义:将最低有效字节(LSB:Least Significant Byte)优先存储(放在较低的内存地址)
- 采用示例:Intel 系 CPU(x86、x86_64)、ARM(默认小端)
示例:存储 0x12345678 时
Little Endian: [0x78][0x56][0x34][0x12]
- 理念:许多数值运算先处理低位字节,具有较高效率
- 背景:还有一些如跳转指令等先读取地址低位字节的设计思想
● 为什么会分歧?理念的不同
#这种差异并非单纯喜好问题,而是初期硬件架构设计中优先考虑事项不同所致。
- 大端:注重对人类更易阅读和指令可视性(例如:先操作码,再高位操作数)
- 小端:针对数值的加法、比较等,从低位开始处理对处理器内部更为便利进行优化
● 混合环境的挑战
#在跨网络通信或二进制文件互操作时,字节序不一致会导致 Bug 或数据损坏。
例如:
- TCP/IP 中标准使用 Big Endian(网络字节序)
- Windows 二进制文件使用 Little Endian(Intel)
- 在不同字节序间传送结构体时,字段的解释会出现错乱
● 应对不同字节序的方法
#在大端和小端混合的环境中,需要明确的转换与调整手段。以下是常见的应对策略:
- 使用
htonl()
/ntohl()
等 API 进行主机⇔网络字节序转换(常见于 C 语言与系统编程) - 在文件格式中明确标注字节序
示例:WAV、PNG、TIFF 等在规范中包含字节序的指定 - 在通信协议层面事先约定
示例:Protocol Buffers 或 MessagePack 等设计为无需关心字节序即可使用
由此可见,事先认识字节序差异并进行明确处理对于构建可靠的软件至关重要。
● 名称由来
“大端(Big)”“小端(Little)”一名源于乔纳森·斯威夫特《格列佛游记》中“从大端还是从小端敲开煮熟的蛋”之争(Big-Endian vs Little-Endian)。
正是象征着“由微小差异引发的严重对立”。
栈上参数的压入方式:从前往后?还是从后往前?
#调用函数时,参数通常会被压入栈(push)后传递。
● 关于栈:
栈是在内存中以“后进先出(LIFO)”方式管理数据的区域,用于存放函数的参数、返回地址、本地变量等。
每次调用函数时都会压入新的栈帧,函数结束时则统一弹出(pop),这是基本的运行机制。
在此过程中需注意两点:
- 将参数压入栈的顺序是“从前(左边)还是从后(右边)?”
- 调用后,谁负责释放栈上参数区域(收尾)?
- 由调用方(caller)清理?
- 由被调用方(callee)清理?
这些差异称为“调用约定(calling convention)”,会因使用的架构和平台而异。
若不了解其规则,函数调用后可能导致栈状态混乱,进而出现意外行为或崩溃。
● 参数的压入顺序:从前还是从后?
#-
右到左(从后往前压):
最常见的方式(如:cdecl
)- 与可变参数(如
printf()
)兼容性好 - 最后一个参数最先被压入栈
- 与可变参数(如
-
左到右(从前往后压):Pascal 系调用约定(如:
pascal
、fastcall
)- 与可变参数(如
printf()
)兼容性差 - 第一个参数最先被压入栈
- 与可变参数(如
● 栈清理:由 caller 还是 callee?
#-
caller clean-up(由调用方清理栈):
典型例:cdecl
- 易于支持可变参数
- 调用方需知道参数个数
-
callee clean-up(由被调用方清理栈):
典型例:stdcall
- 调用方更为简洁
- 参数个数需固定
● 参数压入方式示例(右到左 / 左到右)
#考虑调用函数 sum(a, b, c)
的场景。
■ 右到左(从后往前压)方式:以 cdecl
等为例
int result = sum(1, 2, 3); // 调用方
此时,栈中按以下顺序压入:
push 3 ← 最后一个参数
push 2
push 1 ← 第一个参数
call sum
→ 在栈上,“最后一个参数位于最顶端”。
→ 与可变参数(如 printf("%d", x)
)兼容性好。
→ 栈清理由“调用方(caller)”进行。
■ 左到右(从前往后压)方式:以 pascal
、fastcall
等为例
result := sum(1, 2, 3); // 类 Pascal 调用
此时,栈中按以下顺序压入:
push 1 ← 第一个参数
push 2
push 3 ← 最后一个参数
call sum
→ 在栈上,“参数顺序与调用顺序一致”。
→ 可读性高,易于观察,但不适合可变参数。
→ 栈清理多由“被调用方(callee)”进行。
● 补充
实际上也存在寄存器传递(如 fastcall
)等方式,将最初的若干参数放入寄存器,剩余参数压入栈。
不同的调用约定会导致栈整理方式不同,因此为确保函数调用兼容性,统一约定至关重要。
● 执行效率与兼容性
#调用约定 | 参数顺序 | 可变参数支持 | 备注 |
---|---|---|---|
cdecl |
右→左 | 支持 | C 语言标准。栈清理解由调用方(caller) |
stdcall |
右→左 | 不支持 | Windows API 使用。清理解由被调用方(callee) |
pascal |
左→右 | 不支持 | 旧 Pascal 系。注重可读性 |
fastcall |
优先寄存器 + 右→左 | 有限 | 前两个参数寄存器传递,剩余按右→左压栈 |
vectorcall |
优先寄存器 + 右→左 | 有限 | 积极利用浮点 / SIMD 寄存器。Windows x64 引入 |
- 寄存器传递(如
fastcall
,vectorcall
等) 是为加快函数调用而引入的。 - 若 ABI 不同,可能导致库或二进制间不兼容,需特别注意。
- 特别是在 Windows 与 Linux 间,默认调用约定不同。(如:Windows 采用
stdcall
系,Linux 采用cdecl
系)
● 栈上参数压入方式:总结
#- 参数压入顺序与栈清理责任由调用约定(calling convention)定义
- 是否处理可变参数、注重性能还是追求二进制兼容性,都会影响合适选择
- 在跨平台开发或接口设计时,需要明确统一调用约定
字符编码:UTF-8 vs UTF-16
#字符编码的差异会影响文本处理、国际化、文件保存、通信协议等多方面。尤其是 UTF-8 和 UTF-16 作为典型的 Unicode 编码方式,在用途和特性上存在明显区别。
● UTF-8
#- 特点:可变长(1~4 字节)Unicode 编码
- 兼容性:与 ASCII(0x00~0x7F)二进制兼容。与现有 C 字符串兼容性高
- 采用示例:
- Web(HTML、HTTP、JSON 等标准编码)
- Linux / macOS 等文件系统
- Go、Rust、Python 等语言标准
- 优点:
- 以英语为主的内容紧凑
- 适合通信和存储
- 缺点:
- 随机访问不便(1 字符不等于 1 字节)
● UTF-16
#- 特点:主要以 2 字节(或 4 字节)编码的 Unicode 形式
- 兼容性:不兼容 ASCII(英数字也为 2 字节)
- 采用示例:
- Windows 内部 API(Windows NT 及以后)
- Java 的
char
类型,.NET 的System.String
- 优点:
- 对东亚文字丰富的内容更友好(多为 2 字节)
- 易于随机访问(以 2 字节为基本单位)
- 缺点:
- 对某些字符需要使用代理对(4 字节表示)(如表情符号或增补汉字)
- 二进制可移植性和兼容性问题较多
● 代理对问题
#在 UTF-16 中,U+10000 以上的字符(如部分表情符号或历史文字)由**两个 16 位值(代理对)**表示。
无法正确处理该情况的程序会导致乱码、崩溃或安全漏洞。
● 日本的混乱因素:Shift_JIS 的存在
#作为日本特有的字符编码 Shift_JIS 仍在某些场景中使用(如 Windows 旧软件、邮件、CSV 等)。
- 按字节存在歧义(如 0x5C 是 “¥” 还是 “\”)
- 与 Unicode 相互转换时容易出现问题
- 在 UTF-8 / UTF-16 / Shift_JIS 混用的环境中,乱码、错误处理、解析困难 等问题频发
● 在 Excel 中保存 CSV 时:默认不是 UTF-8
在 Excel 中以 “CSV(逗号分隔)” 保存时,当前日版 Windows 仍以 Shift_JIS(CP932)保存。
若想保存为 UTF-8,需要明确选择 “CSV UTF-8(逗号分隔)” 格式。(该选项在 Excel 2016 及以后版本首次可用)
很多用户仍直接保存为 .csv,却误以为是 UTF-8 导致乱码的情况非常多。
个人认为这是极其令人困扰的设计。
● 字符编码:总结
#特性 | UTF-8 | UTF-16 |
---|---|---|
字节长度 | 可变长(1 ~ 4 字节) | 主要 2 字节(+ 代理对时 4 字节) |
ASCII 兼容 | 有 | 无 |
优点 | 轻量、适合通信和存储 | 方便随机访问、高速的内部处理 |
主要用途 | Web、Linux、文件系统 | Windows、Java/.NET |
选择字符编码时,需要充分理解目标系统、数据与兼容性要求。尤其在跨语言、跨环境开发中,明确的转换和校验非常关键。
换行编码:LF vs CRLF
#文本文件的**换行编码(换行字符)**因操作系统和工具的历史沿革不同而有所差异,常在兼容性和版本管理方面引发问题。
● 各种换行编码的含义
#换行编码 | 符号 | ASCII 码 | 含义 |
---|---|---|---|
LF | \n |
0x0A | Line Feed(换行) |
CR | \r |
0x0D | Carriage Return(回车) |
CRLF | \r\n |
0x0D 0x0A | Carriage Return + Line Feed(回车+换行) |
● 各 OS 的标准
#OS/环境 | 换行编码 | 备注 |
---|---|---|
Linux | LF | Unix 传统。1 字节,简洁 |
macOS (现行) | LF | macOS X 及以后使用 LF(旧 Mac OS 为 CR) |
Windows | CRLF | 作为文本文件标准沿用至今 |
GitHub | 建议 LF | 为了保证仓库一致性 |
● 问题示例
#- diff 显示全部行都有差异
仅因换行编码不同,看似文件整体被修改 - 乱码或构建失败
在 Linux 上执行 Windows 创建的脚本时,会出现^M
导致失败 - Git 管理混乱
提交时被误判为每次都修改了换行
● 应对策略
#- 使用 Git 时,可在
.gitattributes
明确控制换行编码:* text=auto *.sh text eol=lf *.bat text eol=crlf
- 在编辑器设置中统一换行(如:VSCode、IntelliJ 等)
- 在 CI(持续集成)中加入换行检查
● 补充:macOS 的历史演变
- 旧 Mac OS(至 Mac OS 9):使用 CR(0x0D)作为换行
- macOS X 及以后(基于 Unix):切换为 LF,与 Linux 兼容
● 换行编码:总结
#- 换行编码不一致可能成为团队开发和跨平台环境中的重大障碍。
- 在早期阶段制定规则并自动化、强制执行,是避免踩雷的窍门。
浮点数舍入:IEEE 754 vs 商用四舍五入
#浮点数的**舍入(Rounding)**方式是影响计算精度与业务准确度的重要概念。
尤其在将“恰好处于中间值(如 2.5)”舍入到哪一侧时,科学技术领域与商业领域存在不同惯例。
● IEEE 754:偶数舍入(round to nearest even)
#- 定义:对于中间值(如 2.5、3.5 等),舍入到最接近的偶数
- 别名:“Bankers' rounding(银行家舍入)”
- 特点:
- 用于消除舍入方向的偏差
- 在统计上可相互抵消累积误差
- 示例:
- 2.5 → 2(偶数)
- 3.5 → 4(偶数)
- 1.25 → 1.2(舍入到小数第一位时)
● 商用四舍五入:round away from zero
#- 定义:对“.5”恰好值总是向远离零的方向舍入
- 特点:
- 接近用户直觉
- 广泛用于会计与金额处理
- 示例:
- 2.5 → 3
- -2.5 → -3
- 1.25 → 1.3(舍入到小数第一位时)
● 比较与使用场景
#角度 | IEEE 754(偶数舍入) | 商用四舍五入 |
---|---|---|
使用领域 | 科学技术、计量、标准计算 | 会计、销售、UI 显示 |
对中间值的处理 | 舍入到偶数 | 向远离零的方向舍入 |
舍入误差 | 平均而言中立 | 累积后可能产生偏差 |
● 不同实现语言与函数的差异
#语言 / 库 | 默认舍入方式 | 备注 |
---|---|---|
C / C++ (roundf ) |
IEEE 754(偶数舍入) | 依赖库实现 |
Python (round() ) |
IEEE 754(偶数舍入) | Python 3 系采用偶数舍入,round(2.5) 返回 2 |
Excel | 商用四舍五入 | ROUND 函数总是向远离零的方向舍入 |
Java (BigDecimal ) |
可选 | 可显式指定,如 RoundingMode.HALF_EVEN 等 |
Python 2 与 Python 3 中的 round()
函数在舍入处理上不同。
Python 2 在四舍五入时使用“向远离零方向舍入”(round-away-from-zero),而 Python 3 默认使用“偶数舍入”。
● 注意事项
#- 在税费计算、利息计算等场景中,舍入规则可能由法规制定。
- 在微秒级的时序处理或仿真中,舍入误差的累积可能产生重大后果。
- 若在规范中未明确写入舍入方法,则不同实现者可能产生不一致行为。
● 浮点数舍入:总结
#- IEEE 754:追求统计上的中立性
- 商用四舍五入:更贴近用户直觉,适用于金额计算
根据用途,养成显式指定舍入规则的习惯非常重要。
小数点表示:点号 vs 逗号
#在数字表示中,用作小数点的符号因国家和文化圈不同而异。
该差异在CSV 读取、Excel 数值解析、软件国际化等场景中可能引发严重混乱。
● 主要表示差异
#示例 | 地区/国家 | 说明 |
---|---|---|
3.14 |
日本、美国、英国等 | 使用句点 . 作为小数点 |
3,14 |
德国、法国、意大利、俄罗斯等 | 使用逗号 , 作为小数点 |
● 千分位分隔符也相反
#数值 | 使用句点小数点区域的表示 | 使用逗号小数点区域的表示 |
---|---|---|
1,234.56 | 1,234.56 |
1.234,56 |
→ 句点和逗号的用法完全相反!
● 典型问题示例
#- 读取 CSV 文件时,“3,14” 被当作字符串处理
- 在英语设置的 Excel 或 Python 中,逗号被解释为分隔符,导致错误
- 数值无法正确计算
- 自动解析失败,导致加总与汇总处理出错
- Excel 中因区域设置导致的行为差异
- 在日文/英文环境下,句点是小数点,千分位分隔符是逗号
- 在德语环境下,逗号是小数点,千分位分隔符是句点
● 应对策略
#- 读取 CSV 时指定区域设置(如 Excel、pandas 等)
- 示例:
pd.read_csv("file.csv", sep=";", decimal=",")
- 示例:
- 在 UI 设计中考虑用户的区域设置
- 内部处理始终使用统一表示(如句点),在 I/O 时进行转换
● 示例:在 Python 中读取
#import pandas as pd
# 针对德语区域的 CSV 处理
df = pd.read_csv("data.csv", sep=';', decimal=',')
● 小数点表示:总结
#- 小数点表示因国家而完全相反。
- 尤其在 CSV 与 Excel 中要特别注意,区域设置易引发误解。
- 内部处理使用统一表示 + 在 I/O 时支持区域设置 是可行的对策。
文件路径分隔符:斜杠(Unix) vs 反斜杠(Windows)
#在表示文件路径(目录结构)时,不同操作系统使用不同的分隔符。
这一差异会影响跨平台开发、Shell 脚本、库间兼容性等。
● 各操作系统的路径分隔符
#操作系统 / 环境 | 分隔符 | 示例 | 备注 |
---|---|---|---|
Unix/Linux | / |
/usr/local/bin |
标准斜杠表示 |
macOS | / |
/Applications/App |
基于 Unix,因此相同 |
Windows | \ |
C:\Program Files\App |
使用反斜杠(\ )作为标准 |
Web / URL | / |
https://example.com/path/to/resource |
URL 始终使用斜杠 |
● 注意事项
#-
在 Windows 中,反斜杠也用作转义字符,需注意
- 例如:
\n
表示换行,\t
表示制表符 - 表示路径时,可能需要写成
"C:\\path\\to\\file"
以进行转义
- 例如:
-
在 Python 等部分语言中,斜杠也被视为 Windows 路径分隔符
# 在 Windows 上也可用 path = "C:/Users/YourName/Documents"
● 对策与最佳实践
#-
使用与语言或环境无关的方法:
- Python:
os.path.join()
或pathlib.Path
- Java:
Paths.get()
或File.separator
- .NET:
Path.Combine()
- Python:
-
在脚本或配置文件中始终使用斜杠(以确保与 Unix 兼容)
-
明确区分绝对路径与相对路径
特别是在 Shell 脚本或 CI/CD 中,路径解析混淆会带来麻烦
● 集成开发环境的处理
#许多 IDE(如 Visual Studio Code、IntelliJ 等)会在内部处理 OS 依赖的路径分隔符。
但对外部文件(CSV、批处理文件、Makefile 等)仍需注意。
● 文件路径分隔符:总结
#- 分隔符差异往往是导致程序无法运行的“地雷”点。
- 内部处理使用 OS 无关的 API,外部表示遵守明确规则至关重要。
总结:应如何面对“两种标准”?
#如前所述,在软件开发现场中,常常存在“两种标准”的情况。
这并非简单的混乱或分裂,而是基于各自的技术背景、历史和目标进行优化后的结果。
这些差异有时被戏称为“宗教战争”,但实际大多是基于设计思想、兼容性和可维护性的选择。
与其问“哪一个正确?”,
更重要的是理解“为何如此”以及“如何协调”。
要在实际环境中无问题地运维,需要关注以下两点:
- “知道存在两种(或更多)标准”
- “能够根据需要灵活应对”
与其被规范和标准差异左右,
能够灵活调整、适应对方才是真正的专业能力。