V语言评测 (2023)

Lobsters Hottest 工具

摘要

本文是对2023年V语言的全面评测,批评了其文档质量、内存管理缺陷以及功能不完整等问题。

<p><a href="https://lobste.rs/s/1rjyeh/v_language_review_2023">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/20 02:08

# V 语言评测(2023) 来源:https://n-skvortsov-1997.github.io/reviews/ 你发现了一种名为 V 的新编程语言。它看起来不错,网站上有诸多承诺,语法也很优雅,但它实际表现如何呢?本文所描述的一切均针对此提交:`b66447cf11318d5499bd2d797b97b0b3d98c3063`(https://github.com/vlang/v/commit/b66447cf11318d5499bd2d797b97b0b3d98c3063)。本文总结了我使用该语言 6 个月的经验,以及我在撰写本文时从 Discord 上收集到的信息。文章相当长,因为我试图尽可能详细地描述所有内容,以便任何人能够复现相同的行为。 学习新编程语言从哪里开始?没错,从文档开始。V 语言的文档是一个巨大的文件:`docs.md`(https://github.com/vlang/v/blob/master/doc/docs.md)。在开头不远的地方,你可以注意到 V 语言内置的类型。类型 `i128` 和 `u128` 前面的小 `soon` 前缀描述了整个语言的现状。这条说明至少已经在文档中存在 4 年了(`提交`(https://github.com/vlang/v/commit/65a8db85254b8d8d02098843202142e61aa02570)),看来我们还需要再等一段时间。 接着你可能会注意到,与 C 和 Go 不同,`int` 始终是 32 位。但在 0.4.3 版本(https://github.com/vlang/v/releases/tag/0.4.3)中,它现在在 64 位系统上是 64 位,在 32 位系统上是 32 位。你说这只是几处错误,但不,这就是整个 V 文档的现状。开发者如此之少,以至于无法保持文档处于正确状态。文档通常没有描述语言的最重要部分——例如,关于泛型的部分(https://github.com/vlang/v/blob/master/doc/docs.md#generics)只有几个代码示例,没有适当的说明。 诸如下面这样的承诺也常见于文档中: > 目前,泛型函数定义必须声明其类型参数,但未来 V 将从运行时参数类型中的单字母类型名推断泛型类型参数。 现在向下滚动到最有趣的部分:V 中的内存管理。在现代编程语言中,这几乎是语言最重要的部分。V 提供什么?首先是“垃圾回收”,这是一个很好的选择,大大简化了生活;第二个选择是“arena”,也是一个很好的选择;“手动内存管理”也适用于有经验的程序员。最后也是最有趣的选择是“自动释放”。前两种选择工作相对良好,所以我们来看看后两种。 ### 手动内存管理 在此模式下,所有分配都使用 libc 的 malloc 函数,开发者必须自己清理内存。但是标准库函数内部分配的内存呢?让我们看看 `is_ascii` 方法(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/builtin/string.v#L2379C2-L2379C2): ``` @[inline] pub fn (s string) is_ascii() bool { return !s.bytes().any(it < u8(` `) || it > u8(`~`)) } ``` 这看起来是一个小而安全的函数,但如果你在手动内存管理下调用它,你将会有内存泄漏,因为没有人清理 `bytes()` 方法(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/builtin/string.v#L2040)分配的内存。这里还有更多(https://github.com/vlang/v/blob/master/vlib/builtin/string.v#L822)类似的(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/builtin/string.v#L901)示例(https://github.com/vlang/v/blob/master/vlib/builtin/string.v#L338)。而这仅存在于字符串方法中;在整个标准库中随处可见。好吧,你可以在你的代码中不使用这些函数。让我们看看如果你用 V 在“手动”模式下写一个 Web 服务器会怎样。V 有一个名为 `vweb` 的内置框架。官方示例包含以下代码: https://github.com/vlang/v/blob/master/examples/vweb/vweb_example.v 我已尽可能简化它: ``` module main import vweb struct App { vweb.Context } pub fn (mut app App) index() vweb.Result { return app.text("Hello World") } fn main() { vweb.run(&App{}, 8082) } ``` 接下来(https://github.com/vlang/v/blob/master/vlib/vweb/vweb.v#L571),数组(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/vweb/vweb.v#L542)在 vweb 代码中从未(https://github.com/vlang/v/blob/master/vlib/vweb/vweb.v#L572)被释放(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/vweb/vweb.v#L958)(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/vweb/vweb.v#L729)。这意味着你在 vweb 上的应用程序将会有内存泄漏。 好吧,并不是每个人都写 Web,也许你需要一个简单的 CLI 工具?不幸的是,标准库中所有用于 CLI 的字符串插值都会分配内存,然后又不清理。从以上所有内容中,我可以得出以下结论:V 中的手动内存管理是一个无法用于实际应用程序的特性。最多只能在简单程序中使用,其中你从头开始编写所有内容,或者内存泄漏对你来说不严重。 ### 自动释放 现在我们来谈谈本节中最有趣的部分。让我们看看文档中如何描述此模式: > 第二种方法是 autofree,可以通过 `-autofree` 启用。它处理大部分对象(约 90-100%):编译器在编译期间自动插入必要的 free 调用。剩余的一小部分对象由 GC 释放。开发人员无需更改代码中的任何内容。“它只是有效”,就像在 Python、Go 或 Java 中一样,只是没有追踪所有事物的繁重 GC 或为每个对象使用昂贵的引用计数(RC)。 令人惊讶的是,他们在每句话中都撒了谎。让我们从头开始,文档向我们保证,使用编译器插入的 `free` 调用,90-100% 将自动清理。考虑到在 Rust 中要实现相同的功能,你需要为编译器提供大量帮助,这听起来相当乐观。V 编译器结果“比 Rust 编译器聪明得多”。 当我在语言仓库的讨论(https://github.com/vlang/v/discussions?discussions_q=is%3Aopen+autofree&page=1)中寻找时,我遇到了一个有趣的评论(链接(https://github.com/vlang/v/discussions/12343#discussioncomment-5828322)): > 在我的 V 程序中,只有 0.1% 是自动释放的,99.9% 是由垃圾回收器释放的。这完全取决于你正在制作的程序。GC 仍然非常快。 但是,让我们不要把这个当作事实,我们自己试试。这是最简单的代码: ``` module main struct Data { data []bool } fn main() { p := Data{ data: [true, false] } println(p.data) } ``` 使用 `v -autofree main.v` 编译它。然后运行 `valgrind`: ``` ==653065== Memcheck, a memory error detector ==653065== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==653065== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info ==653065== Command: /test ==653065== [true, false] ==653065== ==653065== HEAP SUMMARY: ==653065== in use at exit: 2 bytes in 1 blocks ==653065== total heap usage: 6 allocs, 5 frees, 1,085 bytes allocated ==653065== ==653065== LEAK SUMMARY: ==653065== definitely lost: 2 bytes in 1 blocks ==653065== indirectly lost: 0 bytes in 0 blocks ==653065== possibly lost: 0 bytes in 0 blocks ==653065== still reachable: 0 bytes in 0 blocks ==653065== suppressed: 0 bytes in 0 blocks ==653065== Rerun with --leak-check=full to see details of leaked memory ==653065== ==653065== For lists of detected and suppressed errors, rerun with: -s ==653065== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) ``` 出了点问题。同样有趣的是,我们的程序中只有一个数组,但 valgrind 显示分配了 1kb 的内存。请记住这一点,当我们继续讨论网站上关于 V 避免不必要分配的声明时。 让我们编译一个带有 vweb 服务器的简单示例: ``` module main import vweb struct App { vweb.Context } pub fn (mut app App) index() vweb.Result { return app.text("Hello World") } fn main() { vweb.run(&App{}, 8082) } ``` 然后我们用 valgrind 运行它。我没有向服务器发出请求,只是等待了 10 秒钟: ``` ==653318== Command: /server ==653318== [Vweb] Running app on http://localhost:8082/ ==653318== [Vweb] We have 1 workers ==653318== ==653318== Process terminating with default action of signal 2 (SIGINT) ==653318== at 0x498882D: select (select.c:69) ==653318== by 0x58AAF2: net__select (in /home/skvortsov/server) ==653318== by 0x642D81: net__select_deadline (in /home/skvortsov/server) ==653318== by 0x58B122: net__wait_for_common (in /home/skvortsov/server) ==653318== by 0x58B40B: net__wait_for_read (in /home/skvortsov/server) ==653318== by 0x591D43: net__TcpListener_wait_for_accept (in /home/skvortsov/server) ==653318== by 0x5915F9: net__TcpListener_accept_only (in /home/skvortsov/server) ==653318== by 0x5E57E1: vweb__run_at_T_main_App (in /home/skvortsov/server) ==653318== by 0x5E4262: vweb__run_T_main__App (in /home/skvortsov/server) ==653318== by 0x5F0102: main__main (in /home/skvortsov/server) ==653318== by 0x63F220: main (in /home/skvortsov/server) ==653318== ==653318== HEAP SUMMARY: ==653318== in use at exit: 122,833 bytes in 2,395 blocks ==653318== total heap usage: 2,659 allocs, 264 frees, 541,413 bytes allocated ==653318== ==653318== LEAK SUMMARY: ==653318== definitely lost: 1,004 bytes in 32 blocks ==653318== indirectly lost: 106 bytes in 15 blocks ==653318== possibly lost: 272 bytes in 1 blocks ==653318== still reachable: 121,451 bytes in 2,347 blocks ==653318== suppressed: 0 bytes in 0 blocks ==653318== Rerun with --leak-check=full to see details of leaked memory ==653318== ==653318== For lists of detected and suppressed errors, rerun with: -s ==653318== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) ``` 没有请求,在 10 秒内我们绝对丢失了 1kb 内存。 让我们回到文档中的描述: > 剩余的一小部分对象由 GC 释放。 这再次不是事实。我找到了一个最近的提交(https://github.com/vlang/v/commit/207203f5998e6b1844a32fe628e0eb64325db64d),其中传递 `-autofree` 标志会立即将 `gc` 设置为 `none`: ``` '-autofree' { res.autofree = true res.gc_mode = .no_gc res.build_options << arg } ``` 所以,这个说法完全错误。 你知道为什么做了这个改动吗?让我们看一个例子: ``` fn main() { ptr := malloc(1) free(ptr) } ``` 然后我们看看 `free` 函数的定义(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/builtin/builtin.c.v#L586): ``` @[unsafe] pub fn free(ptr voidptr) { $if prealloc { return } $else $if gcboehm ? { // It is generally better to leave it to Boehm's gc to free things. // Calling C.GC_FREE(ptr) was tried initially, but does not work // well with programs that do manual management themselves. // // The exception is doing leak detection for manual memory management: $if gcboehm_leak ? { unsafe { C.GC_FREE(ptr) } } } $else { C.free(ptr) } } ``` `$if`(https://github.com/vlang/v/blob/master/doc/docs.md#if-condition)指定了在编译期间评估的条件。以前,当我们只传递 `-autofree` 而没有明确传递 `-gc none` 时,条件 `$if gcboehm ?` 为真,并且由于 `gcboehm_leak` 默认也未设置,因此 `free` 最终成为一个不执行任何操作的空操作函数。这是生成的 C 代码: ```c void _v_free(voidptr ptr) { #if defined(_VPREALLOC) { } #elif defined(_VGCBOEHM) { } #else { C.free(ptr); } #endif } ``` 所有这些代码都是 C 预处理器代码,所以编译器看到的代码如下: ```c void _v_free(voidptr ptr) { } ``` 这不会释放任何东西。 让我们回到文档: > 剩余的一小部分对象由 GC 释放。 这甚至不是真的,即使 V 在某处插入了 `free`,它们也没有任何效果,所有内容都是由 GC 清理的。即使在此修复之后,这也不是真的,因为在传递 `-autofree` 标志时 GC 被禁用了。 你可能看过这个视频:https://www.youtube.com/watch?v=gmB8ea8uLsM 在其中,语言作者展示了他编辑器 Ved(https://github.com/vlang/ved),并展示了如何使用 `v . -autofree` 编译它,并声称他的技术足够成熟,像文本编辑器这样复杂的应用程序不会泄漏。我尝试使用最新版本的 V 并启用 `autofree` 标志构建编辑器,启动二进制文件时得到以下错误: ``` V panic: as cast: cannot cast `map[string]toml.ast.Value` to `[]toml.ast.Value` v hash: 0966fd3 /tmp/v_1000/ved.5480247081914024169.tmp.c:13797: at _v_panic: Backtrace /tmp/v_1000/ved.5480247081914024169.tmp.c:14296: by __as_cast /tmp/v_1000/ved.5480247081914024169.tmp.c:43867: by toml__Doc_value_ /tmp/v_1000/ved.5480247081914024169.tmp.c:43836: by toml__Doc_value /tmp/v_1000/ved.5480247081914024169.tmp.c:44579: by main__Config_init_colors /tmp/v_1000/ved.5480247081914024169.tmp.c:44551: by main__Config_reload_config /tmp/v_1000/ved.5480247081914024169.tmp.c:46595: by main__main /tmp/v_1000/ved.5480247081914024169.tmp.c:50668: by main ``` 没有 `autofree` 一切都工作正常。好吧,显然 autofree 在 3 年里变得更糟了。有趣的是,语言作者自己的项目无法与其语言的主要特性配合工作。 让我们尝试使用 `autofree` 构建编译器本身: 然后让我们尝试使用生成的二进制文件再次编译自身: 我们在运行时得到错误: ``` ./v2 self /tmp/v_1000/v2.10486918756004741764.tmp.c:25123: at string_starts_with: RUNTIME ERROR: invalid memory access /tmp/v_1000/v2.10486918756004741764.tmp.c:35912: by os__impl_walk_ext /tmp/v_1000/v2.10486918756004741764.tmp.c:35888: by os__walk_ext /tmp/v_1000/v2.10486918756004741764.tmp.c:42645: by v__pref__detect_musl /tmp/v_1000/v2.10486918756004741764.tmp.c:42743: by v__pref__parse_args_and_show_errors /tmp/v_1000/v2.10486918756004741764.tmp.c:4811: by main__main /tmp/v_1000/v2.10486918756004741764.tmp.c:5835: by main ``` 让我们回到文档的最后部分: > 开发人员无需更改代码中的任何内容。“它只是有效”,就像在 Python、Go 或 Java 中一样,只是没有追踪所有事物的繁重 GC 或为每个对象使用昂贵的引用计数(RC)。 我们在上面发现,在提交 207203f(https://github.com/vlang/v/commit/207203f5998e6b1844a32fe628e0eb64325db64d)之前,传递 `-autofree` 标志我们**得到**了“追踪所有事物的繁重 GC”,之后即使在最简单的示例中我们也**得到**了内存泄漏。 我还要指出,语言作者早在 `0.3` 版本时(提交(https://github.com/vlang/v/blob/0f9537ece544b7fda31cadf4dc95fd4b552f94be/ROADMAP.md))就承诺使这项技术“可用于生产”,然后是 0.5(https://discord.com/channels/592103645835821068/592106336838352923/1136589637658345563),也许在 0.6(https://discord.com/channels/273534239310479360/818964227783262209/1146427952083513467),以及 ROADMAP(https://github.com/vlang/v/blob/master/ROADMAP.md)中是 1.0。 > “装到成功为止”。 有趣的是,语言作者认为将 `autofree` 与 GC 一起使用没有意义(https://discord.com/channels/592103645835821068/592106336838352923/1126201270902997114),尽管文档说正是 GC 清理了剩余的“10%”对象。真是奇妙。 因此,从以上所有内容可以得出结论,`autofree` 是一项非常粗糙的技术。语言作者试图通过该视频推广它,根据评论来看他成功了,我不明白为什么人们相信他,因为一个简单的测试表明,即使简单的程序也泄漏严重。 **近 5 年后,V 最有趣的功能仍处于非常初级的阶段,而作者除了承诺一切很快就会实现外什么也没做。** 已经很清楚,语言作者及其忠实的追随者会开始说 `autofree` 尚未可用于生产,但我所描述的问题...

相似文章

正确设计编程语言(2024)

Lobsters Hottest

这篇博客文章提出了新编程语言的设计指南,强调了修复常见问题,如缩进多行字符串、正确的文件路径处理、一致的文件扩展名和可扩展的语法特性。

金钉子:复活 Vale(n) 编程语言

Lobsters Hottest

这篇文章讨论了一个雄心勃勃的项目,旨在复兴Vale编程语言并创建一种名为Valen的新语言,目标是实现与Rust的无缝集成,以支持跨语言泛型和内存安全。

迈向可理解的软件

Lobsters Hottest

本文批判了当前的编程实践和对大语言模型的依赖,反而主张通过更好的抽象、文档和软件栈来使代码更易于理解和维护。

揭示VLM可解释的故障模式

arXiv cs.AI

本文介绍了Revelio,这是一个通过搜索离散概念组合来系统性地发现视觉语言模型(VLM)中可解释故障模式的框架。应用于自动驾驶和室内机器人领域,它揭示了此前未报道的、可能导致碰撞或安全危险的漏洞。