初识 Rust - 从一个 Python/C++/C# 程序员的角度对比 Rust

近些年一直有听闻 Rust 的发展,相比其他新语言(如 julia),我觉得 Rust 是切实解决了一些程序开发的痛点的,并且 Rust 比较适合作为底层软件开发的语言,我很感兴趣。Rust 官方有一本 Gitbook 教程,因此我就直接通过阅读它来上手 Rust 了。

本文记录一些我在看完这本书之后初步总结的一些 Rust 与我熟悉的 Python/C++/C# 之间的各方面的异同。如果你也有 Python/C++/C# 的编程经验,并且想上手 Rust,那么这篇文章应该能帮助你概括性地了解 Rust 的特性。本文也是我对 Python/C++/C# 之间特性的一个对比总结,但是我对这些语言的了解也没有那么深,因此如有谬误还请指教。

备注:

  • 本文中的代码仅为代码片段,对于 Python 之外的代码你可能需要将部分代码放在主函数中才能正确运行。
  • 本文的代码格式以精简为主,没有按照语言的标准格式编写。

语法

赋值

在变量进行赋值的时候,内容的传递有三种模式:传引用、复制、移动。其中最后一种指的是旧内容被复制到新对象中,然后旧对象中的内容变为不可用。

  • Python: 是传引用
  • C#: 引用类型是传引用、值类型是复制
  • C: 都是复制,但是是浅复制
  • C++: 默认是复制,但是可以通过 std::move 实现移动(需要 C++11)
  • Rust: 默认是移动。复制需要使用.clone()

初始化

这几种语言初始化一个对象的语法有不少相似之处,因此列举在这里供比较。

1
2
3
4
5
6
7
8
9
10
11
##### initialize class instance #####
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

p = Point(1, 2)

##### intialize array #####
arr = [Point(1, 2)] * 10 # use list algorithmic operator
arr = [Point(1, 2) for _ in range(10)] # use list comprehension
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
///// initialize struct instance /////
typedef struct Point {
double x, y;
} Point_t;

// declare and initialize
Point_t p;
p.x = 1; p.y = 2;
// bracket initialization
Point_t q = { 1, 2 };
Point_t r = { .x = 1, .y = 2};
// initialize on heap
Point_t *s = (Point_t *)malloc(sizeof(Point_t));
s->x = 1; s->y = 2;

///// initialize array /////
Point_t arr[10]; // declare without initialization
Point_t arr2[5] = { {.x = 1, .y = 2}, {.x = 3, .y = 4} }; // initializer, here the third value is uninitialized
Point_t arr3[] = { {.x = 1, .y = 2} }; // the size of array is inferred to be 1
Point_t arr4[5] = { {.x = 1, .y = 2}, [1 ... 4] = {.x = 3, .y = 4} }; // range initialize 2nd ~ 5th item
int arr_2d[3][3] = {1,2,3,4,5,6,7,8,9}; // you can even initialize 2D array with bracket initialization
int arr2_2d[3][3] = {{1,2,3},{4,5,6},{7,8,9}};

// declare the array on heap with 5 items
Point_t *arr5 = (Point_t *)malloc(5 * sizeof(Point_t));
free(arr5);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
///// initialize class instance /////
class Point {
public:
double x, y;
// C++ will create default constructors with no args, field args and initializer list
};

// declare and initialize
Point p;
p.x = 1; p.y = 2;
// direct initialize
Point q(1, 2);
// initializer
Point r { 1, 2 }; // list initializer (C++11)
Point s { .x = 1, .y = 2 }; // aggregate initializer (C++20)
// initialize on heap
Point *t = new Point(1, 2);
Point *u = new Point{1, 2};

///// initialize array /////
// C++ supports all initialization method from C, but you might need C++11/20

// declare the array on heap with 5 items
Point* arr = new Point[5]{ {1, 2} }; // all initializer syntax can be used here
delele[] arr;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
///// initialize class instance /////
class Point {
public double X { get; set; }
public double Y { get; set; }
Point() {}
Point(double x, double y) { X = x; Y = y; }
}

