近些年一直有听闻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++: 有
const
,constexpr
(后者更接近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
里面有String
和byte[]
相互转换的工具。 - 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++: 有
struct
和class
,但是struct
只是一个成员默认为public
的class
,没有本质区别,是为了兼容C而存在的。 - Rust: 只有
struct
(unsafe
模式下有union
) - C#:
struct
、class
、interface
、enum
、delegates
。其中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.ExpandoObject
和System.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体系,还有
virtual
和override
。菱形问题可以通过虚继承解决。 - 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引入了
async
,await
关键字,并且有asyncio
库实现各个层级的异步封装(封装过多反而导致使用起来很摸不着头脑) - C: 没有内置支持
- C++:
<future>
库提供了异步的初步支持,C++20引入co_await
,终于在语言层面支持了程序流中插入异步块,不过真是太不elegant了,并且还只主要是给库的开发者用的。另外好像C++的协程默认都是单开线程的,而不像是其他语言可以进行单线程协程。 - C#: 从C# 5.0引入了
async
,await
关键字,应该是这些语言中引入最早的,也是支持最简明的,最容易上手的。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对变量声明周期的管理,写代码时经常需要用到Mutex
和Arc
进程
- Python: 在
os
和subprocess
库中提供了创建进程的函数。另外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
常用数据结构
下表总结了各个语言中常用数据结构的对应关系(非严格对应,他们的实现上或多或少有点区别)
Python | C++ | C# | Rust |
---|
list | vector | List | Vec |
SortedDict | map | SortedDictionary | BTreeMap |
dict | unordered_map | Dictionary | HashMap |
SortedSet | set | SortedSet | BTreeSet |
set | unordered_set | Set | HashSet |
- | function | Action /Func | Fn /FnMut /FnOnce |
- | list | LinkedList | LinkedList |
deque | deque | Queue | VecDeque |
heapify | make_heap | PriorityQueue | BinaryHeap |
- | unique_ptr | - | Box |
- | smart_ptr | - | Rc |
另外,C++ std::const_cast 可以在 Rust 中用Cell<T> 和RefCell<T> 起到类似效果(对应引用类型和指针类型?) | | | |
Rust特有的特性
在别的语言里面没有的概念,以及Rust独特的语法特性如下
- 引用检查(Borrow checker)
- 生命周期(Lifetime)声明
{}
代码块和if
、else
、break
也都是表达式,而非语句- 功能强大的宏系统
总而言之,语法设计上最优雅的我觉得还是C#和Python,功能和性能最强大的还是C++,最简单和底层的还是C,但是Rust至少有望替代C,这也是我为什么学习这些语言的原因。Rust有一些语法,虽然套用了同一个格式,但是却有很多是编译器特殊支持的(最常见的就是跟Trait相关的,如Box
),这就使得Rust有时候很不优雅,关于这一点可以看TUNA的讲座。
本文仅为我读完Rust官方入门教材之后的总结,之后有实战经验了我可能会再写一些心得吧~
另外在总结本文的时候还发现了一个对比Kotlin和C#的网页,挺有意思的,贴在这供参考