单个Emoji字符的字符串长度函数评估大于1

2021-03-27 06:38:35

这不是错误的那个"🤦🏼♂️" .length == 7

但它更好的是那个"🤦🏼♂️" .len()== 17,而不是无用的那个len("🤦🏼♂️")== 5

不时地显示,在JavaScript中,包含EMOJI的字符串的长度导致数量大于1(通常为2),然后进行得出的结论,即哈哈JavaScript是如此破碎 - 许多喜欢的奖励。在这篇文章中,我会尝试说服你嘲笑JavaScript,这比首次出现的洞察力不那么富有洞察力,并且Swift的字符串长度的方法不是明确的最佳状态。但是,Python 3的方法是明确的最糟糕的方法。

"🤦🏼♂️" .Length == 7作为JavaScript评估为True。让我们在Firefox中尝试JavaScript控制台:

哈哈,对吗?好吧,你被告知,Python社区遭受了Python 2对阵Python 3拆分,以及其他事情来获得Unicode右。让我们试试Python 3:

$ python3python 3.6.8(默认值,2019年1月14日,11:02:34)[GCC 8.0.1 20180414(实验)[Trunk Revision 259383]在Linuxtype"帮助","版权&# 34;,"学分和#34;或"许可证"有关更多信息。>>> Len("🤦🏼♂️")== 5true>>>

好吧。现在,Rust有利于从之前的语言学习。让我们尝试生锈:

$ cargo new -q loget $ cd长度$ echo' fn main(){println!(" {}""🤦🏼♂️" .len() == 17); }' > src / main.rs $货物运行-qtrue

包含一个图形单元的字符串由5个Unicode标量值组成。首先,有一个基本的角色,这意味着一个人的面孔柏。默认情况下,该人将有一种卡通黄色。下一个字符是一个表情符号Skintone修饰符,改变了人皮肤的颜色(以及在实践中,也是人的头发的颜色)。默认情况下,该人的性别未定义,例如,苹果默认为他们认为男性外观和例如, Google默认为他们认为女性外观。接下来的两个标量值明确地选择了一个男性典型的外观,无论字体和供应商如何。性别规范而不是像肤色一样的表达特定的修饰语,而是使用零宽度木匠用(皮肤定位的)面部柏树人明确地连接的表情符号预测性别符号(男性标志)。 (是否是一种很好的或坏的想法,即肤色和性别规范使用不同的机制是不可能的。)最后,变体选择器-16使我们能够明确到我们想要多色EMOJI渲染而不是单色。 Dingbat渲染。

上面的每种语言都将字符串长度报告为字符串占用的代码单元数。 Python 3字符串存储Unicode代码点每个由cpython 3存储为一个代码单元,因此字符串占用5个代码单元。 JavaScript(和Java)字符串具有(潜在无效)UTF-16语义,因此字符串占用7个代码单元。 rust字符串(保证有效)UTF-8,因此字符串占用17个代码单元。我们将稍后再次返回实际存储,而不是语义。

关于Python 3在2019-09-09中添加了关于Python 3:最初本文声称Python 3保证了UTF-32有效性。这是错误的。 Python 3保证字符串的单位保持在Unicode码点范围内,但不保证缺少代理人。它不仅允许未配对代理,这可能是通过希望与潜在的UTF-16的价值空间兼容来解释,但Python 3允许实现甚至代理对,这是一个真正的奇异设计。之前的结论是达到的结论,即Python 3比我想象的更搞砸了!随着示例字符串在Python 3中构造的方式,突然发生Python 3字符串以匹配字符串的有效UTF-32表示,因此它仍然是UTF-32的说明,但其余的文章已经略微编辑避免声称Python 3使用UTF-32。

有一种语言。以下使用的Swift 4.2.3,这是我在ubuntu 18.04上研究的最新版本:

$ mkdir swiftlen $ cd swiftlen / $ swift包init-q-type可执行文件$ swift包init - type executablecreation可执行包:swiftlencreating package.swiftcreating Readme.mdcreating .gitignoreCreating源/创建源/ swiftlen / main.swift创建测试/创建测试/ Linuxmain.SwiftCroaring测试/ SwiftLentests /创建测试/ SwiftLentests / SwiftLentests.Swift创建测试/ SwiftLentests / XCTestmanifests.swift $ Echo'打印("🤦🏼♂️" .count == 1) ' >源/ swiftlen / main.swift $ swift运行swiftlen 2> / dev / nulltrue

(不使用Swift Repl为例,因为它似乎没有接受Ubuntu上的非ASCII输入!SWIFT 5.0.3打印相同,REPL仍然被打破。)

好的,所以我们发现了一种认为字符串包含一个可数单元的语言。但是可数单位是什么?这是一个扩展的图形集群。 (“扩展”以区分从旧的尝试定义现在称为传统图形集群的图形集群。)定义在Unicode标准附件#29中(UAX#29)。

给定有效的Unicode字符串和Unicode的版本,所有上述都是明确定义的,并且它保存在列表上更高的每个项目比列表上更低的项目更大或相等。

其中一个并不像其他人,但是:前三个数字对于任何有效的Unicode字符串,它是否包含当前分配的标量值或者是来自未来的任何有效的Unicode字符串,并包含目前写的软件的未分配标量值知道的。此外,计算前三个长度不涉及来自Unicode数据库的查找。但是,最后一个项目取决于Unicode版本,涉及从Unicode数据库查找。如果字符串包含程序使用的Unicode数据库的副本,则该字符串值是所知道的,则该程序将可能会占据字符串中的扩展图形群集,而与Unicode数据库的副本更新的程序相比,该程序相比有关这些标量值的分配(以及其中一些分配结果是组合字符)。

给定的编程语言必须仅选择上述一个语言是不这样的。如果我们运行此Swift程序:

使用unicode_segmentation :: Unicodesegation; fn main(){let s ="🤦🏼♂️&#34 ;; println!(" {}}",s.graphemes(true).count()); println!(" {}",s.chars()。count()); println!(" {}}",s.encode_utf16()。count()); println!(" {}",s.len());}

