记录 | 一个Debug经历&&一个跳出双层循环的方法

首先声明:为了保持严谨的求学态度,各位程序员面对bug时还是尽量搞懂问题的根源,否则后期这个bug可能会造成很大的影响(其实很多情况下也不会再造成什么影响)。

第二条声明:这篇记录只是写了一个解决问题的路子,里面具体技术细节老潘也没太搞懂,也没有必要搞懂,反正快速得到需要的结果就够了。所以有问题不要问老潘,老潘也不懂,请百度或Google。


一个风和日丽的劳动节假期。

某个技术团队需要解决一个小问题,他们想让用Unity3D开发的工程读取串口数据作为物联网领域非常会吹牛逼的潘老师闻之欣然规往。在飚到四轮离地的公交车上,老潘已经在网上找到了C#读取串口数据的现成轮子,可以直接使用,所以这确实是一个小问题。

然而,由于Unity3D的软弱性,C#的一些标准库函数不能使用。在Windows系统上运行毫无问题的代码放进U3D工程中就完全没法用。例如,以下语句大致作用是捕获每次串口数据并触发括号里那个事情,这个事情是判断目前这个命令下该干什么,这条是程序不断读取串口并且根据命令执行不同动作的关键一步:

my_port.DataReceived += newSerialDataReceivedEventHandler(DoSomething)

然而很巧地,U3D不支持这种做法,如果想让程序接到串口命令后,根据命令执行不同的U3D动作,使用这条代码是行不通的。

潘老师看到甲方团队已经写好的程序结构是类似这样的。

void Start() {
    // 写了点什么代码在这里
}
void Update() {
    // 写了点什么代码在这里
}

那好,潘老师猜测这个工程运行后从Start()开始,从网上找到了U3D下能够使用的读串口的代码后,潘老师使用线程启动串口读取的动作,启动的线程放在Start()里面。

my_thread = new Thread(receive_thread);
my_thread.Start();

再创建一个receive_thread()的函数,这里面放一个while循环,while里不断循环重复尝试从串口数据中读出来命令。这样一旦串口数据更新,字符串receive_str也会立刻跟进(但愿)。

void receive_thread() {
    while(true) {
        Stringreceive_str = my_port.ReadLine();
    }
}

