连接号是一种标点符号。和只有一种形式的间隔号不同,在中国大陆它有三种形式:短横线“–”、一字线“—”和浪纹线“~”。

麻烦的地方在于,国标只规定了这三种形式的长相和宽度,一个字也没提对应的码位。今天通行的U+2013U+2014U+FF5E,是W3C事后替它推断出来的——推断得站得住,但毕竟不是国标白纸黑字的规定,这中间的缝隙塞满了误用。

用途

短横线

  • 化合物的名称或表格、插图的编号,例如:
    • 3–戊酮
    • 表 2–8
  • 门牌号码、电话号码,以及用阿拉伯数字表示的年月日,例如:
    • 安宁里东路 26 号院 3–2–11 室
    • 010–88842603
    • 2011–02–15
  • 在复合名词中起连接作用,例如:
    • 吐鲁番–哈密盆地
  • 某些产品的名称和型号,例如:
    • WZ–10 直升机
  • 汉语拼音、外来语内部的分合,例如:
    • shuōshuō–xiàoxiào(说说笑笑)
    • 盎格鲁–撒克逊人
    • 让–雅克·卢梭(“让–雅克”为双名)
    • 皮埃尔·孟戴斯–弗朗斯(“孟戴斯–弗朗斯”为复姓)

最后两个例子里,短横线和间隔号同时出现,分工正好相反:间隔号分开姓和名,短横线把双名或复姓连成一体。

以上说的都是排版意义上的写法。程序里的日期字段、电话号码字段是另一回事,那里2011-02-15就该是ASCII。

一字线和浪纹线

这两种一般可以互换。

  • 标示相关项目(如时间、地域等)的起止,例如:
    • 沈括(1031—1095),宋朝人。
    • 2011 年 2 月 3 日—10 日
    • 北京—上海特别旅客快车
  • 标示数值范围(由阿拉伯数字或汉字数字构成)的起止,例如:
    • 25~30 g
    • 第五~八课

相关标准

《标点符号用法》(GB/T 15834–2011)

第4.13.1节“定义”说:

标号的一种,标示某些相关联成分之间的连接。

第4.13.2节“形式”说:

连接号的形式有短横线“-”、一字线“―”和浪纹线“~”三种。

(这份广为流传的电子版在这里的短横线打的是ASCII的U+002D,一字线用的是U+2015而不是U+2014——引文照录,两处后面都会说到。)

第5.1.6节说:

连接号中的短横线比汉字“一”略短,占半个字位置;一字线比汉字“一”略长,占一个字位置;浪纹线占一个字位置。连接号上下居中,不出现在一行之首。

注意这一节从头到尾说的都是“比汉字‘一’略短”“占半个字位置”这种排版意义上的长相,没有出现任何码位。这正是后面所有混乱的源头。

附录A.11“连接号用法补充规则”说:

浪纹线连接号用于标示数值范围时,在不引起歧义的情况下,前一数值附加符号或计量单位可省略。

示例:5公斤~100公斤(正)

5~100 公斤(正)

相对1995年的老版本,这一版还砍掉了一种形式。前言说:

取消了连接号中原有的二字线,将连接号形式规范为短横线“-”、一字线“―”和浪纹线“~”,并对三者的功能做了归并与划分(4.13)。

台湾“教育部”《重订标点符号手册》(修订版)

修订说明说:

“连接号”为新增。

也就是说,1987年的旧版根本没有连接号这一条。修订版第21页给出两种形式:

连接号 甲式:— 乙式:~

位置 占一个字的位置,居正中。

说明 用于连接时空的起止或数量的多寡等。

两式可自由选用,例如“西元1811—1872 年”和“西元1811~1872 年”都算对。

讽刺的是,同一页的用法举例里,“那篓黄鱼每条重约1-1.6 公斤”用的却是U+FF0D FULLWIDTH HYPHEN-MINUS,和上面自己定义的甲式并不是同一个字符。这和间隔号那篇里国标自己把间隔号印成全宽是同一类事故。

W3C《中文排版需求》

第5.1节“连接号”把两岸的规定并列了一遍,然后加了一条注:

《标点符号用法》(GB/T 15834—2011)中没有指定这三个符号的码位,但是基本上可以推断一字线是U+2014 EM DASH [—],浪纹线是U+FF5E FULLWIDTH TILDE [~]。但是对于“短横线”,该标准5.1.6节规定“短横线比汉字『一』略短,占半个字位置”,因此可以是U+2013 EN DASH [–]。这些连接号的实际长短根据所用处理系统和使用字体会有区别。

