在Java世界工作了10年之后,我最近更认真地看待Rust。我几乎没有开始Rust编程(即,我是一个完整的新手),但我感到很热情:我相信Rust值得关注,因为它是语言平衡的转变。
品前看菜:
Rust可以在嵌入式芯片,Web应用程序和分布式系统中运行。它结合了C的速度,这是一种现代,强大的系统,可以安全地管理记忆和高质量的生态系统。它使编写并发代码变得简单而安全。Rust社区热情,善良,非常活跃并且构建了出色的软件。Rust已经在生产中使用,它工作正常,但有些部分仍在完善中。我要加入乐趣,不让你知道这是错的:-)
从Java到Rust
Java和Rust是非常不同的语言,据说可以针对不同的空间。我被问到为什么Java开发人员 - 一个无聊的开发人员 - 会对Rust感兴趣。事实证明,我选择Java的理由与现在引导我走向Rust的理由相同。让我描述指导我选择语言和技术的原则:* 咚咚咚咚 * -----信息学的三个定律!
信息学的三大定律
有许多规则或法律试图将软件的制作或制作方式正式化。一些人从Isaac Asimov的机器人三法则中汲取灵感。Asimov的定律很有趣,因为它们是一个有点令人无法抗拒的Sci-Fi参考,但它们也很好,因为它们很小并且优先级清晰。
我将它们称为信息学三定律。这些定律在其内容中可能不是非常原始 - 而不是真正的定律 - 但是像Asimov的定律一样,它们试图找到最基本的公理,从中可以推导出理想的性质。它们是尽力而为的法则,因为完美是我们无法掌握的。
开始了:
1. 程序必须正确。
2. 程序必须是可维护的,除非它与第一法相冲突。
3. 计划必须有效,除非它与第一或第二法相冲突。
第一部规定意味着计划在所有情况下都应该按预期行事。它意味着软件应该尝试避开所以的错误,安全问题,永远不应该崩溃。
第二定律意味着程序应该精心设计和记录 -因为它需要对代码有很好的理解 - 并且模块化,因为大的整体块是不可维护的。还应该有帮助开发人员维护程序的工具。部分演进不应该保证完全重写,而另一个开发人员应该能够进行必要的更改,因为设计清晰,并且因为语言可以掌握。
第三定律意味着计划应该倾向于最有可能的投入的速度和资源消耗的最佳平衡 - 也就是说,根据它们的使用方式。这意味着我们应该使用最好的数据结构来完成工作,以实现最佳权衡 - 因为大多数时候没有全局优化 - 适当地规划规模并使代码合理的运行它的计算机。
这三项定律共同创造了一个充满活力的生态系统和社区的要求。开发人员无法单独编写正确,可维护和高效的程序,因为现代程序依赖的代码多于单个开发人员可以生成的代码。他们必须重用社区提供的组件。他们必须分享社区健康的信念 - 无论是增长还是大到足以维持现有组件并建立新组件。这使得围绕语言的社区成为选择编程环境的最重要因素。
三个定律和秩序
毫无疑问,正确性是我们在软件中的主要关注点。如果一个软件无法正常工作,那就不值得运行了。如果它不安全,则存在安全风险。这显然使我们的第一定律成为正确。
但是,有人可能会争论可维护性是否应该比效率更重要,反之亦然。当完全忽略可维护性时,程序可能有可能实现更好的性能,并且仅考虑正确性和可维护性的编程可能导致程序完全低效。
实际上,可维护性和效率是相互交织的:良好的架构是围绕有效的解决方案构建的。例如,XI Editor 旨在成为“未来20年”的高性能编辑器,它是围绕Rope数据结构构建的。专门自上而下设计系统可能会妨碍实现有效的解决方案; 自下而上地设计它们可能会泄漏太多的实现细节,并且会阻止干净,松散耦合的架构。
尽管如此,如果一个软件是正确的并且效率极高但无法维护,那么它只不过是一个黑盒子。如果我们无法理解,我们是否相信它是正确的?反过来说,一个低效但正确+可维护的程序可能还是可以挽救的。可维护性成为第二定律。
定律和语言
信息学的三个定律仅涉及程序。然而,编程语言可以提供保证,构造,工具,执行环境,社区或生态系统,帮助开发人员制定守法程序。让我们来看看一些球员。
系统语言
C和C ++共享一些属性。他们非常适合编写快速,低占用空间的代码。手动内存管理将安全性留给了开发人员(他们几乎无法记住他们离开手机的位置!)。C就像买个玩具不给电池一样还需要自给自足,因此您经常最终重写数据结构; C ++是一个怪物,每个项目都定义了他们使用或不使用的语言部分(谷歌因不使用例外而闻名)。因为它们都不安全,取决于另一个库会增加安全漏洞的风险。
Go 填补了C ++里能把开发者吓跑的 Void类型。它旨在快速并使用例程轻松编写并发代码。它是有笔C ++更安全垃圾收集。它具有简单的类型系统,缺乏泛型和代数数据类型,不支持现代语言代码中的重用功能。尽管如此,它仍然拥有一个非常活跃的社区,可能被Google的光环所吸引,并追随着Docker的脚步。
JVM生态系统
Java是一种相当简单的常规语言。在Java中有一个明确的重用历史,在整合第三方工件方面可能是冠军 - 有时候大型项目看起来像弗兰肯斯坦的怪物。Java具有语言和虚拟机支持的出色工具。现代JVM可以“及时”编译代码,并将其转换为相当高效的本机代码。有不同的垃圾收集器提供不同的权衡。哦,我提到Java社区是巨大的吗?
Kotlin试图通过减少冗长和提供更强大的,安全的类型系统来提供Java“语言”的替代方案。它主要针对JVM,并且具有与Java相同的优点(还有Kotlin / Native)。由JetBrains创建,工具显然非常出色。现在官方支持Android开发,它就在这里。
功能编程 - 或FP - 语言
Haskell和OCaml作为研究项目开始了他们的生命,但近年来在该行业中越来越受欢迎。它们是安全的,提供了很好的设计原语(特别是类型类和模块)和编程模型,根据我的经验,它可以减少错误。它们都是垃圾收集的 - GC是为第一种FP语言LISP发明的。特别是Haskell是完全纯粹的,它提供了很多好处:所有效果 - 例如IO - 都明确表示为类型,这使开发人员不必担心意外发生的副作用,但可能会变得很麻烦。他们的社区都包括许多研究人员,帮助建立坚实,正式的基础。
还有更多的语言
我不打算在那里讨论所有其他语言。有些人在其生态系统和社区中具有非常明显的优势 Python拥有出色的数据分析生态系统,Erlang帮助构建使用actor的容错分布式系统,Scala是Kotlin的老兄,更狂野的兄弟,Clojure和Racket是现代Lisps,TypeScript试图理解JavaScript!
第三定律的觉醒
那里确实有许多有趣的语言。他们中的大多数都有自己的优势和好主意。他们帮助愿意的开发人员遵循信息学三法则多少?
可维护性主要通过良好的设计原语( 即语言结构),良好的工具和社区来解决。有不同的流派对什么构成一组优秀的原语有不同的看法:我个人赞成用现代的强类型FP语言选择的那些。
抛开可维护性,有两组主流语言:具有手动内存管理和垃圾收集的语言。由于我们的开发人员不完善,手动内存管理也意味着不安全,因此缺乏正确性。垃圾收集虽然产生了管理费用,但在过去25年中一直是新主流语言的事实上的标准,因为安全比绝对性能更重要。
Rust是第一种提出替代方案的流行语言 - 自动内存管理和无垃圾收集的安全性 - 它带有强大的FP启发设计原语来构建高级抽象等等。
如果我们现在可以安全地构建更高效的软件,我们不应该吗?或者我们应该优化开发生产力?我们可以两个都有吗?
一个新的挑战者
Rust是一种系统编程语言,运行速度极快,可以防止段错误并保证线程安全。(rust-lang.org)
Rust现在已经存在了一段时间,但直到2015年Rust 1.0发布时才实验。从那时起,这个语言及其生态系统不断发展壮大。虽然多年来它仍将改进,但核心团队承诺不会破坏用户代码。
与Go一样,Rust旨在为对系统编程感兴趣的开发人员提供一个严肃的选择。然而,它从语言研究和现有编程语言的成功经验中借鉴(双关语)。
Rust有一个现代类型系统,可以自动管理内存,并且 - 与轻量级运行时检查一起 - 确保安全访问它。它具有泛型和接近Haskell类型类的特征系统。这使得Rust能够称之为零成本抽象:拥有出色的设计不应该损害性能。James Munns 关于嵌入式编程的精彩演讲描述了Rust嵌入式工作组如何构建一个可重用的组件,以便在不牺牲执行成本的情况下在微小芯片上抽象各种类型的硬件操作。
当我深入研究Rust资源时,从伟大的Rust Book开始,并为我可能感兴趣的项目探索GitHub,我注意到这种不妥协的方法对社区和生态系统的影响有多大。Rust开发人员的目标是实现最佳的运行时效率,最准确的抽象和最强的执行安全性。它使得Rust非常适合低级编程,但对于更高级别的应用程序也非常有趣:虽然人们可以在Rust中编写Linux内核模块,但它也可用于构建REST Web应用程序,区块链节点甚至单页Web WebAssembly中的应用程序!让我们更深入一点,看看它有助于写作守法程序。
正确性
编写正确的程序很难。我们试图完成的任务往往含糊不清。尽管如此,语言可以通过足够的表达来帮助,并且不需要程序员艰难的制定他们的问题。Rust拥有全套的现代语言结构:代数数据类型(此处称为枚举),泛型,特征,类型别名,元组等。它还具有强大的元编程系统,使用宏可以完成繁重工作,例如生成序列化器,特征实现,或定义嵌入式DSL。同样,Rust借鉴了它的灵感来自Scheme及其后代的Racket,后者特别擅长构建DSL--通过支持卫生宏。
正确性的另一部分是没有未定义的行为,安全问题和崩溃风险。Rust通过使类型系统知道所有权来管理没有垃圾收集器的内存。资源拥有由一个单个可变结合和此所有权可以由变量传递给一个函数被转移:Rust称此移动的资源(以类似于C ++的感举动语义)。
当变量绑定超出其词法范围时,它所拥有的资源将被删除 - 也就是说,已经解除分配(可以通过将其移动到遗忘中来提前删除该值)。虽然它们可以证明是“活着的”,但价值观可以通过共享方式不可变地借用,也可以以独占方式可变地借用。这与C ++引用类似,但它允许Rust编译器证明不存在对给定数据结构的共享,可变访问,并且开发人员将不用担心一件事。
最后但同样重要的是,Rust通过确保不跨线程共享非线程安全值来保护开发人员免受许多并发问题的影响。
在Rust中有很多资源可以理解所有权和借用,但我会举一个简单的例子。请注意,所有权的复杂性更多,特别是某些类型可以选择复制而不是移动。例如,“拥有”一个整数是没有意义的,因为它很小,可以简单地复制。Rust编译器也在不断发展,变得更加智能,并允许更直观的代码。
fnmain() {
let s = "Foo".to_string();
let newS = foo(s);
println!("{}", s);
}
fn foo(s:String) ->String{
let s2 ="Bar".to_string();
println!("{}", s2);
s
}
在上面的代码片段中,创建了一个名为s的新String,并将其传递给函数foo。因为foo接受String而不是String的引用,它获取其所有权而不仅仅是借用它:String被移动到foo。后来,还是在主尝试打印时小号的Rust编译器会抱怨被绑定的资源已经被了移动,因此不再可用。该程序将无法编译。
事实证明,foo返回一个未更改的s,但编译器不知道它,也没有任何开发人员只关注主函数,foo的签名和String的特征。知道这些元数据应该足以知道foo获得所有权并且我们对该资源的访问权将丢失。
To make our program compile, we can simply print newS instead. Rust even lets us call it s again, which is great because s was not usable after passing it to foo anyway! The following program prints “Bar” then “Foo”.
为了使我们的程序编译,我们可以简单地打印newS。Rust 甚至让我们再次调用s ,这是伟大的,因为s是不可使用的当把它传递给给foo之后!以下程序打印“Bar”,然后打印“Foo”。
fn main() {
let s = "Foo".to_string();
let s = foo(s);
println!("{}", s);
}
fn foo(s: String) -> String {
let s2 = "Bar".to_string();
println!("{}", s2);
s
}
查看foo,它还会在名为s2的变量下创建另一个String绑定。当它超出范围时它将被删除。到目前为止,这看起来很像自动内存管理,用于在C ++中的堆栈上分配的结构或Java中的GC管理的引用句柄。不同的是,资源-如堆栈或堆分配的内存-总是有精确的一个所有者。
这里,在foo中返回s2而不是s会将s2移回调用者,程序将打印“Bar”“Bar”。顺便说一句,如果你想知道为什么在打印println之后s2仍然可用!,这是因为它打印出来!只借了它!最后,(!)一目了然地显示出 println! 是一个宏。
最后,没有理由在这里分配一个String,因为“Foo”和“Bar”常量已经在二进制文件中。Rust可以直接指向那里并获得我们可以借用的“切片”。我们使用一个名为&str的类型,它可以是借来的String或切片!
可维护性
所有权系统不仅使程序执行安全,而且我认为它还使代码更易于维护和重用。只需浏览一个函数或一个词法范围,Rust开发人员就可以确定其变量的属性。他们可以构建API,更准确地传达应该如何使用它们,并从类型检查器中获取帮助。
让我举几个例子。在C中,编译器不知道函数返回的指针有效多长时间。在垃圾收集语言中,您可以存储函数返回的任何引用,并防止一个潜在的大块内存被释放。许多Java库附带文档,描述何时可以安全地调用哪些方法,对象将处于有效状态多长时间,并通过异常强制执行这些规则或让您处理未定义行为的后果。当涉及多线程时,这变得更加困难。Rust可以返回在编译时已知的给定生命周期内有效的引用,提供副本或提供您可以随时处置的共享引用。
Rust附带了Cargo,一个用于构建,管理依赖项(称为Crates),运行测试,修复警告等的命令行实用程序。拥有社区认可的构建工具意味着可以将重点放在那里。它有助于Cargo的开发人员做出了很好的选择,例如支持开箱即用的盒式语言版本控制,使用人类可读和可编辑的配置格式(TOML),或支持可重复的构建。还有rustfmt,一个自动格式化程序,可以防止浪费时间手动格式化源文件,以及关于制表符vs空格的无休止的争论(扰乱警报:请用4个空格)。
仍然,Rust的工具是一项正在进行中的工作。Java有20年的开端,但语言本身非常适合工具。IDE如何支持宏中的DSL?时间会证明。有一个官方语言服务器,它具有VSCode和Eclipse集成。IntelliJ IDEA还有一个插件。
Rust编译器由LLVM提供支持,LLVM是一个具有高效优化器的成熟基础架构。它还可以定位WebAssembly,它允许使用Rust编写Web应用程序,并允许在虚拟空间中运行不受信任的代码。
在我看来,Rust的核心开发人员一直在积极寻找更好的想法,这是对Sun 长期以来指导Java 的“ Not Invented Here ”综合症的一个令人耳目一新的改变。在这些好主意中,Rust的特性和缺乏结构继承提供了很好的设计原语,有助于构建模块化和可维护的系统。
Rust开发人员在错误处理方面做了另一个很好的选择 Rust具有Result <T,E>类型,其可以是具有成功值的Ok(T)或具有错误的Err(E)。Haskell程序员将识别Either类型。使用常规构造处理错误意味着可以使用所有常用机制 - 包括模式匹配,将Result作为值传递或序列化。
Rust还使用特征来使代码更简洁。像Java的可迭代和它的foreach循环,或Haskell的做的单子符号,Rust拥有健康确实对性状的顶部句法糖这使得它易于构建感觉自然类型。
例如,在尝试使用+操作时使用Rust的std :: ops :: Add trait 。运算符重载在C ++中总是有不好的压力,但这也是Python在数据分析方面如此强大的一个重要原因。Numpy的阵列和矩阵方便地支持我们在纸上使用的相同操作符。为了防止冲突,Rust仅允许定义特征的模块或定义目标类型的模块实现特征。这是一个自定义Point类型支持求和的简单示例。
use std::ops;
#[derive(Debug)]
struct Point(i32, i32);
impl ops::Add<Point> for Point {
type Output = Point;
fn add(self, _rhs: Point) -> Point {
Point(self.0 + _rhs.0, self.1 + _rhs.1)
}
}
fn main() {
let p1 = Point(5, 6);
println!("p1: {:?}", p1);
let p2 = Point(5, 4);
println!("p2: {:?}", p2);
let p3 = p1 + p2;
println!("p3: {:?}", p3);
}
效率
Rust 速度很快,运行速度可与C相媲美。因为它没有垃圾收集器,所以没有隐藏的成本 - 即使没有暂停,GC代码也会在不同的线程中运行并消耗资源。
由于注重效率,社区很容易为所有事情运行基准。由于代码共享既简单又安全,我们可以重用高性能数据结构。在最近的博客中,Bryan Cantrill比较了程序的C和Rust版本,并将40%的运行时改进归因于使用BTreeSet,这是Rust标准集合中开箱即用的高效数据结构。
Rust允许用户控制其数据结构的内存布局,并使间接显式。这有助于编写缓存友好的代码,但也可以与C连接.Rust的FFI与C很简单并且没有开销,这使得调用任何系统原语变得容易(但它是不安全的并且应该被适当地包装)。这是我们在Java中不愿意做的事情,特别是出于稳定性原因 - 段错误会使JVM崩溃 - 但这可能很有用。例如,最快的 Java Web服务器之一使用JNI来调用Linux的epoll,并且似乎比NIO(Java的标准非阻塞网络库)表现更好。
说到这一点,如果我们阻止等待IO的线程,那就没有意义了。Rust带有零成本期货,包括非阻塞,背压流。期货与异步Result <T,E>非常相似,这使得它们与现有模式非常吻合。
Rust的旗舰非阻塞IO库Tokio以未来为基础,为非阻塞编程提供一致且流畅的抽象。Tokio反过来由Web 框架使用的Hyper HTTP库使用。
因为期货和流链接可能变得冗长,所以可以使用async / await 来编写像惯用的Rust代码那样的异步代码。现在,await作为宏实现,但它将在以后成为标准的Rust功能!
社区和生态系统
人们可以看到Rust社区多年来为语言及其生态系统付出的巨大努力。他们从Rust开始的经历非 常愉快和热情。我发现精神动画所谓的Rustaceans邀请和煽动。
Rust有官方论坛和讨论渠道,您可以在那里获得帮助,并看到核心开发人员讨论技术问题。一切都在公开发展和辩论,欢迎捐款。
一周报给出了更新和不断改进的情绪。它选出了“本周的板条箱”来宣传社区的努力。它需要在问题上寻求帮助,有时甚至在正式的Rust分发中。
Rust社区在GitHub上非常活跃,并将许多问题标记为任何想成为贡献者的“第一个好问题”。事实上,在Rust中没有像Apache或Eclipse这样的开源基础,但是有一个强大的自由软件文化。Mozilla赞助了Rust - 许多核心开发人员都是Mozilla员工 - 但是许多大型项目仍然存在于个人GitHub帐户中。
社区仍然足够紧密,每个人都在共同建立一个完整的生态系统。为了安全起见,Rust开发人员基本上重写了所有内容,尽可能地依赖Rust代码而不是包装C,C ++或Go库。
开发人员可以将他们的箱子发布到crates.io。Rust标准库非常小,甚至是可选的,并且通过设计,大多数常见功能的开发 - 例如期货,序列化或日志记录 - 都发生在不同的板条箱中。一些板条箱通过RFC流程标准化。
由于其品质和社区,Rust吸引了大量的人才。Rust有一个很好的 加密 生态系统,用于并发和数据并行的库。您可能有兴趣使用QUIC?这里有一个图书馆!你在考虑Haskell的Quickcheck吗?检查!还是模糊测试?GTK + UI?没问题!你喜欢GraalVM吗?Rust有HolyJit!Nom和Pest是两个用于解析的库。人们在Rust 写OpenGL 视频游戏,其他人写网络服务或者WebAssembly虚拟机。
将来<Rust,_>
Rust通过实现零成本抽象并将一套优秀的,经过验证的创意与一种免费提供内存安全性的新方法相结合,创造了一项新的交易。它通过允许高级构造重新发明系统编程,并可靠地提供高级编程速度和控制。
人们仍然选择语言和框架,因为它们具有生产力并且可以保证用于特定目的。如果您想构建Web应用程序,Ruby on Rail或Java是安全的选择。
我个人被Java的整体工程质量,工具,生产力和舒适性所破坏。Rust在那里显然不太成熟,但我会打赌社区中的动态行动,以及更多采用Rust的公司,并帮助使其成为世界一流的编程环境。
在几年内,Rust可能会提供一个高效,安全的编程环境,感觉像Java一样可靠且无风险。有趣的部分是到达那里。