还是不行,发现进度会卡在while循环里面,没有任何反应,用log输出一下(C#里的相当于print的办法)也不出任何结果。后来查证了一下才发现:循环里用的**ReadLine()**虽然可以在U3D里面使用,但是这个函数是接收一串字符串,在开始执行接收数据后,只有遇到换行标识才会结束接收并形成一个串。然而很巧地,串口来的数据是从一个开发板来的,这串口数据源源不断根本不给换行标识。于是整个while循环其实并没有在循环,只是卡在了ReadLine()一直在等一个根本等不到的换行符。

好,那就用个原始的办法吧,那就让串口的原生数据给程序,通过程序来解码字符串。好在网上有使用List等复杂方法接受数据并且形成字符串的现成轮子,拿来借用一下。所需的只是每条串口数据的起始标志和结束标志,即可不用换行符也能得到一条完整正确的串口数据。

然而很巧地,潘老师居然找不到什么明显的起始标志和结束标志,这就导致不能清晰的判断出一条数据什么时候开始什么时候结束。

探究了半天,发现大多数命令的起始标志还是一致的,那就以起始标志作为分割语句的标志吧

分割完之后,拿着一条得到的原生数据,做一下转码。好在这些码只是字符串对应的ASCII码值,还是很容易转一下的。

string ascii_to_str(byte[] com_data) {
    return System.Text.Encoding.ASCII.GetString(com_data);
}

再从转好的字符串里提取出关键信息。甲方需要得到串口返回的命令对应ID号,串口传回来的数据类似这样:

Result:ID(5),Text["老潘家的潘老师"]

这里括号中的数字5就是关键信息,使用正则表达式给他拿出来:

Regex rgx = newRegex(@"Result:ID(([\s\S]*?)),Text");
Match match = rgx.Match(indata);  // indata是需要识别的字符串
string result_id = match.Groups[1].Value;

但是预想他只拿出括号里的数字,结果得到的是一个带括号的(5),但是没有时间纠结这个,至少有关键信息可以进行下一步了。这里可能是正则的括号问题,是不是应该转义一下?

在使用这个命令号进行判断的时候,使用了会被人嘲笑的最简陋的if列表:

if (result_id == "(0)") {
    // 0号命令需要执行的代码
}
if (result_id == "(1)") {
    // 1号命令需要执行的代码
}
if (result_id == "(2)") {
    // 2号命令需要执行的代码
}

在if列表里面填具体的U3D动画时,运行报错动画必须放在主线程,就是Update()中。不能放在这个另扯出来的自定义线程里。经过尝试动画需要放在Update()里面

(以下是猜测,如果错误请指出)这时候潘老师突然想到,U3D的机制可能是,在Update()里面进行工程运行的大循环,这个大循环的大概作用机制是不断刷新屏幕来更新屏幕上的像素信息。但凡编写程序涉及到动画,目前见到的解决方法基本都是通过循环刷新来做的。刷新的速度单位是帧,或者fps。刷新不仅可以对屏幕刷新,还可以对键盘鼠标等设备刷新,提高接受信号的灵敏度**(所以玩游戏的还是fps越高越好)**。

这时候就陷入了一个问题,接收串口信息的这个线程本身其实就是一个死循环,如果进入到这个线程就势必没法往下进行,不能往下进行就没办法循环起来刷新动画的这个事情了。

那看来需要改一下,不在Start()里运行读串口的死循环线程,而改为在Update()里,随着整体的刷新来不停执行这个线程,而线程自己不循环,只执行一次。整体上看,还是相当于一直在循环这个线程的功能。而且还可以直接把判断写在Update()里面,当识别到不同的数字,执行不同的动作。再者,线程返回值好像做起来挺麻烦,那干脆就把存数字的字符串弄成public变量,每次循环,这个变量就更新,if列表也接受这个值进行判断。潘老师的智慧闪闪发光。

这么做当然是不可以的了。虽然想法没什么问题,但是U3D在某种玄学力量的作用下,导致这个Update()的循环速度根本不够。串口偶尔会一下子给很多条数据,但Update()循环一次只能读一条数据,就会导致堵车。结果就是,一条命令已经发出了很久,屏幕上的动画才开始播放。

while循环读取串口数据的速度倒是很快,基本和串口发送数据的速度一致,但是因为跳不出来循环导致没法往下进行,也是没办法用的。

一筹莫展,潘老师掉了半斤头发,这时候甲方有人提醒我,给while加一个判断条件,判断到那个括号里的命令号被识别出来之后,跳出快的while循环,进入慢循环播动画,不就搞定了吗。是的,潘老师果然还是头发多太年轻。

最后一步还是没能平稳的走完。虽然进入大循环里的小循环里成功的判断出了命令号到来,也跳出了小循环进行下一轮的大循环,但是动画突然开始鬼畜,不停地很快地播放动画的前一秒的瞬间。

因为用的List存储了串口来的原生数据,小循环识别命令号成功并且结束循环的时候没有把List里的这条数据清空,结果导致下一轮大循环来到,进入小循环后直接这条信息就又被识别出命令号并且结束了小循环,触发了动画。懂了吗?

反正最终,潘老师用了一天,为甲方解决了一个小问题。坐在飞翔的公交车上,潘老师给甲方发了一条消息:

“今天那程序,即使你觉得哪里写的不好,也不要轻易改。不然”


附:一个非常简易的不用goto的跳出双层循环的python示例。如果不加最后三行else,continue,break每次就只能跳出里面那层while。while和for都可以这样用甚至两个循环混着也能用。

i = 0
while True:
    while True:
        i += 1
        print(i)
        if i == 4:
            break
    else:
        continue
    break