“基本上可以推断”“因此可以是”——这不是国标的规定,是W3C的推理。推理本身站得住,缺的只是国标那一句白纸黑字;可大部分中文技术文章直接把它当成规定来引用,这一步偷换悄无声息。

同一节还说台湾的甲式是U+2013 EN DASH。但《重订标点符号手册》修订版PDF的文字层里,甲式那四处用的其实是U+2014 EM DASH。更要紧的是,手册白纸黑字写着甲式“占一个字的位置”——按W3C自己“半个字用EN DASH、一个字用EM DASH”的逻辑,甲式本就该是U+2014

半个字,谁做到了

U+2013最常挨的一句批评是:Unicode给它的东亚宽度属性是AAmbiguous,歧义),根本没承诺过半宽。

1
2
3
4
5
6
7
8
>>> import unicodedata as u
>>> for c in '-–—~':
...     print(repr(u.east_asian_width(c)), u.name(c))
...
'Na' HYPHEN-MINUS
'A' EN DASH
'A' EM DASH
'F' FULLWIDTH TILDE

这话没错,但它什么也证明不了。A的意思是Unicode替字体做主,把宽度留给语境和字形设计——U+2014AU+2015A间隔号U+00B7也是A。想知道“半个字”到底有没有落地,不能查属性表,得去量字体。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os, unicodedata
from fontTools.ttLib import TTFont, TTCollection

FONTS = [('宋体',     'simsun.ttc',           0), ('黑体',     'simhei.ttf',        None),
         ('楷体',     'simkai.ttf',        None), ('仿宋',     'simfang.ttf',       None),
         ('标楷体',   'kaiu.ttf',          None), ('微软雅黑', 'msyh.ttc',             0),
         ('微软正黑', 'msjh.ttc',             0), ('等线',     'Deng.ttf',          None),
         ('思源黑体', 'NotoSansSC-VF.ttf', None), ('思源宋体', 'NotoSerifSC-VF.ttf', None)]
CHARS = [0x002D, 0x2013, 0x2014, 0x2015, 0xFF5E]

def pad(s, n):                       # 按显示宽度补空格,汉字算两格
    w = sum(2 if unicodedata.east_asian_width(c) in 'WF' else 1 for c in s)
    return s + ' ' * (n - w)

def width(font, cp):                 # 字符宽度,以一个全角字(em)为单位
    g = font.getBestCmap().get(cp)
    return font['hmtx'][g][0] / font['head'].unitsPerEm

print(pad('字体', 10) + ''.join(f'{"U+%04X" % c:>9}' for c in CHARS))
for name, fn, i in FONTS:
    path = os.path.join(r'C:\Windows\Fonts', fn)
    f = TTCollection(path).fonts[i] if i is not None else TTFont(path)
    print(pad(name, 10) + ''.join(f'{width(f, c):>9.2f}' for c in CHARS))

在我这台Windows 11上跑出来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
字体         U+002D   U+2013   U+2014   U+2015   U+FF5E
宋体           0.50     1.00     1.00     1.00     1.00
黑体           0.50     1.00     1.00     1.00     1.00
楷体           0.50     1.00     1.00     1.00     1.00
仿宋           0.50     1.00     1.00     1.00     1.00
标楷体         0.50     1.00     1.00     1.00     1.00
微软雅黑       0.43     0.54     1.08     0.45     1.00
微软正黑       0.43     0.54     1.08     0.45     1.00
等线           0.50     0.50     1.00     1.00     1.00
思源黑体       0.32     0.53     0.88     1.00     1.00
思源宋体       0.34     0.55     0.87     1.00     1.00

数字不够直观,把“吐鲁番–哈密盆地”在四款字体里排出来,短横线占掉的字宽用底色标出来:

“吐鲁番–哈密盆地”分别以宋体、黑体、微软雅黑、思源黑体排版,中间短横线的字宽依次为一个字、一个字、半个字、半个字

界线划得干干净净:把U+2013画成全宽的,全是宋体、黑体、楷体、仿宋、标楷体这一路从GBK字库继承下来的老字体。它们的码位表里U+2013是双字节的A843,双字节就往全宽方格里塞,一刀切,跟这个字符本来该多宽毫无关系。

而微软雅黑、微软正黑、等线、思源黑体、思源宋体——这些是Windows这些年新配的中文字体,加上开源界用得最多的那套——U+2013都落在0.5个字上下,正是国标要的“比汉字‘一’略短,占半个字位置”。

所以“U+2013在中文字体里就是全宽”这个说法已经过期了。半宽做不到,是二十年前字库的历史包袱,不是这个字符的错。该修的是字体,不是码位。

至于U+002D HYPHEN-MINUS,它确实老老实实半宽,但这不构成用它的理由。Unicode在字符表里给它的注释是:

