【IT行业杂学】“两种正义”交汇之时:潜藏在软件中的标准分裂真相

日本語|English|中国语
| 11 min read
Author: shuichi-takatsu shuichi-takatsuの画像
Information

为了覆盖更广泛的受众,这篇文章已从日语翻译而来。
您可以在这里找到原始版本。

引言:行业潜藏的“两种标准”问题

#

在软件开发的世界中,经常存在规格或行为分为两派的“标准分裂”现象。
人们常会问“哪一种是正确的?”,但实际上这只是因为各自有其历史或技术背景罢了。

此次将介绍一些与这种“两种标准”相关的例子。


数组从“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”开始:

  • 矩阵:A1,1,A1,2,,An,mA_{1,1}, A_{1,2}, …, A_{n,m}
  • 求和符号:i=1nai\sum_{i=1}^n a_i
  • 斐波那契数列:F1=1,F2=1,F3=2,F_1 = 1, F_2 = 1, F_3 = 2, …

这是基于“在‘数数’的概念中,第一个被计为第 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”开始:重视数学自然性和公式可读性
  • 自由设置型:可实现高灵活性和高抽象度的设计

并非哪一种“正确”,而是根据用途和上下文做出合适的选择这一点才重要。

Information

在 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]
  • 理念:类似人类十进制的书写方式,先读高位,注重直观的可读性
  • 背景:也有一些指令集架构设计为“操作码 → 操作数”的顺序
Information

作为采用大端的 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 等设计为无需关心字节序即可使用

由此可见,事先认识字节序差异并进行明确处理对于构建可靠的软件至关重要。

Information

● 名称由来
“大端(Big)”“小端(Little)”一名源于乔纳森·斯威夫特《格列佛游记》中“从大端还是从小端敲开煮熟的蛋”之争(Big-Endian vs Little-Endian)。
正是象征着“由微小差异引发的严重对立”。


栈上参数的压入方式:从前往后?还是从后往前?

#

调用函数时,参数通常会被压入栈(push)后传递。

Information

● 关于栈:
栈是在内存中以“后进先出(LIFO)”方式管理数据的区域,用于存放函数的参数、返回地址、本地变量等。
每次调用函数时都会压入新的栈帧,函数结束时则统一弹出(pop),这是基本的运行机制。

在此过程中需注意两点:

  1. 将参数压入栈的顺序是“从前(左边)还是从后(右边)?”
  2. 调用后,谁负责释放栈上参数区域(收尾)?
    • 由调用方(caller)清理?
    • 由被调用方(callee)清理?

这些差异称为“调用约定(calling convention)”,会因使用的架构和平台而异。
若不了解其规则,函数调用后可能导致栈状态混乱,进而出现意外行为或崩溃。

● 参数的压入顺序:从前还是从后?

#
  • 右到左(从后往前压)
    最常见的方式(如:cdecl

    • 与可变参数(如 printf())兼容性好
    • 最后一个参数最先被压入栈
  • 左到右(从前往后压):Pascal 系调用约定(如:pascalfastcall

    • 与可变参数(如 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)”进行。

■ 左到右(从前往后压)方式:以 pascalfastcall 等为例

result := sum(1, 2, 3);  // 类 Pascal 调用

此时,栈中按以下顺序压入:

push 1  ← 第一个参数
push 2
push 3  ← 最后一个参数
call sum

→ 在栈上,“参数顺序与调用顺序一致”。
→ 可读性高,易于观察,但不适合可变参数。
→ 栈清理多由“被调用方(callee)”进行。

Information

● 补充
实际上也存在寄存器传递(如 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 混用的环境中,乱码、错误处理、解析困难 等问题频发
Information

● 在 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(持续集成)中加入换行检查
Information

● 补充: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
Information

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()
  • 在脚本或配置文件中始终使用斜杠(以确保与 Unix 兼容)

  • 明确区分绝对路径与相对路径
    特别是在 Shell 脚本或 CI/CD 中,路径解析混淆会带来麻烦

● 集成开发环境的处理

#

许多 IDE(如 Visual Studio Code、IntelliJ 等)会在内部处理 OS 依赖的路径分隔符。
对外部文件(CSV、批处理文件、Makefile 等)仍需注意

● 文件路径分隔符:总结

#
  • 分隔符差异往往是导致程序无法运行的“地雷”点。
  • 内部处理使用 OS 无关的 API,外部表示遵守明确规则至关重要。

总结:应如何面对“两种标准”?

#

如前所述,在软件开发现场中,常常存在“两种标准”的情况。
这并非简单的混乱或分裂,而是基于各自的技术背景、历史和目标进行优化后的结果。

这些差异有时被戏称为“宗教战争”,但实际大多是基于设计思想、兼容性和可维护性的选择。

与其问“哪一个正确?”,
更重要的是理解“为何如此”以及“如何协调”

要在实际环境中无问题地运维,需要关注以下两点:

  • “知道存在两种(或更多)标准”
  • “能够根据需要灵活应对”

与其被规范和标准差异左右,
能够灵活调整、适应对方才是真正的专业能力

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。