一把双刃剑:Scratch数据类型的隐式转换

网友投稿 2019-11-28 10:50

笔者曾经也写过一篇关于数据类型的文章:《从数据类型到计算思维》,对https://cdn.china-scratch.com/timg/191130/1050244214-0.jpg这个积木在数据类型不同的情况下的执行逻辑做了一个简单汇总,并总结了一些规律。事实上,Scratch是一门弱数据类型的语言,不同于强类型语言在遇到函数引数类型和实际调用类型不匹配的情况经常会直接出错或者编译失败,弱数据类型的语言会进行隐式转换,有时会产生难以意料的结果。本文将在前篇《从数据类型到计算思维》的基础上对Scratch数据类型隐式转换问题进行补充、完善。

01

在前篇从数据类型到计算思维》中,通过几个简单的测试,总结了几条https://cdn.china-scratch.com/timg/191130/1050244214-0.jpg积木在不同数据类型情况下执行逻辑的规律。

源码佐证:

_setCostume (target, requestedCostume, optZeroIndex) {if (typeof requestedCostume === 'number') {// Numbers should be treated as costume indices, alwaystarget.setCostume(optZeroIndex ? requestedCostume : requestedCostume - 1);} else {// Strings should be treated as costume names, where possibleconst costumeIndex = target.getCostumeIndexByName(requestedCostume.toString());

数字类型的参数被当做造型编号来执行,字符类型的参数被当做造型名称来执行(前提是有对应的造型名称时)

if (costumeIndex !== -1) {target.setCostume(costumeIndex);} else if (requestedCostume === 'next costume') {target.setCostume(target.currentCostume + 1);} else if (requestedCostume === 'previous costume') {target.setCostume(target.currentCostume - 1);// Try to cast the string to a number (and treat it as a costume index)// Pure whitespace should not be treated as a number// Note: isNaN will cast the string to a number before checking if it's NaN} else if (!(isNaN(requestedCostume) || Cast.isWhiteSpace(requestedCostume))) {target.setCostume(optZeroIndex ? Number(requestedCostume) : Number(requestedCostume) - 1);}

字符类型的参数会被优先“尝试”转换为数字类型,并作为造型编号执行。

https://cdn.china-scratch.com/timg/191130/1050244214-0.jpg只是诸多会对数据类型进行自动转换的积木之一,也是在使用时最容易出BUG、最具代表性的积木。几乎所有积木都会对数据类型进行自动转换,有的甚至对输入的数据类型做了限制,如,无法直接在https://cdn.china-scratch.com/timg/191130/1050253951-3.jpg积木中输入字符。

有的积木保是有自己的关键词的,如https://cdn.china-scratch.com/timg/191130/1050244214-0.jpg,可以通过给变量赋值“previous costume”而实现“上一个造型”的效果。还有在《Scratch3.0的十宗罪》中提到的在Sc3.0中访问列表末项、随机项的方法。https://cdn.china-scratch.com/timg/191130/1050251441-5.jpg本身是允许输入数字类型的。但是通过变量赋值、或是在中文输入法下拼写完整单词直接敲入,可以强行输入字符类型的关键词,如“all”、“any”、“last”、“random”,实现直接访问指定项的效果。 if (typeof index !== 'number') {if (index === 'all') {return acceptAll ? Cast.LIST_ALL : Cast.LIST_INVALID;}if (index === 'last') {if (length > 0) {return length;}return Cast.LIST_INVALID;} else if (index === 'random' || index === 'any') {if (length > 0) {return 1 + Math.floor(Math.random() * length);}return Cast.LIST_INVALID;}}

02

转数字类型失败

然而并不是所有积木都“幸运”地拥有关键词的。那些没有关键词的、或者是参数不等于关键词的,Scratch会如何处理呢? static toNumber (value) {// If value is already a number we don't need to coerce it with// Number().if (typeof value === 'number') {// Scratch treats NaN as 0, when needed as a number.// E.g., 0 + NaN -> 0.if (Number.isNaN(value)) {return 0;}return value;无法被转换为数字类型的参数(即NaN),将会被转换为0。有无数多的BUG都是因为这个原因而产生的,比如角色没有移动到预期位置,却总是在舞台中央停滞不前。https://cdn.china-scratch.com/timg/191130/105025C44-6.jpg很有可能就是这两个变量中的参数被转为0了。这个例子更多的会发生在访问列表的特定项时,索引越界,返回空字符,空字符被转换为0,像这样https://cdn.china-scratch.com/timg/191130/1050255253-7.jpg。列表的问题足以新开一篇文章来探讨,在此就不费过多笔墨了。

03

布尔值的规则

其实这个小节的主题才是笔者动笔写这篇文章的主要动机。它来源于一位群友的问题。一个非常经典的问题。

https://cdn.china-scratch.com/timg/191130/1050262402-8.jpg

比较运算符的连续嵌套使用

初学者会这样写脚本的原因是将数学中的习惯带到编程里来了(别问笔者是怎么知道的,问就是笔者也犯过相同的错误。。)乍一看好像没有什么问题。但是要知道,程序是一步一步执行的,像这样嵌套关系的积木,也是先执行上层的,再执行下层的(相当于数学中的括号)。3>2的结果是true(不打引号以区分字符类型),再比较true和1的大小。到这一步,不论接下去的结果如何,都应该能发现,和数学中连续比较的的3>2>1不一样,这么写是错的,是肯定无法得到预期结果的。本着 Scratch 格物堂的“格物”精神,继续往下看。true和1怎么比大小?这还不简单吗?《Scratch 3.0的大小比较是如何进行的》一文中已经说得很清楚了,字符类型的数据在参与比较运算时,比较的是ASCII码或者说是Unicode码。而数字0的十进制编号是48,A是65,a是97。就算Scratch不区分大小写,二十六个字母怎么的也是排在数字后面,字母是肯定大于数字的,“true”>1肯定是true。

https://cdn.china-scratch.com/timg/191130/1050264J8-9.jpg

“true”>1

看似没毛病

但是一运行整个脚本。。。

https://cdn.china-scratch.com/timg/191130/10502C120-10.jpg

于是开始倒推哪个环节出了问题。

用《Debug in Scratch —— Part 1》中提到的“气泡输出法”进行排查:

https://cdn.china-scratch.com/timg/191130/10502C341-11.jpg

曾经引以为傲的“气泡输出法”

https://cdn.china-scratch.com/timg/191130/10502C0P-12.jpg

说“true”

还是没问题

细心的读者可能会发现,上文中的true,是什么时候被打上引号的?true在参与比较运算时,真的是“true”吗?我们想当然地以为true被转换成了字符类型。但是不是字符类型还能是什么?NaN,难道被转成了0吗?0>1的确是false没错,好像说通了。但是换几个用例再次进行测试,发现结果又不太一样。篇幅有限,不再进行引导。直接上测试印证猜测

测试1:

https://cdn.china-scratch.com/timg/191130/1050264127-13.jpg

布尔类型(true)参数参与算术运算(+空字符)

测试2:

https://cdn.china-scratch.com/timg/191130/10502BG6-14.jpg

布尔类型(false)参数参与算术运算(+空字符)

这两个小测试足以证明,

true被转换为数字类型时,会被转换为1;false在被转换为数字类型时,会被转换为0.

在上文中的比较运算符的连续嵌套使用示例中,第二次其实是在比较1>1,结果自然是false了。至于为什么“气泡输出法”会失效,这是因为https://cdn.china-scratch.com/timg/191130/10502LJ3-15.jpg这类积木会将参数转换为字符类型。


04

实例

也许使用布尔值参与比较运算的确实用价值不高,一般在案例实战中不会这么用,仅仅需要了解其原因即可。但是笔者的的确确见过一种使用布尔值参与算术运算,巧妙利用数据类型隐式转换这个特点的案例。下面和读者朋友们分享一下。
一起盗窃案,有4名嫌疑人。这4人只有一名小偷。已知,这4名嫌疑人中,有且仅有1人在说谎。
首先用脚本把4个嫌疑人的供词用逻辑表达式表示出来:

https://cdn.china-scratch.com/timg/191130/10502G127-16.jpg

1号:“不是我。”

https://cdn.china-scratch.com/timg/191130/10502Kc0-17.jpg

2号:“小偷是3号。”

https://cdn.china-scratch.com/timg/191130/10502K5H-18.jpg

3号:“小偷是4号。”

https://cdn.china-scratch.com/timg/191130/10502Ic2-19.jpg

4号:“3号在说谎”


接着根据条件“4个嫌疑人中有且仅有1人在说谎”,可以推导出结论:以上4个逻辑表达式中,有3个是成立的,有1个是不成立的。再结合上文中get的新技能:布尔值在被转换为数字类型时,true会被转换为1,false会被转换为0,不难得出以下结论:

https://cdn.china-scratch.com/timg/191130/10502I1U-20.jpg

4名嫌疑人中,有且仅有1人在说谎


最后要做的就只有一件事了:遍历,从小偷=1开始,逐个验证即可。篇幅有限,具体步骤就没必要再细说了。
案例来源:《Scratch趣味编程进阶》 作者:谢声涛


05

总结

如果用哲学中的矛盾观来分析数据类型的隐式转换这个问题的话,积木本身的种类、本身的参数类型是主要矛盾,居于主导地位,它决定数据将会被转换成什么类型;而参数的数据类型则是次要矛盾,位居从属地位。如果积木本身的参数类型与实际参数类型不匹配,则会优先尝试将其转化为本身的参数类型,同时兼顾地“考虑”一下参数本身的“想法”,看看它是否“愿意”被转换为那种类型,若“不愿意”,则另做打算(比如转成0或者再比如https://cdn.china-scratch.com/timg/191130/1050244214-0.jpg的复杂转化逻辑)。
Scratch数据类型的隐式转换这个问题,笔者秉持辩证的观点来看待。首先这样无疑对新手来说无疑是十分友好的,易上手。否则动不动地给你报个错,如果对Scratch的数据类型不够了解的话,几乎解决不了。那么现有的教学知识体系中又要多一块庞大却又对逻辑思维能力的提升不大且极为枯燥的内容了,这绝对是在“劝退”初学者。另一个优点就是,只要你的想象力足够丰富,就能利用这个特点做出精妙的案例(前提是你要清楚地知道自己在做什么)。

但是隐式转换带来的负面影响也同样地明显。相比强类型的语言,Scratch在数据类型这方面来说显得不够严谨。其次,和其他弱类型语言一样,隐式转换总是被打上“它们的存在将导致错误的发生”的标签。一如上文中的3>2>1。Scratch数据类型隐式转换所造成的BUG数不胜数,细分下去种类繁多。并且就像他的名字一样,十分隐蔽,极难排查,即使像上文中那样运用一般的debug方法,也很有可能会因为再次发生隐式转换而失效。

--end--

声明:本文章由网友投稿作为教育分享用途,如有侵权原作者可通过邮件及时和我们联系删除:freemanzk@qq.com