最近在学一门新语言。此语言近年来越来越受欢迎(为了不引战,就不说具体是啥了,大家应该也能猜到),它的确提出了一种有意思的并发编程模型,但我个人认为在语言(语法)设计上很多地方颇为糟糕,其他人也有评论“看着很美好,写起来全是坑”。在这些“坑”中,一致性差是我认为比较难接受的。举例来说:它的 len(string) 为 11,但在 for in 循环中,string 只能迭代 5 次。
因为本人经验不算丰富,想问问大家能接受这么奇怪的设定吗,或者其他语言中也有这么奇怪的事情吗?我个人觉得这种不一致的设计是非常反人类的。
友好讨论别打架:)
1
wutiantong 2019-05-24 23:22:51 +08:00
猜不到,就你所说的这个“不一致”不是什么大问题。
这块主要是字符编码的锅吧?跟语言没多大关系的。 |
2
vencent OP 我认为:
如果一个字符串是 byte 类型,那调用 len 函数获取到的就应该是有多少个 byte,for in 循环应该是对 byte 数组循环; 如果一个字符串是字符类型,那调用 len 函数获取到的就应该是有多少个字符,for in 循环应该是对字符数组循环。 这种调用 len 函数返回 byte 个数,for in 循环又变成对字符数组进行循环的怪异行为,就非常超出人的正常思维。 好吧,我说的是 go 语言。 |
3
SuperMild 2019-05-24 23:33:18 +08:00 2
你可能用过的语言很少吧,对数字的处理、对字符的处理是每学一种新语言都要特别留意的地方,最容易有差异的地方。
Go 相对来说稍稍更贴近底层多一些,所以处理字符会给你暴露更多细节,让你有更多机会去做特殊操作。 另外,Go 官方文档对此有很清晰的、深入浅出的讲解。 |
4
zjyl1994 2019-05-24 23:37:35 +08:00
go 的话,转成 rune 数组就好了
|
5
wutiantong 2019-05-24 23:38:32 +08:00
@vencent 问题在于你为啥非要把 len() 跟 for-in 绑定在一起,凑成一对儿呢? 我也不是很懂你的这个概念。
|
6
wutiantong 2019-05-24 23:41:06 +08:00
@vencent 可能因为我没学过 go,说的有点想当然;我意思是,如果 for-in 与 len() 是不一致的,那应该能有一个别的函数可以跟 for-in 匹配在一起?
|
7
vencent OP 我认为正常思路来说 len 代表了一个对象的长度,也就是“我能对它迭代多少次”,而 for-in 则是迭代的过程。这两个为什么会有不一样的表现?
我充分了解 rune 类型的存在,但是我想吐槽的其实是语言设计上的问题。既然你都有 rune 类型用来表示字符数组了,为什么 string 还会有这么怪异的表现... |
8
yvescheung 2019-05-24 23:53:59 +08:00
纯英文字符串不会出现这种问题,因为都是一个字节对应一个 rune,中英文或者欧洲语系的字符混合解码是所有编程语言都会遇到的问题,python 中这种坑更多
|
9
vencent OP @yvescheung 能具体讲讲为什么 python 中这种坑更多吗?我主力语言是 python 但是没有感受到这种坑
|
10
KKKKKK 2019-05-25 00:11:29 +08:00 via iPhone
Rust 也有这个问题,string.chars()会以 Utf 编码,所以,长度和直接 len 不一致
|
11
KKKKKK 2019-05-25 00:11:46 +08:00 via iPhone
不过这是个好的设计
|
12
Mohanson 2019-05-25 01:23:18 +08:00 via Android
我玩过的几乎所有现代语言都是这么设计的… 如果一个字符串包含一个中文字, 众所周知它长度是 2,你 for in 的时候,它返回两个 byte 给你,你又要来这里吐槽了
|
13
gamexg 2019-05-25 02:26:02 +08:00 1
golang string 内部是一个 []byte,编码是 utf-8。len 获取的是 []byte 的尺寸,for in 获取的是 rune 。
原因猜测是: 如果希望 len 获取 rune 长度,由于字符串是 utf-8 编码,意味着需要遍历整个字符串才能计算出来长度,这并不是个好主意。 for in 给出 rune 的原因应该是 golang 设计之初就意图解决编码问题,全局默认 utf-8 编码,尽量隐藏编码操作。for in 部分为了不让开发者手工处理编码,那么只能直接给出 rune 类型了。 对于 len,我不希望为了获取 rune 长度而遍历整个字符串。 对于 for in 我其实是可以接受 返回 byte。 但是如果真的返回的是 byte,那么很有可能又会出现 python 里见过的问题,老外开发的一些库未考虑 中文编码,涉及中文操作就会挂,需要奇淫技巧来搞定这个问题。 |
14
hakono 2019-05-25 02:32:32 +08:00 via iPhone
不过是获取长度的方法和别的语言不一样罢了这都能吐槽。。。
go 的话要获取 Unicode 字符数字使用 utf8.RuneCountInString(str1) 或者 len( []rune(str1) ) 丑是丑了点但你都 for range 把 unicode 字符一个个取出来了,一搬也不会有统计有几个 Unicode 的需求了。。。 |
15
dacapoday 2019-05-25 02:33:01 +08:00 2
len 的语义更接近于表示数据的尺寸,而非容器的容量或长度。迭代则以容器的容量为单位。
|
16
exonuclease 2019-05-25 02:41:51 +08:00 1
len 看起来是一个比较通用的函数 它应该是直接计算占了多少内存的 字符串会有专门的计算长度用的函数吧
|
17
hhhsuan 2019-05-25 09:24:12 +08:00 1
把 string 和 string.bytes 分开就合理了,go 的设计确实有问题。
len(string) == utf8 字符个数 for c in string == 遍历 utf8 字符 len(string.btyes) == byte 个数 for b in string.bytes == 遍历 bytes |
18
passerbytiny 2019-05-25 09:38:44 +08:00 2
@Mohanson #11 错。Java 从 90 年代起,一个中文字的长度就是 1。对于同样采用 UTF -16 编码的 Javascript 来说,中文字的长度也是 1。对于采用 UTF-8 的语言——例如 PHP,中文字的长度是 3,但它们一般会额外提供获取字符长度的函数。你所谓的众所周知,只存在于仍然可以采用(并且人为强制使用) GB2312 或 GBK 编码的古老语言或古老开发方法中。
@vencent 你说的这个,压根就不算语言设计问题,因为 for in 循环才是语言设计,len(string) 只是个函数,压根谈不上设定。而你对 len(string)的理解有偏差,Java、PHP、Oracle ( varchar),与字符串有关的 lengh 方法,含义都是字节个数,而不是字符个数,go 维持传统是没错的。之所以有坑,是因为没有额外提供像 Java 的 String.codePointCount() 、PHP 的 mb_strlen()、Oracle 的 nvarchar 这样的补偿机制。 不过我看你的举例,貌似 Go 的语言编码不是 UTF-16,这就真是语言设计问题了。 顺便最后说一下,Java 中,只有在其中没有 Unicode SMP 的字符的时候,String.length() 才等于字符个数,否则也是不等于的。而该机制是从 Java 1.5 就开始的,是很早之前的事。以后再碰到说 Java 由于采用 Unicode 所以所有单个字符的长度都是 1 的人,你应该知道他学艺不精。 |
19
Death 2019-05-25 10:10:40 +08:00
或许可以这么理解,len 是面向底层的,for in 是面向使用者的
|
20
polebug 2019-05-25 10:35:37 +08:00 via Android
这有什么关系呢 一门语言的设计 理解它为什么这样设计不就好了吗 by the way 没有继承之类的设计 你接受了吗
|
21
vencent OP @dacapoday
@exonuclease 起初我也想过会不会“ len 的语义更接近于表示数据的尺寸,而非容器的容量或长度”。然而官方文档告诉我们: func len(v Type) int The len built-in function returns the length of v, according to its type: Array: the number of elements in v. Pointer to array: the number of elements in *v (even if v is nil). Slice, or map: the number of elements in v; if v is nil, len(v) is zero. String: the number of bytes in v. Channel: the number of elements queued (unread) in the channel buffer; if v is nil, len(v) is zero. 可见除了 string 之外,其余类型作为 len 函数的参数获取到的都是“容器的容量或长度” |
23
mooncakejs 2019-05-25 11:18:18 +08:00 via iPhone
没有提供 byteLen,
|
24
Nooooobycat 2019-05-25 11:19:38 +08:00 via Android
strings.Count
|
25
misaka19000 2019-05-25 11:20:30 +08:00
这个确实有点别扭
|
26
luozic 2019-05-25 11:24:50 +08:00 via iPhone
含义混乱
|
27
azh7138m 2019-05-25 11:28:46 +08:00
文字处理复杂的很,我猜 迭代的是 "字",长度求的是 byte,那我觉得 ok。
|
28
liulaomo 2019-05-25 11:29:25 +08:00
遍历 byte 的时候,使用 for i, b := range []byte(s) {} 即可(标准编译器对[]byte(s)做了优化,不会赋值底层字节),或者使用 for i := 0; i < len(s); i++ { b := s[i]; ...}。
遍历 rune 的时候,使用 for i, r := range s。 这么设计虽然牺牲了一点一致性,但达到了两全。 |
29
vencent OP 感谢 @passerbytiny 的指教。
> len(string) 只是个函数,压根谈不上设定。 其实我认为在一门语言中,len 函数的作用应该保持一致性,也就是说,它不能一会儿表示的是数据的内存尺寸,一会儿表示的是容器的容量或长度。这方面 Python 做的非常好,所有内置类型表现的非常一致。包括 str 类型,返回的也是字符个数而不是 bytes 个数(顺便一提,感谢指出 Java、PHP 与字符串有关的 lengh 方法含义都是字节个数。不过我其实认为这样的“传统”是不对的)。即使自定义类型也可以实现 Sized 接口(即实现__len__方法)使得对象的长度可以通过 len 函数获取。而 go 语言的 len 函数对于其他内置类型来说获取的是容器的长度(即元素的个数),而对于 string 类型获取的却是字节数。这种奇怪的不一致我认为是它的设计问题。 |
30
CRVV 2019-05-25 12:08:42 +08:00
@vencent
> 而 go 语言的 len 函数对于其他内置类型来说获取的是容器的长度(即元素的个数),而对于 string 类型获取的却是字节数。这种奇怪的不一致我认为是它的设计问题。 对所有类型,len 返回的都是 总字节数 / sizeof(一个元素) 对字符串,一个元素指的是 byte,否则 sizeof(一个元素) 不是常数 这是它一致的地方 另一个 len 不返回 code point 数量的原因是,得到字符串里 code point 个数需要把整个字符串遍历一遍,这样会慢 另一方面,code point 的个数也不一定是你想要的结果,因为在某些情况下多个 code point 才对应一个字,你会不会想让 len 返回字的个数? 至于为什么 for _, char := range "string" 是用 code point 来遍历而不用 byte 来遍历 我觉得你不会希望用 byte 来遍历字符串的 |
31
passerbytiny 2019-05-25 12:53:25 +08:00
@vencent #25 只有字符串才有字符个数,所以我不明白你说的“所有类型,包括 str 返回的都是字符个数”是什么意思。你如果要追求所有类型一致,那么只有基于 bytes 才有可能一致。
你对 func len(v Type) int 的文档的理解也不对,Array、map、String、Channel 返回的全部是元素个数,String 的单个元素是字节,不是字符。你需要知道的是,String 只是表面上看起来是一个有序字符序列,但本质上是表示一段字符的特殊数据结构。至于它的长度是字节数还是字符数,通常取决于大多数人怎么干,而大多数人选择字节数。不是不想选择字节数,而是真得好难。 |
32
passerbytiny 2019-05-25 13:04:25 +08:00
我突然发现了一个问题,怎么能迭代 String ?
这点还是 Java 做得好,String 是个单体不可变对象,完全没有集合的性质,根本不能被迭代。要想迭代,toCharArray() 得到 char[] charArray、 {一堆操作得到 String[] strArray}、或者{一堆操作得到 int[] codePointArray}。 |
33
jackmod 2019-05-25 13:10:32 +08:00
string 的内容是什么。有些东西会按某种 encode 计数,那确实是坑。
|
34
cnt2ex 2019-05-25 13:10:53 +08:00
如果我们现有字符( character )的定义,那么把字符串( string )的定义为字符的一个序列( a sequence of characters )。*自然地*,len 应该返回一个字符串含有多少个字符,而不是根据内部实现返回这个字符串占据了多大的空间。顶层的定义不应该被低层实现给限制。
|
35
SuperMild 2019-05-25 13:20:04 +08:00
@passerbytiny 作为单体对象就必须保存一些辅助信息,占用内存增加,各种转换又需要额外的内存和运算。而 Go 的设计者明显希望弄一些比较简单、相对更底层一些的数据结构,减少占用内存和运算。比如 slice 就十分精妙,模型很简单,需要携带的信息很少,但用起来也足够方便,string 也是采用类似的设计思想。
|
36
dacapoday 2019-05-25 15:33:09 +08:00
@vencent #21 你可能在字符串是数值还是容器上存在疑惑。从底层的角度,String 不过是一个元素类型为 char 的不定长数组。但是在多数语言中,对字符串采用 immutable 数据结构实现,声明的 String 类型,其行为更类似数据值而非容器引用。
go 这里“ String: the number of bytes in v.”就是表示字符串的数据有多大,而不是有几个字符。由于 go 采用的 utf-8 为变长编码,byte 的索引序与字符索引序不一致。如果期望字符的行为和传统语言(如 java)一致,应当考虑 go 中的 rune 类型(码点),以及 unicode 包进行处理。 |
37
dacapoday 2019-05-25 15:45:58 +08:00
@dacapoday #36 即使从语言设计上,题中“ len 和 for in 行为不一致”也是伪命题。len 面向底层,字符串处理则是更高层次的,对象化的数据操作。go 的应用领域多在后端,必然需要访问底层数据结构的能力; python 则是脚本语言,期望简易理解和操作。其版本 3 更是希望能回避版本 2 中糟糕的编码问题,选择全面封装字符串的编码实现。
|
38
karllynn 2019-05-25 15:56:45 +08:00
大哥,这是你不会用
[]byte, string, []rune 之间相互转换先学会 |
39
dacapoday 2019-05-25 15:58:00 +08:00
@vencent #29 归根结底,go 和 python 是两种语言,不能因为函数名都是'len'就想当然认为其语义和行为一致。
|
40
mornlight 2019-05-25 17:20:16 +08:00
「我个人觉得这种不一致的设计是非常反人类的。」
不认同。Go 里面很多设计是取舍后的结果,不同语言的设计哲学不同很正常。 |
41
dyxLike 2019-05-25 18:15:16 +08:00 via Android
@passerbytiny 准确来说 len 应该不是字节个数,而是 unicode 单元个数吧,一个字符用 utf-8 表示 len 可能是 1/2/3/4 用 utf-16 表示可能是 1 或者 2
|
42
love 2019-05-25 18:36:53 +08:00
|
45
cpdyj0 2019-05-25 18:44:18 +08:00
没咋用过 go,但是这个设计确实很别扭
|
46
cpdyj0 2019-05-25 18:47:05 +08:00
@Mohanson 那啥,反正 Java 中的操作是 String 一律存储 UTF-16 (其实这不好),然后返回 UTF-16 码点的数量。严格来说不是字符个数,只能算是 codepoint 数量,不过一般情况下可以等价为字符个数。
|
47
lrxiao 2019-05-25 19:05:52 +08:00
问题在于 string 处理的复杂度。。这和语言没什么关系了吧
|
48
abcbuzhiming 2019-05-25 19:09:31 +08:00
Go 还是个很年轻的语言,所以有些设计上的问题是很正常的各位,不要神话一门语言,就算它是 google 发明出来的也一样。具体到楼主的问题,我个人的看法是,Go 最开始是设计出来取代的 C 的,非常偏底层,所以字符串这块的抽象就不像其他更高级的语言,我个人认为这个可能会和 Go 的价值取向有关,如果 Go 以后打算进一步去应用层,那么迟早会变的和其它高级语言类似,如果 Go 的想法就是扎根底层,那很可能也就是这样了
|
49
tairan2006 2019-05-25 19:17:25 +08:00 via Android
Python2 的 len 也是错的,你用中文试试。Java 的 unicode 设计更失败,CPP 就不说了,压根不考虑编码。go 的已经够好用了好吗,转成 rune 数组不就完了。
|
50
linvaux 2019-05-25 19:19:02 +08:00 via Android
怎么感觉在说 go 呢?
|
51
ysc3839 2019-05-25 19:21:15 +08:00
一开始以为说的是 Python。
印象中 Python 是用 wchar_t 来存 Unicode 字符串的,在 Windows 上 wchar_t 是 2 字节的,所以遇到一些占两个 wchar_t 的字符可能会掉坑里? 刚刚去查了一下,Python 3.3 开始引入了 Flexible String Representation https://www.python.org/dev/peps/pep-0393/ 可以根据内容来选择 1, 2, 4 字节。 |
52
elfive 2019-05-26 06:09:34 +08:00 via iPhone
字节串和字符串的一点点区别
|
53
jinliming2 2019-05-26 08:15:50 +08:00 via iPhone
JavaScript 在处理高位 Unicode 也是这样的问题,用 for in 没问题,但是一个字符被拆成多个字符,用 for of 可以正确处理这些字符,但是迭代次数又和 length 不一样。
|
54
bringyou 2019-05-26 13:07:38 +08:00
go 迭代 string 的时候,rune 和 byte 的区别吧。
|
55
liulaomo 2019-05-26 22:24:23 +08:00
@vencent
> 而 go 语言的 len 函数对于其他内置类型来说获取的是容器的长度(即元素的个数),而对于 string 类型获取的却是字节数。这种奇怪的不一致我认为是它的设计问题。 这是一个误解。Go 种的字符串应该被看作是一个只读字节切片。也就是说,字符串(做为容器)的元素类型为 byte。 |
56
liulaomo 2019-05-26 22:30:14 +08:00
但是,遍历一个字符串得到的是 rune 而不是它的 byte 元素,这确实是一个例外 https://gfw.go101.org/article/exceptions.html#container-elements-iteration
Go 里面这样的细节上例外很多,但是 Go 在宏观设计上的一致性比较高,例外很少。 |