那是出乎意料的!事实证明,Unicode-segsation不实现Unicode分段规则的最新版本,因此它给出了Zero Widey Joiner通用处理(ZWJ之后的右侧)而不是表情符号上下文中的较新的细化。

让我们再试一次,但这次与Unic-segment =" 0.9.0"在Cargo.toml:使用Unic_segment :: Graphemes; Fn Main(){Let S ="🤦🏼♂️&#34 ;; Println!(" {}",graphemes :: new(s).count()); println!(" {}",s.chars()。count()); println!(" {}}",s.encode_utf16()。count()); println!(" {}",s.len());}

在生锈案例中,字符串(这里只有字符串切片)知道它们包含的UTF-8代码单元的数量。 LEN()方法调用只返回自创建字符串(在这种情况下,编译时间)以来已存储的此数字。在其他情况下,会发生什么是创建迭代器,而不是实际检查迭代器将产生的迭代器将产生的值(字符串切片,Unicode群集,Unicode标量值或UTF-16代码单元)方法只需消耗迭代器并返回由迭代所产生的项目数。之后,计数未存储在字符串(切片)上的任何位置。如果我们想再次稍后知道计数,我们必须再次迭代字符串。

这在设计空间中引入了一个值得注意的问题:如果创建字符串时会热切地计算给定类型的长度数量?或者如果有人要求它应该计算长度?或者当有人要求它然后自动存储在字符串对象中时,它应该计算,以便如果有人再次要求它可以立即可用?

