00:00
当从像javascript或Python这样的脚本语言过渡到系统编程时,你会接触到一个全新的陌生概念,世界站、堆指针、系统调用、局部性并并行等等。众所周知,Javascript生态系统的一个相当大的部分正在转向rust。但同样众所周知的是,很多人在这个过渡中失败了。时不时的,我会在reddit上看到人们寻求帮助、资源、建议或学习rust的指南。当然,最好的指南式书籍。然而,因为这本书是关于rust的,它并没有深入到一些更普遍的与系统编程相关的概念的细节。通过一些努力,仅从这本书就能学会rust。但我认为,当你理解了借用检查器试图拯救你免受的所有陷阱时,学习会更容易。为了澄清,我将制作的这些视频不是rust教程,相反,我们将探索各种低级概念,这将帮助你理解和欣赏语言背后的许多设计决策。
01:06
大家好,我叫乔治,这是core dumped, 这个视频探讨了系统编程语言的一个基本特性,固定大小类型。也许当你从Python或javascript过渡到rus或C时,你首先注意到的是,你不能在不指定类型的情况下声明变量。即使有类型注释,你也会发现在脚本语言中,一个数值变量简单的被定义为一个数字。而在rust或C中,不仅有整数和小数值,而且每种都有不同种类,为什么?用几个词来说,系统编程语言允许你指定你想用多少空间和内存来表示你的数据,让我们看看这意味着什么。正如你所知,在计算机领域,所有信息都表示为比特序列,每个比特可以是0或1。用二进制表示数据的想法是考虑序列中所有可能的比特组合,并将每个组合与特定值关联。
02:08
例如,如果我们想用两位长的序列表示数字有四种可能的组合,所以我们将这些组合与直0 1、2和3关联。但是因为我们用完了组合,我们不能表示值4。为了解决这个问题,我们需要增加序列的长度,这样做现在使序列能够表示多达8个不同的值。如果这还不够,我们只需要继续向序列中添加比特,直到我们有足够的组合来表示我们想要的值。请注意,因为每个比特只能取2个,且只有2个不同的值,所以每次我们向序列中添加一个额外的比特,我们可以表示的值的范围就会翻倍。但这对我们为什么重要呢?嗯,当你运行一个程序时,它的所有变量都位于内存中的某个位置。
03:02
而内存是有限的。所有这些类型的目的是给你超能力明确而精确的告诉编译器你需要多少空间来表示你的程序将使用的信息。为了方便,计算机不直接操作比特,而是操作8比特的组,称为字节。这意味着你可以用比特表示某物的最小数量是8。这也是为什么你通常遇到8位、16位、32位或64位数字,而不是像3位或47位这样的非传统位技术数字。让我们看看变量大小,你可能有重要性的一个小例子。假设在你的程序中,你试图表示一个人的年龄,你决定使用一个64位无符号整数,他在内存中使用8个字节。正如你所看到的,一个64位长的序列足以表示一个年龄,这个年龄在非潮见的情况下会超过100。然而,如果你记得一个字节有8位,这意味着一个单独的字节可以表示多达256个不同的值。
04:07
而因为年龄永远不会达到255的值,你永远不会使用那8个字节中的7个。如果所有那个内存区域都没有被使用,你就是在浪费空间。所以在这种情况下,一个无符号的8位整数就是你需要的一切。在脚本语言中,你不需要关心这些。当你声明一个变量时,是解释器而不是你的责任来决定变量需要多少空间和内存。可能我说的是,几乎总是你会使用比必要的更多的内存。现在,如果我们谈论一个单独的变量,7个未使用的字节可能听起来并不多。毕竟现代计算机有千兆字节的RAM,但是如果你正在处理成千上万个不同的年龄呢?在这种情况下,你会浪费很多空间和内存。
05:01
此外,Rust的类型系统允许你定义一个数字是否可以为负,因为年龄不能为负。通过使用无符号整数,我们告诉编译器拒绝任何试图将可能为负的值分配给年龄变量的代码片段。另一个有用的场景,这种强制的明确性是有用的,是当你操作不同类型的数值时。在javascript中,你可以这样做,仅仅通过看这段代码,你能告诉结果将是一个整数还是一个浮点值吗?仅仅通过看这段代码,你能告诉解释器会在执行操作之前四舍五入浮点数,还是会将整数转换为浮点数,然后执行操作吗?如果你经常使用javascript,你可能知道答案,那很好。不好的是你的答案来自你的经验或隐藏在互联网某个地方的文档,而不是这段代码提供的任何信息。在rust中,你不能这样做。
06:02
为了避免陷阱,你需要明确表达你试图实现什么。这待我们进入这个视频的下一个主题性能。在脚本语言中,一旦解释器为变量分配内存,当这些变量后来需要时,它怎么知道它们是什么?因为记住,在计算机中,一切都是用比特表示的。如果程序后面需要添加这两个值,没有办法知道这些比特是表示一个数字、一个字符串还是其他任何东西。有多种方法可以解决这个问题,但最简单的方法是为每个值附加额外信息。因此,如果这些值后来需要解释器,可以先读取该信息,以确定它是否必须执行数值操作。如果是数字或连接,如果是字符串。一个不错的动态类型系统也可以使用这些信息在运行时捕获错误。
07:03
例如,如果你试图将一个字符串与一个数字相加,它可以抛出一个错误,就像Python那样。一个真正糟糕的语言,尽管如此,不会告诉你任何东西,而是隐式的转换其中一个值,然后执行任何可能的操作。信不信由你。这种无法描述但只能称为未定义行为的狗屎,被认为是一个特性。无论如何,我在这里试图说明的是,这一切都不是神奇的完成的。首先,这些标签需要额外的空间和内存。在这里,由于屏幕空间有限,我将它们表示为,如果它们只占用一个字节,但在现实生活中,他们实际上需要几个字节。现在,你不仅无法控制变量的大小,而且解释器还为每个变量使用额外的内存。第二个问题也许是最重要的问题是,这种方法对简单的算术和其他操作数据的操作施加了巨大的开销。
08:04
这些标签必须被初始化,这需要时间。他们必须被读取,这需要时间,他们必须被比较,这需要时间。此外,他们必须在运行时被写入。正如你可能猜到的,这也需要时间,所以所有这些任务都需要CPU时间。这意味着大多数时候解释器正在执行所有这些额外的步骤,而不是执行你的代码。另一方面,当你被要求在代码中提供有关变量的具体类型和大小的信息时,编译器可以使用这些信息来发出非常高效的机器代码。在这种情况下,比如编译器知道这两个将在运行时存储在内存中的某个地方的变量是32位数字,所以它将发出机器代码来获取这两个值,执行加法,然后将结果存储在内存中的另一个地方。就这样,没有使用额外的内存,没有额外的验证,所有这些都是因为你在编译时提供了这些信息。
09:05
这就是静态类型语言通常比动态类型语言快几个数量级的主要原因之一。即使它们不是系统编程语。好了。现在你知道为什么你的变量大小很重要了。最后,考虑到因为原始值的大小在编译时已知,这意味着更复杂值的大小也在编译时已知。所以你刚刚看到的一切都适用于你的自定义数据类型。除了你并不总是需要固定大小的类型。例如考虑这个场景,在这里,编译器将拒绝这段代码,因为他不知道数组的大小。解决方案是指定大小。然而,这意味着每次你初始化这样的变量时,你必须向数组提供确切的那么多元素。在这种情况下,2个。
10:02
否则编译器会抱怨。但是有些情况下你不知道数组的大小。在编译时。或者简单地是你的意图让它的大小在运行时变化。解决这个问题的方法是使用一个特殊的内存区域,你的数组可以在那里动态增长或缩小。而不是持有数组本身。你的自定义类型持有对该内存区域的引用数组就位于那里。加上其他信息,比如数组的大小,所以我们总是知道该区域有多大。这个限制背后的原因超出了这个视频的范围。从这里开始,事情开始变得更加复杂,因为现在我们必须考虑你可能听说过的这些内存区域。栈和堆,然而这些概念值得他们自己的视频,所以如果你想学习,更多考虑订阅,如果你学到了一些东西,或者只是喜欢看这个视频,请按赞。
我来说两句