编程新手常见问题:数据结构使用、减少冗余、设计状态

柏舟   新冠4年 05-05

不会使用结构体

我最开始学编程的时候,只会C和python。当时用C做作业不会用结构体,喜欢将不同含义的数据用数组存,然后用脑子记每个index对应的数据含义。那个时候并不明白为什么需要结构体,后来发现当代码量上来之后,根本记不住。

使用结构体,可以明确代码中隐含的含义,提高代码的可读性。此外,可以借用工具检查正确性,尤其是rust这种严格的语言,要求每个字段显式初始化,新增删减字段都会提示问题,大大提高了代码的维护性。

喜欢从上到下用同一个数据结构

当时用C做作业还喜欢从上到下操作同一个数据结构,并且觉得这样简单,而且减少了内存分配。这种做法的最大问题是当修改了数据时,使数据隐含了状态,导致必须要自己在人脑中推算状态的改变。

建议针对每个任务/函数设计专用的数据结构,尽量将数据结构设计成不可变的。你需要做的事情就是确定输入和输出的流程,然后把流程中每个步骤实现,最后使用胶合层将流程之间不同的数据结构粘起来。由于设计时数据是不可变的,写起来没什么心智负担,很容易。

表面上看,从头到位使用同一个数据结构可以减少拷贝,但实际上,算法和数据结构是强耦合的,很难维护。一旦修改数据结构,所有算法都需要改,而且基本没有并行的可能。

冗余

函数参数和数据结构冗余

每个函数只输入需要的参数,不要输入冗余的参数。然而,函数的输入中存在结构体,很容易出现冗余。当冗余发生的时候,就会降低代码的可复用程度。当其他模块想要使用函数时,就需要费劲地满足里面不需要的字段,为了满足这些字段,可能使用了默认值。当时不会发生问题,一旦代码结构发生改变,比如函数又重新使用了不需要的字段,就根本无法检查是否出现了问题。

一个比较好的实践是使用接口作为参数,结构体可以通过接口实现多态,从而实现最小的输入。比如:

interface IRadius{
  double Radius{get;}
}

class Ring:IRadius{
  double Radius{get;set;}
  double Radius0{get;set;}
}

class Circle:IRadius{
  double Radius{get;set;}
}

那么你不需要对class做装箱拆箱,就可以实现最小的输入。

所以,尽量使用最小的数据结构,字段之间一定是无关,最好是正交的,使用冗余的结构就是给自己挖坑。但是,一个函数可以有很多输入,哪些应该组装成结构体,哪些应该散装,这需要根据系统架构进行设计,把耦合的放一起,松耦合的散装。

代码冗余

不用的代码就应该删掉,不要因为自己辛辛苦苦敲了半天白给了就舍不得删。首先,代码存在就一定需要维护,更新的时候就会纠结遗留不用的代码需不需要维护。其次,一旦项目过了一段时间,有的代码就看不懂了,那时候就更不敢删了。

不想删的一个重要原因是不知道应不应该删,新手需要学习版本管理——git工具。不确定的时候,就开一个分支,写着写着就会发现遗留的代码没用了,这时只需要把旧分支删了。

状态问题

大部分的状态问题都可以写成

\[ x_{n+1}=f(x_n,u) \]

其中,x是状态,u是输入参数。

状态可以是动力系统的运动情况和质量等物理量,也可以是数据库、队列、调度器或者外设设备。状态问题大部分是资源问题,由于资源是有限的、独占的,所以注定存在状态。状态和无状态代码是不同的,无状态的代码可以水平无限扩展,而有状态的代码在存在多个消费者的情况下一定会存在竞争问题。

可以通过面向对象设计将状态和状态改变的相关方法放在一起,然后通过外部f函数调用状态相关方法,总之,f的实现是与状态改变无关的。这样就可以实现状态分离,提高维护性的同时将系统的瓶颈留在了状态上。

约定

初始化

有的字段是之后才初始化,容易在过程中误用,你必须小心判断是不是null,所以尽量一次将结构体初始化。

此外,new方法对应的初始化不要抛出异常,因为在C++中new方法在分配内存之后,抛出异常很容易出现内存泄漏。所以,对于C#,java的new方法不要抛出异常,虽然没有内存泄漏问题,但是还是存在资源的handle需要close。new方法一般默认输入参数是正确的,需要检查参数的情况下,将构造函数设置为private,参数检查等等放到静态方法里面。

特殊值

我初学编程的时候喜欢用特殊值表示特殊含义。比如int大于零是正确值,小于零是错误码。如果不是嵌入式环境或者高性能要求请不要这么做。最好使用enum定义,其次是定义特殊的type,至少一眼能看出这个字段和其他字段type不匹配,定义域可能不同。

总结

上面的内容都是编程的技巧,没有涉及系统设计的问题。大体的思想是尽量复用算法和数据结构,将复杂度转移到编程语言中,让工具帮忙检查错误,而不是用人脑维护。复制粘贴代码很容易,但是类似流程修改是就很难。

资源相关的问题非常复杂,编程语言内部的队列、堆栈、锁和外设都有不同的处理方式,系统初始化等全局变量、配置文件都可以算状态问题,如果有人看的话可以详细说说这个话题。

这篇文章主要针对新手,默认新手已经入门了编程语言。如果有人感兴趣的话,我接下来可以写写编程环境、包管理器的内容,为什么C++的CMake让我感到恶心。