初识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()

初始化

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

  • Python
  • C
  • C++
  • C#
  • Rust
 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++
  • Python
  • C
  • C++
  • C#
  • Rust
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代码块中。
  • Python
  • C
  • C++
  • C#
  • Rust
 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函数即匿名函数,有可能是一个闭包

  • Python
  • C
  • C++
  • C#
  • Rust
 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关键字支持
  • Python
  • C++
  • C#
  • Rust
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#的网页,挺有意思的,贴在这供参考

使用 Hugo 构建
主题 StackedJimmy 设计,Jacob 修改