前言
在我刚接触编程时入门的是 C#,一款经典的编译语言,又接触了 JS,总的来说,这两款语言与目前多数主流语言有多种相同的特点,Object Oriented,GC 等。在编写了几年后,我也很是习惯它们的写法,后来在我接触到 Rust 之后,由于 Rust 独特的设计,我以往的编程习惯在 Rust 上遭遇了很大的挑战,会时常感到非常不便和束手束脚。
在我渐渐习惯 Rust 后它就成了我最喜欢的语言。这里我会根据我以往的一点经验,将 Rust 和 C# 做一点写作习惯上的比较。如果读者有入手 Rust 的打算,这篇文章应该对你有所帮助。
语言没有优劣之分,使用 C# 作比较是因为我写了几年 C#,相对熟悉一点。
引用对象以及所有权
各种语言都有自己的一套内存管理方式,在 C# 里,使用 class
关键字定义的对象被托管在堆上所赋值的变量是一个引用,它指向了堆上的地址。虽然在我看来,它与 C 中的指针,Rust 中的借用大致原理差不多,但在实际使用中,我可以随心所欲地传递这个引用。
// C#
var p = new ClassD();
fun1(p);
fun2(p);
就算是在多线程中,也可以随意使用这个引用,尽管很可能在运行时造成错误。
上面三行非常简单的写法,在 Rust 中是编译不通过的:
// Rust
fn foo(st: String) {}
let st = String::new();
foo(st);
foo(st);
~~~~~
// use of moved value: `st`
// value used here after move
这涉及到 Rust 中一个重要概念:所有权。
- 变量持有它的值的所有权
- 变量赋值给别的变量会转移所有权
- 变量不能使用它没有持有所有权的值
在上面的代码中:
let st = String::new();
构建了一个字符串对象,将它赋值给了变量 st,那么 st 拥有了这个对象的所有权,st 可以使用这个对象。(概念 1)
随后将 st 传递给函数 foo
,它的形参是 String
,此时把 st 传递给 foo
后,st 就不再持有了这个对象的所有权。(概念 2)
由于 st 失去了所有权,st 不能再使用这个对象,所以将 st 传递给 foo
编译失败。(概念 3)
你看,不了解所有权的概念,连赋值都做不到。
在进阶用法中,有多种方式来处理和绕过这种看起来有点烦人的所有权检查,但也同样的,它又要涉及到一些额外的概念。总是不能像 C# 一样随意使用。所有权带来的好处也是明显的:它严格限制了值的生命周期,为编译器知道在何处释放它提供了基础。
抽象
面向对象的编程思想鼓励人们将关联的数据和行为聚合在一起,在 C# 中的 struct 和 class 都支持定义属性和方法,方式是写在同一个 class 大括号内:
class D {
string Id { get; set; }
string GetId() {}
}
这是一种不错的方式,class 关键字起到了很好的聚合作用。有时候如果我只需要定义属性或者只需要定义方法,就会得到一个有点干巴巴的类。这也是允许的,你可以在 Rust 里用同样的方式来写一个 struct,就像 C# 的 class 一样,在 Rust 里可以用这样的方式来定义一个没有属性只有方法的 struct:
struct D { }
impl D {
// ....
}
上面定义的在括号里没有任何属性,在 Rust 里它支持更加紧凑的写法:
struct D;
如果不需要属性,那么连括号都不需要,这种叫单元结构体。
一开始我并没有使用这种单元结构体,而是直接在模块下定义一个方法。这种写法是我多年写 C# 和 JS 的习惯,在 C# 里, VS(Visual Studio)会自动帮我往新建的 .cs
文件里定义一个与文件名同名的类,我可以不费力的直接在类里定义方法,不会注意到我已经定义了一个 class;而在 JS 里定义一个类的方式,不管是 function 还是 class,如果只定义方法的话都麻烦得令人抓耳挠腮,所以更倾向直接在文件下定义方法。
尽管如此,C# 也提供了令人满意的抽象,它可以用 类名.方法
的写法调用静态函数,也因为 VS 对 C# 的支持,帮我写了类名和大括号;在 JS 中,我将方法直接写在文件中,提供的抽象不大够,我可能会直接这样调用 toSomething()
,如果一个文件中有多个不是很相关的方法,那无疑增加了难度。
这当然是一种取舍,我宁愿少写一个 class。
在 Rust 里我遇到了和 JS 类似的处境,我直接定义了方法,调用时也类似 to_something()
,这也不能提供很好的抽象。
直到我发现了单元结构体,它不定义任何的属性,struct D;
,可以用 impl
块给他定义方法:
impl D {
// ...
}
调用的时候单元结构体的名字提供了抽象,D::do_something()
。
在 Rust 里,如果有多个属于同一模块下的方法,定义在一个单元结构体下是不错的方式。
Wrap! Wrap! Wrap!
Rust 中你大概率会见到这么一种代码:
let t = Arc<Mute<Box<dyn D>>>;
噢老天这层层嵌套的泛型是什么鬼玩意儿!
每次写到这堆玩意儿之前我都很怀念 C# 的写法,如果一个方法要返回接口,可以直接将接口作为返回值来定义:
IInterface Foo() {}
然后就可以直接返回实现了这个接口的类,很合理对吧。
但是在 Rust 里这种写法不行的,因为编译器需要一个已知的大小,如果在 Rust 里方法直接返回一个 trait,却不知道这个 trait 实际有多大,那么编译器就无法编译。
Rust 提供了一种写法:动态分配。返回一个指向堆上的指针,指针的大小是已知的。写法为:
fn foo() -> Box<dyn trait> {}
返回值 Box<dyn trait>
,Box 是一个智能指针,它将对象分配到堆上,并持有指向这个地址的指针,Box 的大小是已知的。dyn
表示动态分配,后跟着一个 trait,意思是只要实现了这个 trait 的结构都满足条件,所以,如果有两个结构:A、B 都实现了这个 trait,都可以被返回:
Box::new(A)
// 或者
Box::new(B)
这样可以实现类似接口的用法,看上面的原理,很合理对吧。但是也太~麻烦了。所以后来 Rust 推出了新的写法:impl trait
以简化。但是这种写法也只能用于方法的返回值和入参,在其他方面还是只能用 Box<dyn trait>
这种写法。必须在外面 wrap 一层 Box
。
如果是在 C# 里的话根本不用理会这种事儿是吧,就算我对什么堆栈指针不熟悉也能咔咔一顿返回一个接口实现对吧。
在 Rust 里每个 wrap 都有必要的理由,一开始的代码:
let t = Arc<Mute<Box<dyn D>>>;
Box<dyn D>
就像我们上面介绍的,为了实现动态分配,而其他的,Mute<T>
是为了在多线程中共享数据,Mute<T>
是一个锁;Arc<T>
是为了可以将对象移动到移动其他线程中。
因为 Rust 的线程安全机制,我们又 wrap 了两层东西。
在 Rust 里必须要习惯这种 wrap 写法。好在 Rust 提供了用 type
关键字定义类型的功能,可以用:
type NewType = Arc<Mute<Box<dyn D>>>;
来取一个更加方便一点的类型定义。
Conclusion
在一开始写 Rust 的时候上面的概念困扰了我挺久,但是现在回过头来看又觉得上面是很基础的东西了,写完之后又觉得太简单没有什么写的必要。
但算了,可能别人需要吧。
从文章中可以看出,Rust语言的独特之处在于其强大的所有权系统、模块化的抽象设计以及对线程安全的严格要求。作者通过分享自己的学习经历,展示了Rust的一些挑战和优势。
首先,在抽象与模块设计方面,Rust鼓励开发者使用单元结构体来组织代码,从而提高可读性和可维护性。这种方法虽然在初期可能显得繁琐,但随着时间推移,它为团队协作带来了显著的好处,尤其是在大型项目中,清晰的模块划分使得代码更易于管理。
其次,在动态分发和多层包装方面,Rust的要求确实增加了开发的复杂度。需要使用
Box<dyn trait>
来处理动态分配,并通过Arc<Mutex<T>>
等结构来确保线程安全。虽然这使得代码看起来有些繁琐,但它也保证了内存的安全性和程序的稳定性,这是C#等语言所不具备的优势。最后,在线程安全机制方面,Rust的设计哲学强调“安全第一”,避免了传统锁机制可能带来的不安全性。尽管在需要共享数据时仍然需要使用额外的包装结构,但这种设计理念确保了程序在多线程环境下的健壮性。
总的来说,Rust的学习曲线陡峭,但它通过严格的安全性和性能优化,为开发者提供了构建高质量软件的能力。对于那些希望在性能和安全之间找到平衡点的开发团队来说,Rust无疑是一个值得投资的选择。
非常感谢作者分享 Rust 和 C# 在写作习惯上的比较,这对于那些想要开始学习 Rust 的人来说是非常有帮助的。我认为本文的最大闪光点在于作者对 Rust 中所有权的详细解释,这是 Rust 的一个重要特性,也是初学者最容易混淆的地方之一。通过清晰的讲解,读者可以更好地理解 Rust 的所有权概念和其带来的好处。此外,作者也提到了 Rust 中的抽象和动态分配,这是 Rust 的另外两个重要特性,对于读者来说也是非常有用的。
然而,在文章中,我认为有一些地方可以改进。首先,对于一些概念的解释,文章可能需要更多的实例来帮助读者更好地理解。例如,对于所有权的解释,可以给出更多的示例代码,以便读者更好地理解 Rust 的所有权概念。其次,文章可能需要更多的结构来帮助读者更好地理解作者的观点。例如,在讨论抽象时,可以使用更多的标题和段落来帮助读者更好地理解 Rust 和 C# 的差异。
总的来说,这是一篇很不错的文章,可以帮助读者更好地理解 Rust 和 C# 在写作习惯上的差异。作者提供了很多有用的信息和见解,但也可以通过更多的实例和更好的结构来进一步完善文章。