答案Rust具有语言的Unicode编码形式的代码单元中的长度存储在字符串创建时,并且当有人要求它们(然后忘记并不存储在字符串上时,其余的

SWIFT是一种更高级别的语言,并没有将其字符串内部内部的确切性质作为API合同的一部分记录。事实上,SWIFT串的内部表示在SWIFT 4.2和SWIFT 5.0之间大大变化。例如,如果对字符串的不同视图保持在创建次数,则不会记录。文档确实说字符串是复制写入的,因此第一个突变可能涉及复制字符串的存储。

值得注意的是,设计空间不包括任何内容。 C编程语言是这种情况的一个突出示例。 C字符串甚至没有记住他们的代码单位数。要找出代码单元的数量,您必须在Sentinel值旁边迭代字符串。在C的情况下,Sentinel是U + 0000的代码单元,因此它从可能的字符串内容中排除一个Unicode标量值。但是,这不是一个基于Sentinel的设计的严格必要的属性,这些属性不记得任何长度。 0xFF不会在任何有效的UTF-8字符串中作为代码单元发生,并且在任何有效的UTF-32字符串中都不会发生0xFFFFFFFF,因此它们可以分别用作UTF-8和UTF-32存储的Sentinels,而无需排除A来自Unicode值空间的标量值。没有16位值,永远不会发生在有效的UTF-16字符串中。但是,有效的UTF-16字符串不包含未配对的代理,因此原则上,未配对的低代理人可以用作想要使用保证有效的UTF-16字符串的设计中的Sentinel,这些rom-16字符串不记得其代码单位长度。

在其存储原始Unicode编码形式的代码单元中计数的字符串的长度(即,以utf-8,UTF-16和UTF 32为其字符串语义选择的编程语言)不像另一个长度。它的长度是在创建新字符串时必须知道必须知道的长度,因为它是能够为字符串分配存储所需的长度。偶数C,它迅速忘记在创建字符串之后的存储原本机Unicode编码表单中的代码单元长度,必须在为新字符串分配存储时知道此长度。

也就是说,设计决定是关于是否要记住这一长度。它不是关于是否急切地计算它。你只需要在String创建时间 - 即。热切地。

考虑到记住此数量使得字符串连接是一个常见的操作,与不记得这个数量相比,实现的速度基本上更快,记住此数量是根本合理的。此外,这意味着您不需要维护Sentinel值,这意味着子字符串操作可以产生与原始字符串共享缓冲器的结果,而不是必须复制以便能够插入哨兵。 (请注意,如果您希望急切地保持零终止,因此可以轻松地融合这种福利,以便C字符串兼容性。)

即使我们已经确定字符串实现是有道理的,以记住代码单元中的字符串的存储长度,也不回答字符串实现是否应该记住其他长度或哪种类型应在最符合人体工程学的API中提供长度。 (正如我们上面的那样,SWIFT使得扩展的石墨簇数量更加符合人体工程学,以获得代码单元或标量值长度。)

此外,如果要记住任何其他长度,那么有问题是应该被急切地计算为字符串创建时间还是懒洋地计算有人第一次要求它。很容易看出为什么至少后者没有意义于像生锈这样的多线程系统编程语言。如果对象的某些属性懒惰地初始化,则在多线程的情况下,还需要解决这些计算的同步。此外,如果要能够稍后添加辅助信息,则需要至少针对指向辅助信息的指针分配空间或者您需要具有字符串信息所在的辅助信息的哈希表是关键,因此辅助信息,即使在不存在时,也具有在运行时系统中具有全局状态的存储影响或含义。最后,对于系统编程,即使它意味着“始终O(n)”而不是“可能O(n)但有时O(1)”,也可能更希望清楚地清楚地了解给定操作的时间复杂度。即使后者严格看起来,它也不太可预测。

对于更高级别的语言,空间需求或同步问题的参数可能不会导致决定性。它更重要的是考虑一些给定的长度数量。这通常忘记围绕互联网辩论,这些辩论围绕最长度是最“正确”或“逻辑”的争论。因此,对于不映射到存储分配大小的长度,它们有什么好处?

事实证明,在Firefox代码库中有两个地方有人想要知道未被存储为UTF-32的字符串中的Unicode标量值的数量,并且不支付给标量值实际的注意力。用于WebRTC的NAT(STUN)的会话遍历实用程序的IETF规范具有奇和的属性,其在某些协议字符串上置于长度限制,使得限制表示为Unicode标量值的数量,但在UTF-8中发送字符串。 Firefox验证了这些限制。 (限制看起来像一个任意的两个功率(128个标量值)。规范有关于可能结果的字节长度的备注,根据IETF UTF-8 RFC,这是近五年的IETF UTF-8 RFC错误STUN RFC的出版时间。具体来说,令人惊叹的RFC重复说,UTF-8的128个字符可以是763字节。要到达该号码,您必须假设UTF-8字符可以启动长度为6个字节,而不是在普遍的UTF-8 RFC和Unicode标准中相反,并且128的最后一个字符是零终止者,因此已知只需要一个字节。 )在这种情况下,希望知道非存储长度的原因是施加限制。另一种情况是报告JavaScript错误的源位置的列号。

我们将返回的长度限制可能不是频繁的使用情况,以证明制作字符串知道特定的长度,而不是在被要求时计算的长度。既不是错误消息。

用于询问长度的另一个用例是通过索引迭代并使用长度作为循环终止条件1990s Java样式。像这样:

for(int i = 0; i< s.length(); i ++){//与s.charat(i)做某事}

在这种情况下,它实际上对于字符串对象上的数字的长度实际上是重要的。此用例与索引到字符串的要求,以找到与“长度”代表的单位计数对应的第n个单元应该是快速操作。

以上模式的结论在于应该预先计算的长度(以及索引单元应该是什么),它是首次出现的。上述循环不按索引随机访问。它顺序使用从零到零的每个索引,但不包括长度。实际上,特别是当通过Unicode标量值迭代字符串时,通常在检查字符串的内容时,您按顺序迭代字符串。这些天编程语言为此提供了迭代器设施,而例如,要通过标量值迭代UTF-8字符串,迭代器不需要了解前面的标量值的数量。例如。在Rust中,尽管字符串切片不知道它们的Unicode标量值数量,但您可以在O(n)时间中执行此操作:

(请注意,CHAR是C和C ++中的8位代码单元(可能的UTF-8代码单元),CHAR是Java中的UTF-16代码单元,CHAR是RURE中的UNICODE标量值,并且字符是一个扩展的石墨剧烈蔓延。)

与其库生态系统一起的编程语言应由Unicode标量值和扩展的图形群集提供迭代,但它不遵循字符串需要了解标量值长度或扩展的图形群集长度向前了解。与代码单元存储长度不同,那些数量对于加速同时加速的操作不适用于不关心字符串的确切内容。具有字符串在其存储原生Unicode编码形式中知道其代码单元长度的观察是非常合理的,不应回答代码单元应该是多少位。

接近这个问题的通常方法是争辩说UTF-32是最好的,因为它在字符意义上通过“字符”提供了o(1)索引,这意味着Unicode标量值,或者参数侧重于utf- 8对UTF-16的某种语言是不公平的。我认为这些是解决这个问题的糟糕方式。

首先,答案应该是UTF-32的论点在两个计数上是糟糕的。首先,它假设随机访问标量值很重要,但实际上它不是。希望通过标量值迭代字符串的功能是合理的,但标量值的随机访问是在YAGNI部门。其次,赞成UTF-32的论据通常会出现在提出论证的人在UTF-16中的代理对,但尚未了解到用户认为是单位的更大的东西甚至更大的图形群集。也就是说,如果逃避UTF-16的变量宽度性质到UTF-32,则通过加倍存储器要求,扩展的石墨簇仍然是可变的宽度。

我稍后会回到长度的公平问题,但我认为在实践中,可以在实践中进行不同的论点,以便选择内存的Unicode编码形式。更相关的参数是:选择UTF-8实际上接受UTF-8存储要求的实现。当更宽单元语义选择了不提供原始内存访问,因此语言,有机会来调整字符串存储,实现方式试图想出的办法,以避免实际支付更广泛的单位成本在某些情况。

JavaScript和Java字符串具有潜在无效的UTF-16的语义。 SpiderMoNkey和V8实现了一个优化,以省略字符串中的每个代码单元的前导零,即将字符串存储为ISO-8859-1(实际的ISO-8859-1,而不是“ISO-8859-1”的Web概念作为Windows-1252的标签),当字符串中的所有代码单位都有零,最重要的一半。 Hotspot JVM还实现了这种优化,但使其是可选的。 Swift 4.2实现了相同思想的略微不同的变体,其中仅存储ASCII字符串作为8位单元,并且其他所有内容存储为UTF-16。 CPython自3.3自3级与代码点语义进行相同的思想:如果至少一个代码点具有高于16位上方的非零位,则串以32位代码单元存储。否则如果字符串在低8位上方具有至少一个代码点的非零位,则将字符串存储为16位单元。

......