如何避免产生赋值语句

我在之前的文章里谈论过赋值语句的危害性。 使用赋值语句会使程序变的冗长,更难理解。但事实上,赋值语句对编程来说是一种基本语句,想限制它的使用几乎是不可能的。幸运的是,我们实际上是能做到的,下面我就会向你展示如何去做。

 

用正确的方式初始化

// 错误       |  // 正确
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() 方法来移动这个坐标点。

然而,从代码本身看,却是没必要的复杂化了,而且有几个主要的错误:

  • 构造函数直接把xy 初始化成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评论

 

:)

 

<think>首先,用户的问题是关于赋值表达式和赋值语句的区别。我需要根据系统级指令和用户可见层的要求来构建回答。 系统级指令: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户可见层: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 参考的引用: - 引用[1]: Python赋值表达式(:=),读作"a walrus b",解决代码重复问题。 - 引用[2]: C语言中赋值运算符(=),将右边表达式的值赋给左边变量。 - 引用[3]: C语句的作用和分类,包括赋值表达式和赋值语句、空语句等。 用户的问题:我想理解赋值表达式和赋值语句的区别 请问赋值表达式与赋值语句的区别 关键点: - 赋值表达式:在Python 3.8引入,使用:=操作符,返回被赋的值,可以用于表达式内部。 - 赋值语句:传统赋值,如a = b,是一条语句,不返回值,不能用于表达式内部。 在C语言中: - 赋值语句:如x = 3;,是一条完整的语句。 - 赋值表达式:赋值操作本身是一个表达式,可以返回一个值,因此可以用于其他表达式中,如y = (x = 3) + 2;。 在Python中: - 赋值语句:a = b,是语句。 - 赋值表达式:a := b,是表达式,返回b的值。 区别总结: 1. **语法和操作符**:赋值语句使用=,赋值表达式使用:=(在支持的语言中)。 2. **返回值**:赋值表达式返回被赋的值,允许在表达式内部使用;赋值语句不返回值,只能作为独立语句。 3. **使用场景**:赋值表达式可以简化代码,减少重复计算;赋值语句用于基本赋值。 4. **语言支持**:在C语言中,赋值操作本身是表达式;在Python中,传统赋值是语句,表达式形式是新增的。 回答结构: 1. **介绍概念**:简要解释赋值表达式和赋值语句。 2. **核心区别**:对比语法、返回值、使用场景。 3. **示例**:提供代码示例,展示不同语言中的使用。 4. **生成相关问题**:根据主题生成相关问题。 使用中文回答,并确保LaTeX格式正确。由于问题涉及编程,数学表达式可能不多,但如果有,要正确使用$...$。 可能的数学表达式:例如,在解释表达式时,如a = b + c,这可以视为行内数学表达式,使用$...$格式。 引用:在回答中自然地添加引用标识。参考引用[1]、[2]、[3],但用户的问题是基于这些引用,所以我需要整合它们。 最终回答: - 开头清晰介绍。 - 逐步解释。 - 代码块(如Python或C)来展示。 - 结尾生成相关问题。</think>### 赋值表达式与赋值语句的区别 在编程中,赋值表达式(assignment expression)和赋值语句(assignment statement)都是用于给变量赋值的机制,但它们在语法、功能和适用场景上有显著差异。以下我将逐步解释它们的核心区别,帮助您清晰理解。我会结合C语言和Python的示例进行说明,因为这些语言在您的引用中被提及(引用[1]、[2]、[3])。 #### 1. **基本概念** - **赋值语句(Assignment Statement)**: 这是一条完整的语句,用于将右侧表达式的值赋给左侧变量。它不返回任何值(在大多数语言中),因此只能作为独立语句使用。语法通常使用`=`操作符。 - 在C语言中:`x = 3;` 是一条赋值语句,将常量3赋给变量`x`(引用[2])。 - 在Python中:`a = b` 也是一条赋值语句,但Python的传统赋值是语句,不返回值(引用[1])。 - **赋值表达式(Assignment Expression)**: 这是一个表达式(而非独立语句),它执行赋值操作并返回被赋的值。因此,它可以嵌入到其他表达式中使用。语法在支持的语言中可能不同,例如Python使用`:=`(海象操作符)。 - 在C语言中:赋值操作本身是表达式,返回被赋的值,例如`y = (x = 3) + 2;` 中,`x = 3` 是一个赋值表达式,返回3(引用[2])。 - 在Python中(3.8+):赋值表达式使用`:=`,如`if (n := len(data)) > 10:`,它返回`len(data)`的值并赋给`n`(引用[1])。 #### 2. **核心区别** 下表总结了主要差异(基于引用[1]、[2]、[3]): | 特性 | 赋值语句 | 赋值表达式 | |--------------------|---------------------------------------|----------------------------------------| | **语法操作符** | 使用`=`(例如`a = b`) | 在Python中使用`:=`(例如`a := b`);在C语言中赋值操作本身就是表达式 | | **返回值** | 不返回值(是语句,只执行赋值) | 返回被赋的值(是表达式,可参与计算) | | **使用场景** | 只能作为独立语句使用(例如在函数体或代码块中) | 可嵌入其他表达式内部(例如在条件判断、循环或数学计算中) | | **代码简化能力** | 较弱,可能导致重复代码(例如需多次计算同一表达式) | 较强,可减少重复计算(例如避免重复调用函数) | | **语言支持** | 几乎所有编程语言都支持(如C、Python、Java) | 部分语言支持:C语言中赋值是表达式;Python 3.8+引入`:=` | 关键点: - **返回值差异**:赋值表达式返回一个值,因此它可以在表达式中作为操作数使用。例如,在C语言中,`$y = (x = 3) + 2$` 等效于 `$x = 3$` 和 `$y = x + 2$`,但更简洁(引用[2])。赋值语句如 `$x = 3;$` 则不能这样使用。 - **表达式 vs 语句**:在编程语言中,表达式(如赋值表达式)会产生一个值,而语句(如赋值语句)只执行动作。这类似于数学中的概念:表达式如 `$a + b$` 可计算值,语句如 `$x = a + b;$` 是命令(引用[3])。 - **优化作用**:赋值表达式能避免重复代码。例如,在Python中,使用 `:=` 可以简化循环或条件判断: ```python # 传统赋值语句(需重复计算) data = [1, 2, 3] if len(data) > 2: print(f"Length is {len(data)}") # len(data) 计算两次 # 使用赋值表达式(避免重复) if (n := len(data)) > 2: print(f"Length is {n}") # 只计算一次 ``` 这里,赋值表达式 `n := len(data)` 返回长度值并赋给 `n`,减少了函数调用次数(引用[1])。 #### 3. **示例对比** 以C语言和Python为例,展示实际代码: - **C语言示例(赋值表达式 vs 赋值语句)**: ```c // 赋值语句:独立使用,不返回值 int x; x = 3; // 语句,仅赋值 // 赋值表达式:可嵌入其他表达式 int y = (x = 3) + 2; // (x = 3) 是表达式,返回3,因此 y = 5 ``` 在C语言中,赋值操作(如 `$x = 3$`)本身是表达式,返回被赋的值(这里是3),这允许它在更复杂的表达式中使用(引用[2])。 - **Python示例(赋值语句 vs 赋值表达式)**: ```python # 赋值语句:只能作为独立语句 a = 10 # 错误示例:if a = 10: # 语法错误,因为赋值语句不能用于条件 # 赋值表达式:可嵌入条件判断 if (b := 10) > 5: # (b := 10) 返回10,并赋给b print(f"b is {b}") # 输出: b is 10 ``` Python的赋值表达式(`:=`)解决了传统赋值语句不能在表达式内部使用的问题,提高了代码简洁性(引用[1])。 #### 4. **何时使用** - **使用赋值语句**:当只需要简单赋值且不需要返回值时,例如初始化变量或更新状态。 - **使用赋值表达式**:当需要在表达式内部赋值并复用值时,例如在循环、条件或数学计算中减少冗余代码。这在性能敏感场景中尤其有用。 #### 总结 赋值表达式和赋值语句的关键区别在于**是否返回值**:表达式返回被赋的值,允许嵌入其他计算;语句只执行赋值,不返回值。在C语言中,赋值操作是表达式;在Python中,赋值语句是默认形式,但3.8+引入了赋值表达式来增强灵活性。理解这一点有助于编写更高效、简洁的代码(引用[1][2][3])。 如果您有特定语言的代码示例需要分析,我可以进一步解释!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值