used generically for hyphen, minus sign or en dash, all of which have dedicated alternatives

一键身兼连字符、减号和短横线,是ASCII时代凑合出来的万金油,Unicode自己都建议按用途换用专门的字符。中文的连接号是一个有明确身份的标点,不该由它来顶班。

顺带看看另外两个。U+2014一字线是三个里字体做得最不齐的:宋体一路刚好一个字,微软那两款做到1.08——国标说“比汉字‘一’略长”,只有它们按字面执行了——思源却只有0.87,比“一”还短。只有浪纹线U+FF5E所有字体都精确1.00,因为它是全宽形式(FULLWIDTH TILDE),宽度写在名字里,没有含糊的余地。

上面量的都是中文字体,对着Word、WPS这类排版场景。网页是另一回事:CSS字体栈里西文字体通常排在前面,而U+2013U+2014在西文字体里也有,于是根本轮不到中文字体出场。这未必是坏事——Segoe UI的U+2013是0.50 em、U+2014是1.00 em,U+FF5E它没有、才落回中文字体拿到1.00,三个宽度全部正中国标。你现在看到的这一页就是这么排的。

别和破折号搞混

破折号和一字线长得几乎一样,是最高发的误用。第4.10.2节说:

破折号的形式是“——”。

第5.1.4节说:

破折号标在相应项目之间,占两个字位置,上下居中,不能中间断开分处上行之末和下行之首。

所以两者的区别是:一字线是一个U+2014,破折号是两个U+2014。前者连接起止,后者注释、补充说明或标示语意转折。“沈括(1031—1095)”用一个,“一个矮小而结实的日本中年人——内山老板走了过来”用两个。

如何输入

在微软拼音、搜狗输入法、百度输入法的中文标点状态下:

  • Shift+`Esc下面那个键)得到浪纹线“~”,即U+FF5E
  • Shift+-得到破折号“——”,即两个U+2014。一字线没有直接对应的按键,把破折号删掉一半就是;
  • 短横线同样没有按键,-键给的是ASCII的U+002D

后两个只能靠别的办法:在Word里输入20132014再按Alt+X;或者在输入法里自定义两个短语,一劳永逸。

在HTML中,–得到短横线U+2013—得到一字线U+2014,两个都有好记的命名实体。U+FF5E没有命名实体,只能写~。这里有个坑:˜不是浪纹线,而是U+02DC SMALL TILDE,一个抬得很高的小波浪;∼(大写T)也不是,它是U+223C TILDE OPERATOR

在LaTeX中,--得到短横线U+2013---得到一字线U+2014。坑更大:~在LaTeX里是不折行空格,不会输出波浪线,要用\textasciitilde或者数学模式的$\sim$;中文文档用xeCJK直接写“~”最省心。

几种常见误用

减号键

U+002D HYPHEN-MINUS [-],键盘上直接打出来的那个,也是所有误用里最普遍的一种——-键就在那儿,谁都懒得多想,前面引的那份国标电子版,短横线打的也是它。

它半宽,所以在“3-戊酮”这种地方看着还算过得去,代价是把一个身份含混的ASCII字符塞进了中文标点的位置;拿它当一字线用,“1031-1095”就短得不像话了。要写短横线,写U+2013

全宽减号

U+FF0D FULLWIDTH HYPHEN-MINUS [-]。宽度对了,可它在字体里通常被设计成算术减号的样子——比一字线短、比短横线粗,位置也偏高。台湾“教育部”那本手册就误用了它。

ASCII波浪号

U+007E TILDE [~]。半宽,且在多数字体里贴着字符上沿,和居中的浪纹线完全不是一回事。程序员写惯了~/,很容易顺手打出来。

日文波浪号

U+301C WAVE DASH [〜]。日文语境里的浪纹线,来自JIS编码,中文不建议使用。理由和间隔号那篇里U+30FB KATAKANA MIDDLE DOT不建议用是一样的。

数学符号

U+223C TILDE OPERATOR [∼],表示“相似于”“近似”;U+2212 MINUS SIGN [−],真正的减号。两者在数学排版里各有本分,都不该出现在标点位置上。

横线

U+2015 HORIZONTAL BAR [―],Unicode给它的别名是quotation dash,用途是引出整段引语,不是连接。

它阴险在长度不稳。回头看那张表:宋体、黑体、思源都把它画成1.00,跟一字线一模一样,所以在这些字体下用错了根本看不出来;可微软雅黑和微软正黑给的是0.45,一换字体就缩成半截。网上流传的那份GB/T 15834–2011电子版,第4.13.2节的一字线用的正是U+2015——在宋体里看着没毛病,这大概就是它能一直流传下来的原因。