在单线程环境中由可变数据类型引起的典型 class 问题是什么?

What is a typical class of issues that is caused by mutable data types in a single-threaded environment?

JS 通过事件循环对并发进行建模。因此没有竞争条件。那么在程序的主要范围内,以下类型安全操作的缺点是什么,可以证明任何警告是正确的:

const m = new Map([["foo", true]]);

//...

m.set("bar", false);

即使我清空 m 这也不应该引起任何问题,因为每个依赖于 m 的操作无论如何都应该考虑空的情况。

也许有人可以说明可变数据类型带来的典型 class 问题。

我知道这个问题可能过于基于个人意见,所以如果您认为它不合适,请随时关闭它。

提前致谢!

[...] since every operation that depends on m should consider the empty case anyway

给事物命名很困难。假设事情 可能 是危险的。

我认为自己是一个务实的开发人员,有时必须发生变化。然而,您的工作是了解可能出现的问题并教育您周围的人了解危险,以便他们做出明智的决定。

我曾经在一次代码审查中指出这个函数正在改变它的参数:(它大致看起来像这样)

function foo(bar) {
  const baz = {...bar};
  baz.a.b.c = 10;
  return baz;
}

函数的作者回复了"Nope, I've cloned it before. So the function is 'pure'"。

如果我没有花时间与那个人坐下来,我们可能会遇到重大的生产问题。事实证明,他们正在改变应用程序状态,结果我们进行了几次误报测试。

这对我来说是变异数据时可能发生的最糟糕的情况:混乱。

突变导致的错误很难追溯。

我总是鼓励我的团队中的人不要为 "impossible case" 编写代码,因为这通常会导致代码中混杂着 "just in case" 检查,这会增加维护工作量并削弱我们对代码的信心。

但是,如果您允许 不受控制 访问您的数据,"impossible case" 就在附近等待。

我已经证明人们会在不知不觉中改变事物。当你的团队中有不同经验水平的人时,你能做的最好的事情是:

  1. 永远不要假设任何事情
  2. 教他们
  3. 使用库来加强不变性

可能不是您所期望的 "academic" 答案,但我想我可以分享一些技巧。

JS models concurrency by an event loop. As a result there are no race conditions.

这并不完全详尽,您还可以通过 运行 您的程序跨多个 child processes, in which case having multiple threads able to mutate the same memory reference could indeed lead to race conditions or deadlocks. And yes, Immutability is one of the design patterns adopted to guarantee thread-safety 在 java 脚本中获得并发性:[基本上] 强制共享数据为只读.

这很好 article 解释了为什么以及如何在多线程环境中遇到竞争条件,例如 java。


你是对的,在单线程语言中改变内存引用并没有错,实际上这就是在 javascript 中很长时间以来所做的事情。不变性最近才获得发展势头。 Hillel Wayne 还解释了如何完全消除并发性有助于消除可变性造成的痛苦。

但我宁愿从不同的角度来解决这个问题:可变性代表了一个体系结构问题,它遍及所有编程语言或环境,无论是多语言还是单语言-线程并不重要。

通过考虑架构,很容易意识到可变性如何导致不可预测的软件。有什么能保证在给定某些条件下该对象将处于确定状态吗?并不真地。有多少实体可以导致给定对象的状态发生变化?这些变化能否得到控制?不是真的,想想主作用域中的变量......实际上任何东西都可能影响它的状态和假设,比如“每个操作都应该考虑 [...]”是不安全的并且非常错误俯卧

因此,虽然可变性不一定是错误的,但不变性是开发人员工具包中的另一个工具,掌握它可以让您成为更好的开发人员。

不可变数据结构(不可更改的对象)只支持一种操作:读取,这使您的程序表现得像摩尔机:给定一个特定的时刻,您的程序将始终处于一个状态它的可能状态.

您的程序是一个始终可以计算和测量的操作管道:

R.pipe(
  R.toUpper, // You know the state of your program here
  R.concat(R.__, '!!'), // or here
)('Hello World'); // or here

您还可以将其中一个阶段与其返回值交换,并且仍然让您的程序按预期运行:

R.pipe(
  R.always('HELLO WORLD'),
  R.concat(R.__, '!!'), 
)('Hello World');

不变性还可以实现时间旅行并使测试变得非常容易,但真正重要的是 它可以很容易地推断出状态及其转换,因为您将每个值都视为这是一个原始的:user.set('name', 'Giuseppe') 变得与 'Giuseppe'.toUpperCase().

没有什么不同

您的程序最终是随时间推移确定的一系列快照:

-> 'Hello World' -> 'HELLO WORLD' -> 'HELLO WORLD!!'
t(0) ----------- t(1) ----------- t(2) ------------- t(n)

注意:虽然您有更多的中间值,但不变性还可以提高性能,因为它使深度相等变得毫无意义。

const user = { name: 'Giuseppe' };
const equals = (given, expected) => given === expected;

const newUser = { ...user, name: 'Marco' };

console.log('are users equals:', equals(user, newUser));

您需要一个 deepEqual 函数来获得具有可变性的相同结果...(在 redux website 上阅读更多内容)

JS models concurrency by an event loop. As a result there are no race conditions.

让我们就此打住。您可能不会得到两个不同的 线程 尝试同时访问同一内存位置,但您仍然可以让程序的并发部分异步访问可变状态并忽略它们的事实并不孤单。这仍然是竞争条件。

一个简单的例子:

var clock = out.value = 0;

async function incrementSlowly() {
  if (clock == 12)
    clock = 0; // reset
  await delay(1000);
  clock++;
  out.value = clock;
}
function delay(t) { return new Promise(resolve => setTimeout(resolve, t)); }
<output id="out"></output>
<button onclick="incrementSlowly()">Tick!</button>

clock 值永远不会大于 12?自己尝试一下快速按下按钮会发生什么。

incrementSlowly 函数的多次调用是 运行 独立的,并且在错误的时间进行检查 - 在它们的延迟期间,另一个实例可能已经递增了 clock再次.

在这个例子中,我使用了可变变量,但使用可变数据结构也是一样的。当有多个代理通过不同方法访问结构时,它并不总是那么明显。

使用不可变数据结构迫使您显式地进行有状态操作,并且很明显 incrementSlowly 实际上访问了两次状态。