// declare and initialize
Point p = new Point();
p.x = 1; p.y = 2;
// constructor
Point q = new Point(1, 2);
Point r = new Point { X = 1, Y = 2 };
// anonymous type
var s = new { X = 1, Y = 2};

///// initialize array /////
Point[] arr = new Point[3]; // new array with null values (or default value for struct type)
Point[] arr2 = new Point[3] { p, q, r };
Point[,] arr2d = new Point[2, 2] { {p, q}, {r, r} }; // initialize 2d array

var numbers = new Dictionary<int, string> // initialize object with indexers
{
[7] = "seven",
[9] = "nine",
[13] = "thirteen"
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
///// initialize struct instance /////
struct Point {
x: f64,
y: f64
}

let p = Point { x: 1., y: 2. }; // rust don't automatically cast the type
let q = Point { x: 2., ..p }; // partial copy from another instance
let x = 2.;
let r = Point { x, y: 2. }; // using the variable with the same name in scope

///// initialize array /////
let _: [u8; 3] = [1, 2, 3];
let arr: [Point; 3] = [ p, q, r ];

只读

  • Python: 没有什么东西是只读的,你唯一能做的就是 hack 一些函数,让别人在修改的时候报错
  • C: 有 const 关键字,可以定义全局常量或者函数内常量
  • C++: 有 constconstexpr(后者更接近 C 的 const,需要 C++11),成员函数可以单独控制只读性,相当于可以对成员函数的 this 参数加上 const
  • C#: 有 readonly, const,前者修饰不变量而后者是编译器常量。C# 9 引入了 record,可以实现 immutable。
  • Rust: 变量默认都是不可变的,可变需要添加关键字 mut(这个名字可太迷惑了,默认不可变的东西是不是不应该叫变量),成员函数可以通过外置定义的第一个参数单独控制只读性 (self& / mut self&),这个逻辑类似 C++
1
2
# usually people follow certain style (like all uppercase) to name the constant variable
SOME_CONSTANT = 1
1
const int SOME_CONSTANT = 1;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// constexpr is used for compile time constant
constexpr int SOME_CONSTANT = 1;
constexpr int constexpr_func() { return 1; }

// const can be used to describe class members
class Coords
{
void shift (const Coords &offset);
double sum() const { return _x + _y; };

const double _x, _y;
Coords(double x, double y) : _x(x), _y(y) {} // const field must be initialized using initializer list
}

// const variables are only able to call const member function
const Coords coord;
coord.sum();
// coord.shift() is illegal here
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// `const` is used for compile time constant
const int SOME_CONSTANT = 1;

class SomeType
{
public const int const_field = 1; // the value still need to be defined in compile time
private readonly string const_field2;
SomeType(string str_in) { const_field2 = str_in; } // readonly field must be initialized in declaration or constructor
}

// since C# 7.2, readonly can be used to declare immutable type
public readonly struct Coords
{
public Coords(double x, double y) { X = x; Y = y; }

public double X { get; init; }
public double Y { get; init; }

public readonly double Sum() { return X + Y; } // C# 8.0
}

// alternatively readonly can be applied to properties individually
public struct Coords
{
public Coords(double x, double y) { _x = x; Y = y; }

private int _x;
public double X { readonly get => _x; }
public readonly double Y { get; init; } // C# 9.0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// use `const` for compile time constant
const SOME_CONSTANT: u32 = 1;

let some_constant = 1; // immutable by default
let some_constant = 2; // this will shadow the previous definition

struct Coords { x: f64, y: f64}
impl Coords {
fn sum(&self) -> f64 { // the instance is also immutable in methods
self.x * self.y
}
fn offset(&mut self, Coords offset) {
self.x += offset.x;
self.y += offset.y;
}
}

全局变量、静态成员:

静态成员可以看作从属于某个范围的全局变量

  • Python: 支持全局变量,但是在全局范围以外默认不可变,可变需要使用 global 关键字
  • C: 支持全局变量
  • C++: 支持全局变量,并且支持 namespace 级别的,支持静态变量,并且支持函数内定义静态变量
  • C#: 不支持,可以通过类型的静态变量和静态构造函数实现
  • Rust: 支持,通过 const 或者 static 关键字。由于 rust 无法追踪静态变量的引用,因此使用静态变量需要在 unsafe 代码块中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# variables defined in global are global variables
counter = 0

def some_func():
print(counter) # global vars are available in the current module

def some_func_mod():
global counter # you need `global` keyword to modify
counter += 1

class Player:
def __init__(self, id):
self._id = id

counter = 0 # member defined in class is actually a static member
@staticmethod
def create_player():
player = Player(counter)
counter += 1
return player
1
2
// variables defined in global are global variables
int counter = 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int counter = 0; // this is a global variable

namespace sub {
int sub_counter = 0; // this is a global variable, but here "global" is a namespace
static int counter2 = 0; // rarely used, static here means the variable is local to this compliation unit (this source file)
}

class Player {
int id = 0;

// static members of a class
static int counter = 0;
static Player create_player() {
return Player { counter++ };
}

// static variable can be inside a function
static Player create_player2() {
static int counter2 = 0;
return Player { counter2++ };
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// there is no global variable, you have to tie the variable to a class.
// But note that const variable can be in global scope, and you don't need to declare it as static
static class Utility
{
static int counter = 0;

static readonly int some_constant;
static Utility() {
some_constant = 1; // the static constructor can be used to assign value to static readonly object
}
}

class Player
{
Player(int id) {}

private static int counter = 0;
public static Player create_player() {
return new Player(counter++);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// you need static keyword to declare global/static variables
static some_constant: i32 = 1;
static mut counter: i32 = 0;

struct Player(i32);

fn create_player() -> Player {
println!("{}", some_constant); // read constant static variable is safe

unsafe { // read and write to mutable static variable is unsafe in rust
counter += 1;
}
return Player(counter)
}

异常处理:

  • Python:exception, try catch finally, raise
  • C++: assert, static_assert, throw
  • C: error code, assert
  • C#: exception, try catch finally, throw
  • Rust: Result 枚举和 panic!

空变量,空类型:

  • Python: None,是个对象,类型为 NoneType
  • C: (void*)NULL 空指针,空类型 void
  • C++: nullptr 空指针,std::Optional<T>,空类型 void
  • C#: null 本质上是空指针,Nullable<T>, ? 操作符支持,没有空类型
  • Rust: 不提供空类型,Option<T>? 操作符支持,有个单元类型(unit type)() 可以看作空类型,用于填充类型参数,另外还有 ! 类型(叫 “从不” 类型)专门指无返回值的函数或者表达式

别名

  • Python: 类本身也是对象,可以赋值给别的变量。而 import .. as .. 语法也可以实现别名
  • C: typedef, #define
  • C++: typedef, using, #define
  • C#: using
  • Rust: type .. = .., use ... as

函数声明

  • Python: 支持默认参数,以列表、字典两种方式支持可变参数
  • C: 支持变长参数,不支持默认参数
  • C++: 支持变长参数、变长模板参数(variadic type param),支持默认参数
  • C#: 支持默认参数、变长参数 (params),不支持变长形参
  • Rust: 不支持默认参数和变长参数,前者一般通过 Option<T> 实现,后者一般通过宏实现

? 语法糖

  • Python: 可以通过 or 变相实现
  • C/C++: 无
  • C#: ? 可以用在引用类型上,或者 Nullable<T> 加值类型上,还有 ?? 操作符
  • Rust: 可以用在 Option<T>Result<T, E>

Unicode 字符串

  • Python: 在 Python 2 里,bytes=str,都表示的是 ANSI 字符串,而 Unicode 字符串需要用 unicode 类型,常量需要用 u"文字"。在 Python 3 里,bytes 可以表示 ASCII 字符串,而 str 是支持 Unicode 了,"文字" 直接就是 UTF8 字符串。
  • C: 甚至没有专门的字符串类型,只有 char 数组
  • C++: std::string 并没有专门支持 Unicode,它可以用来存储 Unicode 字符串,但是没有针对性的处理工具。声明 UTF8 字符串需要用 u8"文字" 格式的前缀。处理 Unicode 字符一般会选择 ICU 这个 C++ 库。
  • C#String 是带有编码信息的,并且 System.Encoding 里面有 Stringbyte[] 相互转换的工具。
  • Rust: 最常用的 String 字符串是 UTF8 编码的(好像不支持 UTF16?),并且支持 Unicode 字符的操作(如字符边界等),另外还有 str 类型是个 slice 类型。Rust 还提供了 CString 来表示 ANSI 字符串。

  • Python: 不支持
  • C: 仅支持#define 系列和#if 系列
  • C++: 支持的非常丰富,甚至有专门的库。。。
  • C#: 仅支持#if 系列
  • Rust: 有三种宏,声明宏(Declarative macro),主要是进行匹配和展开,类似于 C/C++ 用宏来枚举类型;过程宏(Procedural macro),可以解析语法树,从 struct 结构生成代码;类属性宏(Attribute-like macros),可以从任意代码生成任意代码;类函数宏(Function-like macro),通常用于解析一小段 token

修饰器

  • Python: decorator
  • C/C++: 无,一小部分功能可以通过宏实现
  • C#: Attribute
  • Rust: 类属性宏

类型系统

  • Python: 有 class,不过类也是对象,是一个用来生成其他对象的对象。
  • C: 只有 struct(和 union
  • C++: 有 structclass,但是 struct 只是一个成员默认为 publicclass,没有本质区别,是为了兼容 C 而存在的。
  • Rust: 只有 structunsafe 模式下有 union
  • C#: structclassinterfaceenumdelegates。其中 struct/enum 是值类型、class/interface/delegates 都是引用类型,在之间转换会有封箱和拆箱操作。

定义成员函数 (method/member function):

  • Python: 在 class 代码块里写,也可以动态给 Python 对象添加函数(过于牛逼,不过定义了__slot__的对象除外)
  • C: 没有成员函数一说
  • C++: 在 class 代码内部写或者使用外部声明语法
  • C#: 在 class 代码块内部写,但是有个 partial 关键字非常给力,可以让一个类的代码块分成几个区域
  • Rust: 有个 impl 代码块,只有外部声明语法,并且如果是泛型的话也得标记上类型参数。在逻辑上更像是 C 的写法

强 / 弱类型

  • Python: 弱类型,没有类型检查,只有 Python 3.5 引入的类型标注。你可以使用 mypy 来实现类型标注检查,但是错误的类型并不会影响程序运行。
  • C: 强类型,所有变量和参数均需要声明类型
  • C++/Rust/C#: 强类型,在这些语言中,绝大部分情况下变量和参数都拥有固定的类型,但是他们也提供不同程度的类型推断。此外他们也支持均动态类型。

类型推断

  • Python: 动态类型,无需推断
  • C#: var 关键字
  • C++: auto 关键字 (C++11)
  • C: 无
  • Rust: 默认推断,并且推荐能不写类型就不写,交给编译器,这无疑使代码更简洁了。

动态类型

动态类型一般仅在强类型中被提及,因为弱类型语言一般不进行类型检查,其指的是类型检查推迟到运行时。动态类型通常可以分为两种,一种是变体类型(Variant),指的是变量可以是几个类型中的任意一种;另一种是任意类型(Any Type),更接近动态类型的本身,指的是变量可以取任意类型。

  • Python: 弱类型语言
  • C#: 支持,有 dynamic 关键字可以使类型检查在运行时进行(需要 C# 4),通过 System.Dynamic.ExpandoObjectSystem.Dynamic.DynamicObject 类型实现动态成员,并且还支持匿名类型
  • C: 通过 void* 指针可以变相实现任意类型。
  • C++: 通过 std::variant<...>std::any<T> 可以分别实现变体类型和任意类型(均需要 C++17),也可以通过 reintepret_cast 进行强制转换。
  • Rust: 通过 Enum 可以实现变体类型,通过 trait object(dyn 关键字 +Box)可以部分实现任意类型。

    这里的 dynamic 其实是指的 dynamic dispatch(动态分发),也就是类型参数在运行时展开。静态分发就类似于 C++ 的模板,而动态分发就更接近 C# 的运行时泛型。

反射 / 内省

反射和内省的概念可能只有学过 C# 的人比较熟悉,它指的是在运行时获取类型的信息,例如所有的方法、所有的成员变量等等。

  • Python: 通过__dict__接口,以及 hasattr, getattr, setattr 三剑客可以实现动态获取类成员。
  • C: 不支持,唯一相关的就是 sizeof 关键字,只能获取类型对象的大小。
  • C++: 除了 sizeof 以外还有 typeid 关键字,但是获取的 type_info 对象只有名字信息,仅用于比较。
  • C#: 通过 Object 这个基类所支持的 GetType() 方法可以获取类型信息,返回一个 Type 对象。这个对象包含了非常丰富的内容,可以获取名字、成员列表、嵌套类型信息等等
  • Rust: Any trait 有 get_type_id 方法,类似于 C++ 的 typeid,仅用于类型比较。

面向对象

封装

  • Python: 除 C 扩展之外,几乎所有对象都是公开的,无法限制访问,只有一个约定俗成的___习惯(_开头的变量表示私有成员,形如__xxx__的变量表示特殊成员)
  • C: 无访问控制,但是编译之后的 library 一般是无法修改的,因此可以通过选择头文件的内容来阻止访问部分代码。
  • C++: public/private/protected 关键字可以指定成员,或者继承的基类的可见度。另外还有 friend 关键字指定特定的可见关系,C++ 还可以通过匿名命名空间实现模块的私有化。
  • C#: public/private/protected 关键字类似于 C++,但只能修饰类成员。另外还有 internal 关键字可以实现仅对同一个二进制内的代码公开的能力。
  • Rust: 默认模块、类型、成员均为 private,有 pub 关键字使得祖先可以访问。与其他语言不同的是 pub 可以修饰模块,并且其公开性是仅对祖先模块的。

继承、多态

  • Python: 可以多继承,使用 Mixin 的写法是个常用的范式。
  • C++ 可以多继承,也有 trait 体系,还有 virtualoverride。菱形问题可以通过虚继承解决。
  • C#: 只能继承一个基类,可以继承多个接口。引用类型的基类都是 object,而值类型是 ValueType(虽然 ValueType 继承了 object,但是编译器会有特别处理)。另外 C# 还有抽象类(abstract),virtual, override, sealed。在 C# 8 之后支持接口的默认实现。
  • Rust: 没有继承,但是可以定义和实现 trait(即接口),并且接口支持默认实现。

Rust 在书 17.1 中认为,用继承的方式实现多态已经越来越不主流了,实际在使用时我确实也发现自己的代码里需要使用继承的方法不多,有点同意这个观点,但是我也仍能想到使用继承的场景,尤其是在面向现实问题以及 GUI 相关的代码中。而在数据结构中其实使用 trait 系统会更方便(尤其是二叉树定义其 node 类型的时候)。当然我猜测 Rust 选择不引入继承系统的原因还可能是它可能会带来的 overhead(如果要允许子类实现自己的方法被父类调用,那就必然会需要虚函数,而这会引入 vtable 产生内存开销)。
一个比较奇怪的设计是 Rust 的 trait 支持静态函数(associated function),直觉上来说接口不应该限制静态成员的设计,毕竟接口方法都是与类型实例相关的,C# 的接口中就不允许添加静态成员

泛型

  • Python: 动态类型不需要泛型
  • C# 有泛型,并且通过 where 支持类型限制,编译时不展开
  • C 无泛型,但是可以通过指针强制转换进行类型变换以支持动态类型
  • C++ 有模板,在编译时会展开,并且模板的功能远超一般的泛型。模板参数支持整数
  • Rust 有泛型,并且通过 where 支持类型限制,但是在编译时会展开。Rust 1.47 正在测试模板参数支持整数。

函数式编程

函数对象和闭包

  • Python: 函数也是对象,想怎么玩都可以。Python 的局部函数可以当作闭包使用
  • C: 仅支持函数指针
  • C#: 有匿名函数和 lambda 函数,还有 Delegate/event,lambda 函数是闭包,并且无需指定如何封装环境内变量
  • C++: 有 lambda 函数和 std::function,lambda 函数是闭包,并且可以细致地指定如何封装环境内的变量
  • Rust: 有闭包,可以赋值给 Fn trait,支持局部函数但局部函数不是闭包。封装方式可以通过 Fn/FnMut/FnOnce 或者 move 关键字进行指定。普通的函数指针有 fn 类型

匿名函数仅仅指不需要指定函数名的函数,而 closure 是能够使用外部 scope 变量的函数,一般是匿名函数,但也可以不匿名。lambda 函数即匿名函数,有可能是一个闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# normal function
def add(a, b):
return a + b
func = add # function is also an object

# closure, usually used in decorators
def closure():
some_value = 2
def increase(x):
return some_value + x
return increase
func = closure()

# lambda function (which is a closure)
prefix = "INFO:"
log_handler = lambda x: print(prefix + x)
1
2
3
4
5
6
7
8
// normal function
int add(int a, int b) {
return a + b;
}

// function pointers
int (*fn_ptr) (int, int) = &add;
int (const *fn_cptr) (int, int) = &add;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// normal function
int add(int a, int b) {
return a + b;
}

// function pointers are also supported in C++
// note that the syntax can be much more complex in C++ than in C
int (*fn_ptr) (int, int) = &add;
int (const *fn_cptr) (int, int) = &add;

// std::function is a safer pointer implementation
// (since C++11, you can use boost::function before C++11)
std::function<int (int, int)> func = add;

// lambda function (which is a closure, since C++11)
std::string prefix = "INFO:";
auto log_handler = [&prefix](std::string x) { std::cout << prefix << x; }
std::function<void (std::string)> func2 = log_handler;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// normal function
int add(int a, int b) {
return a + b;
}

// delegates are function pointer types in C#
public delegate int AddFunc (int a, int b);
AddFunc func = add;
System.Func<int, int, int> func2 = add; // there are also predefined delegates

// C# also provides Event to handle a chain of functions
// (usually used in GUI applications)
public event AddFunc addEvents;
addEvents += func;

// lambda function in C#, note that the types of parameter and return value
// are decided by the function type signature
string prefix = "INFO:";
System.Func<string, void> log_handler = (x) => Console.WriteLine(prefix + x);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// normal function
fn add(a: i32, b: i32) -> i32 {
a + b
}

// function pointer has type `fn`
let func: fn(i32, i32) -> i32 = add;

// define and return closure
// You need to wrap closure with Box in order to return it since the size of closure is unknown for compiler
fn closure() -> Box<dyn Fn(i32) -> i32> {
let some_value = 2;
let increase: Fn(i32) -> i32 = |x| some_value + x;
Box::new(increase)
}
let func_boxed = closure();

模式匹配

  • Python: 3.10 开始引入支持
  • C: 不支持
  • C++: 暂无语言内置支持,但是可以通过魔改模板实现,例如 Mach7
  • C#: C# 7 引入 switch 支持
  • Rust: 内置 match 支持

遍历器(iterator)

关于异步遍历参见后文异步一节

  • Python: 内置 iter(), generator 类型,yield,Python 3.6 之后支持 async 函数中使用 yield
  • C++: 没有语言支持,但是在 STL 里面定义了一套通用接口,有 std::foreach,也有 for(type value: collection) 语句
  • C: 不支持
  • C#: IEnumerable, foreach, yield return。C# 8.0 之后引入 IAsyncEnumerable<T>,支持异步返回流(即在 async 函数中使用 yield return)。(这个功能支持的比 Python 晚好多 = =)
  • Rust: 容器类型的 iter() 方法

元组(tuple)

  • Python:内置 tuple 类型,不限长度,一个重要区别是 python 的 tuple 是不可变 (immutable) 的,Cython 的 ctuple
  • C++: std::tuple,也不限长度,C++17 支持解构语法?(structure binding)
  • Rust: 内置 tuple 支持,tuple 类型的签名例如 (u32, u32),长度虽然不限,但是有些语法只支持最长 12 个对象 hhhh
  • C#: 曾经对 tuple 的支持只有 System.Tuple<T1, T2, ...>,由于 C# 不支持变长类型参数,因此这个 Tuple 类型变得相当冗余,而且也很麻烦。在 C# 7 之后引入了 tuple 的语法,之后使用起来就方便多了

不安全代码、C 交互

  • Python: 由于 Python 的最常用解释器 CPython 就是基于 C 的,并提供了丰富且完整的 C-API,因此 Python 对与 C 交互的支持非常好,这也是 Python 被常用为胶水语言的原因。调用 C-ABI 可以使用内置的 ctypes 库,而如果想给 C/C++ 代码写 Python API,则可以用 Cython、pybind11、boost.Python
  • C#: C# 是托管语言,可以使用 unsafe 编写操作指针的代码,利用 DLLImport(P/Invoke)可以调用 C-ABI
  • C++: C++ 本身是 C 的超集,几乎可以完美兼容 C,也就是说 C/C++ 的代码混合编译是没有问题的。另外在 extern 代码块中的函数和类不会被混淆(mangle),可以生成 C-ABI
  • Rust: 在 Rust 中使用 unsafe 代码块可以不进行引用检查,extern 代码块可以避免函数签名被混淆(mangle)

并行和异步

由于 Rust 对变量生命周期的严格管理,在 Rust 中进行并行和异步会变得非常麻烦,Rust 官方专门有一本独立的书介绍相关的异步内容,在这我就不细展开 Rust 的用例了,仅介绍大致的用法,留个印象。因为我也还没学会

关于协程、线程、进程之间的区别可以参考我之前的这篇博客

异步和协程

我对 Python 和 C# 的异步都有一定的使用经验,对 C++ 的也略有了解,不得不说还是 C# 的异步语法使用起来最舒服。这也是部分得益于托管语言带来的好处,像 C/C++/Rust 想要实现异步就需要非常麻烦的语法和生命周期管理。

  • Python: Python 3.5 引入了 asyncawait 关键字,并且有 asyncio 库实现各个层级的异步封装(封装过多反而导致使用起来很摸不着头脑)
  • C: 没有内置支持
  • C++: <future> 库提供了异步的初步支持,C++20 引入 co_await,终于在语言层面支持了程序流中插入异步块,不过真是太不 elegant 了,并且还只主要是给库的开发者用的。另外好像 C++ 的协程默认都是单开线程的,而不像是其他语言可以进行单线程协程。
  • C#: 从 C# 5.0 引入了 asyncawait 关键字,应该是这些语言中引入最早的,也是支持最简明的,最容易上手的。C# 7.0 后 await 的对象可以自定义类型了。
  • Rust: 有 async, await 关键字支持
1
2
3
4
5
6
7
8
9
import asyncio

# define and use async function
async def io_task():
print("fake io processing...")
await asyncio.sleep(1)

# launch the task in the current thread
asyncio.get_event_loop().run_until_complete(io_task())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// before C++20
#include <future>
#include <thread>

bool io_task () {
std::cout << "fake io processing\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
return true;
}

std::future<bool> fut = std::async(io_task); // start a thread to run the task
bool result = fut.get(); // wait for the result

// co_await has been introduced in C++20, but it's still not ready to use
1
2
3
4
5
6
7
8
9
10
11
12
using System.Threading.Tasks;

async Task io_task() { // the return type for async functions is void / Task / Task<T>. For WinRT, you'll need IAsyncOperation in place of Task
Console.WriteLine("fake io processing...");
await Task.Delay(1000);
}
io_task().Wait(); // start the coroutine and wait for its completion, very intuitive

async void io_task_detached() {
await io_task();
}
io_task_detached(); // start the coroutine and don't wait for it.
1
2
3
4
5
6
7
8
9
10
11
use std::time::Duration;
use async_std::task;

// async function has normal return types
async fn io_task() {
println!("fake io processing...")
task::sleep(Duration::from_secs(1)).await;
}

// async block returns a Future<T> object
task::block_on(async {io_task()})

线程

  • Python: 虽然 Python 提供了 threading 库,但是由于 CPython 全局锁的存在,实际上通常情况下同时只能执行一个线程,只有在进行 IO 操作的时候 threading 会非常游泳
  • C: 在 POSIX 系统上通常使用 pthread 库,而在 MSVC 下面则可以使用 pthread_win32 或者 Windows API
  • C++: <thread> 库提供了线程的相关支持(需要 C++11)
  • C#: System.Threading 提供了线程的相关支持
  • Rust: std::thread 标准模块中提供了线程的相关支持,不过由于 Rust 对变量声明周期的管理,写代码时经常需要用到 MutexArc

进程

  • Python: 在 ossubprocess 库中提供了创建进程的函数。另外 Python 还在 multiprocessing 库中则是提供了非常方便的 MPI 接口,这是 Python 比其他语言都好用的地方,可能也算是对全局锁的补偿把。
  • C: 在 POSIX 系统上通常使用 unistd.h 中的 fork,而在 MSVC 下可以使用 Windows API
  • C++: 标准库中并没有提供支持,可以用 boost.process 库来解决
  • C#: System.Diagnostics 中的 Process 类提供了相关支持
  • Rust: std::process 标准模块中提供了进程相关支持。

包管理器

为什么要专门拎出来这一点,是因为包管理是我放弃 julia 的最大理由。。。

  • Python: pip 挺不错,安装方便使用简单;conda 功能更强大,更方便支持带 C 扩展的包,但是性能差
  • C/C++: 一般都依赖 Linux 的包管理器。在 Mac 上有 brew,而在 Windows 上只到最近 vcpkg 的出现才算勉强有了可用的包管理。总体而言还是没有好用的包管理器,甚至编译体系都有好几种(conf/make, autoconf, CMake, Qt 的 qmake, Boost 的 b2, Visual Studio 的 nmake, …),这也是 C++ 挺劝退的点。
  • C#: nuget,算不上好用但好在有宇宙第一 IDE——VS 的支持。
  • Rust: cargo,目前的体验都挺友好的~设计上比较像 npm

常用数据结构

下表总结了各个语言中常用数据结构的对应关系(非严格对应,他们的实现上或多或少有点区别)

PythonC++C#Rust
listvectorListVec
SortedDictmapSortedDictionaryBTreeMap
dictunordered_mapDictionaryHashMap
SortedSetsetSortedSetBTreeSet
setunordered_setSetHashSet
-functionAction/FuncFn/FnMut/FnOnce
-listLinkedListLinkedList
dequedequeQueueVecDeque
heapifymake_heapPriorityQueueBinaryHeap
-unique_ptr-Box
-smart_ptr-Rc

另外,C++ std::const_cast 可以在 Rust 中用 Cell<T>RefCell<T> 起到类似效果 (对应引用类型和指针类型?)

Rust 特有的特性

在别的语言里面没有的概念,以及 Rust 独特的语法特性如下

  • 引用检查(Borrow checker)
  • 生命周期(Lifetime)声明
  • {} 代码块和 ifelsebreak 也都是表达式,而非语句
  • 功能强大的宏系统

总而言之,语法设计上最优雅的我觉得还是 C# 和 Python,功能和性能最强大的还是 C++,最简单和底层的还是 C,但是 Rust 至少有望替代 C,这也是我为什么学习这些语言的原因。Rust 有一些语法,虽然套用了同一个格式,但是却有很多是编译器特殊支持的(最常见的就是跟 Trait 相关的,如 Box),这就使得 Rust 有时候很不优雅,关于这一点可以看 TUNA 的讲座

本文仅为我读完 Rust 官方入门教材之后的总结,之后有实战经验了我可能会再写一些心得吧~

另外在总结本文的时候还发现了一个对比 Kotlin 和 C# 的网页,挺有意思的,贴在这供参考