我在之前的文章里谈论过赋值语句的危害性。 使用赋值语句会使程序变的冗长,更难理解。但事实上,赋值语句对编程来说是一种基本语句,想限制它的使用几乎是不可能的。幸运的是,我们实际上是能做到的,下面我就会向你展示如何去做。
用正确的方式初始化
// 错误 | // 正确
int x; |
// ... | // ...
x = init(); | int x = init();
“正确”方式的主要优点是你能很方便的浏览x
的定义的同时知道它的值。这样也能保证x
始终处在一个固定变量状态,大多数的编译器都能检测到这种状态。其次,这样可以使代码减少冗余。
“错误”方式之所以存在完全是因为很多老式的编程语言都强制要求在程序的开始处先声明变量。这样编译器好处理。但现在这已经不是问题了,即使在C语言里。
构造新数据
// 错误 | // 正确
int x = init(); | int x = init();
// ... | // ...
x = something(); | int y = something();
这样做很重要。它能保证变量被定义后不会被改变。不留任何机会。x
的值我们可以保证它是通过init()
初始化的值。
人们使用“错误”方式一般有两个原因:高效和简洁。效率并不是个问题,现代编译器能够通过给变量重新分配地址来优化性能。而由于简洁而导致的语义模糊是得不偿失的。
用函数,不要用过程
// 错误 | // 正确
void to_utf8(string s); | string to_utf8(string s);
|
// ... | // ...
|
string s1 = latin(); | use_string(to_utf8(latin()))
to_utf8(s1); |
use_string(s1); |
“正确”方式使用的是一个普通的数字函数:它接受输入值,返回计算后的值。另一边,“错误”方式使用了过程
。跟函数不一样,过程不仅会影响返回的结果,还能影响其它数据,例如,过程中可以修改它的参数值。这会使这些参数很容易被污染。
所以,当能够使用函数的时候,尽量不要使用过程。你的程序这样会变得更简单。这种技巧可以让你避免去思考如何去做
(变换一个字符串),而是如何被做
(一个变换了的字符串)。要着眼于最终结果,而不是处理过程。
“错误”方式之所以存在完全是由于许多老的编程语言很难处理复杂的返回值。你只能返回单个数字。所以,当需要一个内容更丰富的返回值时,你只能在过
程中达到这个目的。而真正的返回值通常是一些简单的错误标号代码。然而现在不同了,返回复杂的结果已经不再是个问题。即使是在C语言里你也可以返回复杂的
结果。
固化你的对象
在很多的入门级的介绍面向对象编程的课程中,你能看到这样一个著名的二维坐标的例子:
// 非常非常错误
class Point
{
public:
// constructor
Point() { x = 0; y = 0; }
float get_x() { return x; }
float get_y() { return y; }
void set_x(float new_x) { x = new_x; }
void set_y(float new_y) { y = new_y; }
move(Point p) {
x = x + p.x;
y = y + p.y;
}
private:
float x; float y;
};
这样设计的原因很简单:你可以通过构造函数创建一个新的坐标,然后通过set_x()
和 set_y()
进行初始化。内部数据是经过封装的(private
),只能通过get_x()
和 get_y()
来访问。还有个好处是,你可以通过move()
方法来移动这个坐标点。
然而,从代码本身看,却是没必要的复杂化了,而且有几个主要的错误:
- 构造函数直接把
x
和 y
初始化成0了。如果你希望它是其它值,你还需要手工的设置。你不能初始时做到这些
。
- 操作一个点的缺省方法就是修改它。这是一种赋值操作。你被限制了创建一个新值
。
-
set_x()
, set_y()
, 和 move()
方法现场修改这个对象。这些是过程
, 不是函数
。
-
x
(和 y
) 是私有的,但你可以通过get_x()
和 set_x()
操作它们。所以,你认为你是封装了它们,而实际上没有。
-
move()
这个方法不需要放在Point
类里。放在类里使类的体积变大,影响理解和使用。
正确的设计更简单,而且不失功能:
// 正确的
class Point
{
public:
// constructor
Point (float x, float y) {
_x = x; _y = y;
}
x() { return x; }
y() { return y; }
private:
float _x; float _y
}
Point move(Point p1, Point p2) {
return Point(p1.x() + p2.x(),
p1.y() + p2.y());
}
另外,如果你愿意,你可以把_x
和 _y
声明成public和常量。
使用纯功能性数据结构
从上面的介绍里我们说明了应该构建新数据
。这个建议即使是大数据结构也是有效的。意外吗,它并不是像你想象的那样失去作用。有时候你为了避免每次都拷贝整个数据结构,你可能要使用修改操作。而数组和hash table就是属于这种情况的。
这种情况下你应该是使用我们所谓的纯功能性数据结构。如果你想对这有所了解,Chris Okasaki’s thesis (也是同名著作)是个好的教材。这里,我只给大家简单的讲讲linked list。
一个链接表要么是个空表,要么是其中有个单元格存着一个指向另一个表的指针。
┌───┬───┐ ┌───┬───┐
│ x │ ───> │ y │ ───> empty
└───┴───┘ └───┴───┘
这样的数据结构如果在ML语言里是很好设计出来的,但在以类为基础的语言里会稍微有点复杂:
-- Haskell
-- A list is either the Empty list,
-- or it contains an Int and a List
data List = Empty
| NotEmpty Int List
-- utility functions
is_empty Empty = true
is_empty NotEmpty x xs = false
head Empty = error
head NotEmpty x xs = x
tail Empty = error
tail notEmpty x xs = xs
// Java(ish)
class List
{
public:
// constructors
List() { _is_empty = true; }
List(int i, List next) {
_i = i;
_next = next;
_is_empty = false;
}
bool is_empty() { return _is_empty; }
int head() {
if (_is_empty) error();
return _i;
}
List tail() {
if (_is_empty) error();
return _next;
}
private:
int _i;
List _next;
bool _is_empty;
}
你可以看到,现在这个List类是不可变的。我们不能修改List
对象。我们只能在现有的对象外新建新的List。 这很容易
。因为当你构建一个新List时,它会共享现有的大多数的单元。假设我们有个list l
,和一个整数i
:
┌───┬───┐ ┌───┬───┐
l = │ x │ ───> │ y │ ───> empty
└───┴───┘ └───┴───┘
i = 42
此时,在l的顶部加入i
,这样就会产生一个新的list l2
:
┌───┬───┐
l2 = │ i │ │
└───┴─│─┘
│
│ ┌───┬───┐ ┌───┬───┐
l = └──>│ x │ ───> │ y │ ───> empty
└───┴───┘ └───┴───┘
或者,在代码里:
List l = List(x, List(y, List()));
int i = 42;
List l2 = List(i, l); // cheap
l
仍然存在,不可变,而新建的l2
只是多了一个新建的单元。类似的,删除顶部的元素也是不费任何资源的容易。
当我们不能这样做时
有时,你不能避免赋值操作,或者受其它因素限制。也许是你需要更高的效率,你必须修改数据状态来优化程序。或者由于一些外界因素影响,比如一个用户。或者由于你使用的语言不能自动处理内存使用,这些都会阻止你使用纯功能性的数据结构
。
这种情况下你所能做的最好的方式是隔离那些程序中不合规范的代码(那些使用赋值语句的代码)。比如说,你想给一个数组排序,你必须用quicksort。Quicksort严重的依赖于变换转移操作,但是你可以隐藏这些操作:
array pure_sort (array a)
{
array a2 = copy(a);
quicksort(a2); // modify a2, nothing else
return a2;
}
于是,当pure_sort()
这个内部函数不能按照我的建议的去写时,影响并不大,因为它被限制在函数内了。最终,pure_sort()的
行为就像是个普通的函数
了。
相反的,当你与其它业务有交互时,要小心的将交互部分的代码和运算部分的代码分隔开。比如你要写段在屏幕上画个点的程序,而且能根据鼠标的移动而移动。写出来可能会是这样:
// 错误
Point p(0, 0);
wile(true) // loop forever
{
p = move(p, get_mouse_movement());
if (p.x() < 0 ) p = Point(0 , p.y());
if (p.x() > 1024) p = Point(1024 , p.y());
if (p.y() < 0 ) p = Point(p.x(), 0 );
if (p.y() > 768 ) p = Point(p.x(), 768 );
draw(p);
}
这里有个错误,它在主程序里对越界坐标进行了检查。更好的方式是这样:
// 正确
point smart_move(point p, point mouse_movement)
{
float x = p.x() < 0 ? 0
: p.x() > 1024 ? 1024
: p.x();
float y = p.y() < 0 ? 0
: p.y() > 768 ? 768
: p.y();
return Point(x, y);
}
// 主程序
Point p(0, 0);
wile(true) // loop forever
{
p = smart_move(p, get_mouse_movement());
draw(p);
}
现在,主程序变得更简单了。运算部分,smart_move()
,可以进行单独测试,甚至可以在其它地方重用。 现在,如果你不喜欢这样的三元操作的语法,不想按我介绍的规则,不去构造新数据
:
// 这样也不是很差
point smart_move(point p, point mouse_movement)
{
float x = p.x();
float y = p.y();
if (x < 0 ) x = 0;
if (x > 1024) x = 1024;
if (y < 0 ) y = 0;
if (y > 768 ) y = 768;
return Point(x, y);
}
不管你怎么写,smart_move()
始终应该是个函数
。
结论
我说的这些都是关于降低耦合的技巧。每个程序都应该有很清晰的内部边界。每个模块应暴露最少量的接口。这能使程序更易于理解和使用。避免使用赋值语句,坚持对象恒定的原则能使接口清晰明确。但这也不是银弹,这只是辅助手段。很有用的辅助手段。
译文来源:外刊IT评论
:)
分享到:
相关推荐
《编译原理》课程设计——递归下降翻译赋值语句至逆波兰式 递归下降翻译程序设计是一种基于解析技术的方法,它主要用于解析上下文无关文法的语法结构。在编程语言的编译器或解释器中,这种方法被广泛用于语法分析...
阻塞赋值时先计算等号右手方向(RHS)的值,这时赋值语句不允许任何别的语句的干扰,直到现行的赋值完成时刻,即把 RHS 赋值给 LHS 的时刻,它才允许别的赋值语句的执行。 非阻塞赋值的操作符是“”,例如 b ;,块...
在Verilog HDL中,行为建模主要通过两种过程赋值语句实现:`initial`和`always`。这两种语句在建模过程中扮演着重要的角色。 1. **initial** 语句: `initial`语句只执行一次,通常在设计的模拟开始时(时间0时刻...
它们能描述组合逻辑和时序逻辑,包括赋值语句、if语句、case语句、loop语句、next语句、exit语句、子程序、return语句、wait语句和null语句。 在VHDL中,对象主要分为两种类型:变量和信号。对象的赋值有两种形式:...
如果你的程序需要同时处理多个键盘事件,可能需要引入多线程,以避免阻塞主线程,提供更流畅的用户体验。 9. **键盘快捷键**: 除了响应单个键的输入,还可以定义组合键(如Ctrl+X,Alt+F4等)作为快捷键,这需要...
本主题主要聚焦于“WHILE”类型的循环语句,它遵循一种称为“WHILE E DO S1”的结构,其中E是一个布尔表达式,S1则是一段赋值语句。 语法分析是编译器或解释器的第一步,它将源代码转换成抽象语法树(AST)。对于...
**表达式语句**是最简单的语句类型,包括赋值语句,如`s = “Hello “ + name`和乘法赋值语句`i *= 3`。递增运算符`++`和递减运算符`--`也属于表达式语句,它们会改变变量的值。`delete`运算符用于删除对象的属性,...
因此,推荐使用成员初始化器而非赋值语句,这一做法可以显著简化构造函数的编写,同时确保成员变量在构造过程中得到正确且一致的初始化。 成员初始化器是在变量声明的时候就对成员变量进行初始化的语法结构。这不仅...
在C#编程中,创建一个包含10个元素的一维整型(int)数组并在声明时直接赋值是一项基本操作。接下来我们将深入探讨如何实现这一任务,以及如何使用循环语句和随机数生成来选取数组中的5个不重复的元素。 首先,让我们...
变量x取值1: 赋值语句1; 变量x取值2: 赋值语句2; ... 变量x取值n: 赋值语句n; default: 赋值语句m; endcase ``` - 不需要判断顺序,所有分支与条件判断优先级相同。 - 在状态机设计中常用。 - `default`...
1. **歧义问题:** `first`集可以帮助消除文法中的左递归和右递归,没有它可能导致无法正确识别语法结构,从而产生解析歧义。 2. **错误检测:** 在没有`first`集的情况下,解析器可能无法有效地检测和报告错误,...
4. **使用阻塞赋值**:在某些情况下,使用阻塞赋值(`=`)而非非阻塞赋值(`)可以帮助避免锁存器的产生。 ```verilog always @(posedge clk) begin if (enable) out = data_in; end ``` 5. **使用`initial`块...
- **锁存器(Latch)问题**:使用`if-else`或`case`等结构时需要注意避免产生锁存器,因为锁存器可能会导致不可预测的行为。 ##### 3. **Verilog HDL语法细节** - **变量声明**:Verilog支持多种变量类型,如`...
对于更复杂的数据结构,如嵌套的列表或字典,深拷贝能确保赋值操作不产生意外的副作用。 在标签"改变赋值位置"的上下文中,我们还可以探讨变量作用域、闭包以及对象属性的赋值等主题。变量的作用域决定了变量在何处...
为了避免这些问题,设计师应该遵循良好的编程习惯,如始终提供else分支,完全列举case语句的条件,并避免在组合逻辑中让输出直接赋值给自己。同时,使用明确的寄存器声明和时钟控制的always块可以有效地防止Latch的...
在测试文件"test_delay"中,我们可能会看到一个设计实例,其中包含一个或多个非阻塞赋值语句,用于演示这一特性。例如,一个简单的计数器可能会使用非阻塞赋值来递增其计数值: ```verilog module counter ( input...
- **消除左递归**:避免在解析过程中无限递归。 - **提取左因子**:优化文法结构,提高分析效率。 - **LL(1)文法**:确保文法满足自左向右扫描且最多只能向前看一个符号的条件。 **实验设计思想及算法** 1. **...
本程序的输入有三类:指令、赋值语句和计算式 这三类输入均允许在基本元素之间添加任意数目的空格 当出现错误时,本程序会显示Error并给出报错原因 ·指令 指令包含以下三个 exit 退出程序 printvar 输出...
在这个例子中,开发者试图交换两个整数`a`和`b`的值,但错误地在条件判断`if(a>b)`后面只放置了一个赋值语句`a=b;`。由于`if`条件不满足,`a>b`为假,`if`语句体内的`t=a;`并未执行。然而,后续的`a=b;`被执行了,...
4. 属性赋值语句:如`box.property = 100;`,给对象`box`的`property`属性赋值。 5. 方法调用语句:如`box.method();`,调用对象`box`上的`method`方法。 接下来,我们详细讨论各种流程控制语句: **1. 分支语句*...