编程范式

张伯雨 张伯雨     2022-09-18     210

关键词:

编程范式

Rust是一个多范式 (multi-paradigm) 的编译型语言。除了通常的结构化、命令式编程外, 还支持以下范式。

函数式编程

Rust使用闭包 (closure) 来创建匿名函数:

let num = 5;
let plus_num = |x: i32| x + num;

其中闭包plus_num借用了它作用域中的let绑定num。如果要让闭包获得所有权, 可以使用move关键字:

let mut num = 5;

{ 
    let mut add_num = move |x: i32| num += x;

    add_num(5);
}

assert_eq!(5, num);

Rust 还支持高阶函数 (high order function),允许把闭包作为参数来生成新的函数:

面向对象编程

Rust通过impl关键字在structenum或者trait对象上实现方法调用语法 (method call syntax)。 关联函数 (associated function) 的第一个参数通常为self参数,有3种变体:

  • self,允许实现者移动和修改对象,对应的闭包特性为FnOnce
  • &self,既不允许实现者移动对象也不允许修改,对应的闭包特性为Fn
  • &mut self,允许实现者修改对象但不允许移动,对应的闭包特性为FnMut

不含self参数的关联函数称为静态方法 (static method)。

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn new(x: f64, y: f64, radius: f64) -> Circle {
        Circle {
            x: x,
            y: y,
            radius: radius,
        }
    }

    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

fn main() {
    let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
    println!("{}", c.area());

    // use associated function and method chaining
    println!("{}", Circle::new(0.0, 0.0, 2.0).area());
}

为了描述类型可以实现的抽象接口 (abstract interface), Rust引入了特性 (trait) 来定义函数类型签名 (function type signature):

trait HasArea {
    fn area(&self) -> f64;
}

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

struct Square {
    x: f64,
    y: f64,
    side: f64,
}

impl HasArea for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn print_area<T: HasArea>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

其中函数print_area()中的泛型参数T被添加了一个名为HasArea的特性约束 (trait constraint), 用以确保任何实现了HasArea的类型将拥有一个.area()方法。 如果需要多个特性限定 (multiple trait bounds),可以使用+

use std::fmt::Debug;

fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

fn bar<T, K>(x: T, y: K)
    where T: Clone, 
          K: Clone + Debug
{
    x.clone();
    y.clone();
    println!("{:?}", y);
}

其中第二个例子使用了更灵活的where从句,它还允许限定的左侧可以是任意类型, 而不仅仅是类型参数。

定义在特性中的方法称为默认方法 (default method),可以被该特性的实现覆盖。 此外,特性之间也可以存在继承 (inheritance):

trait Foo {
    fn foo(&self);

    // default method
    fn bar(&self) { println!("We called bar."); }
}

// inheritance
trait FooBar: Foo {
    fn foobar(&self);
}

struct Baz;

impl Foo for Baz {
    fn foo(&self) { println!("foo"); }
}

impl FooBar for Baz {
    fn foobar(&self) { println!("foobar"); }
}

如果两个不同特性的方法具有相同的名称,可以使用通用函数调用语法 (universal function call syntax):

// short-hand form
Trait::method(args);

// expanded form
<Type as Trait>::method(args);

关于实现特性的几条限制:

  • 如果一个特性不在当前作用域内,它就不能被实现。
  • 不管是特性还是impl,都只能在当前的包装箱内起作用。
  • 带有特性约束的泛型函数使用单态 (monomorphization), 所以它是静态派分的 (statically dispatched)。

下面列举几个非常有用的标准库特性:

  • Drop提供了当一个值退出作用域后执行代码的功能,它只有一个drop(&mut self)方法。
  • Borrow用于创建一个数据结构时把拥有和借用的值看作等同。
  • AsRef用于在泛型中把一个值转换为引用。
  • Deref<Target=T>用于把&U类型的值自动转换为&T类型。
  • Iterator用于在集合 (collection) 和惰性值生成器 (lazy value generator) 上实现迭代器。
  • 元编程

    泛型 (generics) 在类型理论中称作参数多态 (parametric polymorphism), 意为对于给定参数可以有多种形式的函数或类型。先看Rust中的一个泛型例子:

    enum Option<T> {
        Some(T),
        None,
    }
    
    let x: Option<i32> = Some(5);
    let y: Option<f64> = Some(5.0f64);

    其中<T>部分表明它是一个泛型数据类型。当然,泛型参数也可以用于函数参数和结构体域:

    // generic functions
    fn make_pair<T, U>(a: T, b: U) -> (T, U) {
        (a, b)
    }
    let couple = make_pair("man", "female");
    
    // generic structs
    struct Point<T> {
        x: T,
        y: T,
    }
    let int_origin = Point { x: 0, y: 0 };
    let float_origin = Point { x: 0.0, y: 0.0 };

    对于多态函数,存在两种派分 (dispatch) 机制:静态派分和动态派分。 前者类似于C++的模板,Rust会生成适用于指定类型的特殊函数,然后在被调用的位置进行替换, 好处是允许函数被内联调用,运行比较快,但是会导致代码膨胀 (code bloat); 后者类似于Java或Go的interface,Rust通过引入特性对象 (trait object) 来实现, 在运行期查找虚表 (vtable) 来选择执行的方法。特性对象&Foo具有和特性Foo相同的名称, 通过转换 (casting) 或者强制多态化 (coercing) 一个指向具体类型的指针来创建。

    当然,特性也可以接受泛型参数。但是,往往更好的处理方式是使用关联类型 (associated type):

    // use generic parameters
    trait Graph<N, E> {
        fn has_edge(&self, &N, &N) -> bool;
        fn edges(&self, &N) -> Vec<E>;
    }
    
    fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> u32 {
    
    }
    
    // use associated types
    trait Graph {
        type N;
        type E;
    
        fn has_edge(&self, &Self::N, &Self::N) -> bool;
        fn edges(&self, &Self::N) -> Vec<Self::E>;
    }
    
    fn distance<G: Graph>(graph: &G, start: &G::N, end: &G::N) -> uint {
    
    }
    
    struct Node;
    
    struct Edge;
    
    struct SimpleGraph;
    
    impl Graph for SimpleGraph {
        type N = Node;
        type E = Edge;
    
        fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
    
        }
    
        fn edges(&self, n: &Node) -> Vec<Edge> {
    
        }
    }
    
    let graph = SimpleGraph;
    let object = Box::new(graph) as Box<Graph<N=Node, E=Edge>>;

    Rust中的宏 (macro) 允许我们在语法级别上进行抽象。先来看vec!宏的实现:

    macro_rules! vec {
        ( $( $x:expr ),* ) => {
            {
                let mut temp_vec = Vec::new();
                $(
                    temp_vec.push($x);
                )*
                temp_vec
            }
        };
    }

    其中=>左边的$x:expr模式是一个匹配器 (matcher),$x是元变量 (metavariable), expr是片段指定符 (fragment specifier)。匹配器写在$(...)中, *会匹配0个或多个表达式,表达式之间的分隔符为逗号。 =>右边的外层大括号只是用来界定整个右侧结构的,也可以使用()或者[], 左边的外层小括号也类似。扩展中的重复与匹配器中的重复会同步进行: 每个匹配的$x都会在宏扩展中产生一个单独的push语句。

    并发计算

    Rust提供了两个特性来处理并发 (concurrency):SendSync。 当一个T类型实现了Send,就表明该类型的所有权可以在进程间安全地转移; 而实现了Sync就表明该类型在多线程并发时能够确保内存安全。

    Rust的标准库std::thread提供了并行执行代码的功能:

    use std::thread;
    
    fn main() {
        let handle = thread::spawn(|| {
            "Hello from a thread!"
        });
    
        println!("{}", handle.join().unwrap());
    }

    其中thread::spawn()方法接受一个闭包,它将在一个新线程中执行。

    Rust尝试解决可变状态的共享问题,通过所有权系统来帮助排除数据竞争 (data race):

    use std::sync::{Arc, Mutex};
    use std::sync::mpsc;
    use std::thread;
    
    fn main() {
        let data = Arc::new(Mutex::new(0u32));
    
        // Creates a shared channel that can be sent along from many threads
        // where tx is the sending half (tx for transmission),
        // and rx is the receiving half (rx for receiving).
        let (tx, rx) = mpsc::channel();
    
        for i in 0..10 {
            let (data, tx) = (data.clone(), tx.clone());
    
            thread::spawn(move || {
                let mut data = data.lock().unwrap();
                *data += i;
    
                tx.send(*data).unwrap();
            });
        }
    
        for _ in 0..10 {
            println!("{}", rx.recv().unwrap());
        }
    }

    其中Arc<T>类型是一个原子引用计数指针 (atomic reference counted pointer), 实现了Sync,可以安全地跨线程共享。Mutex<T>类型提供了互斥锁 (mutex‘s lock), 同一时间只允许一个线程能修改它的值。mpsc::channel()方法创建了一个通道 (channel), 来发送任何实现了Send的数据。Arc<T>clone()方法用来增加引用计数, 而当离开作用域时计数

  • Sized用于标记运行时长度固定的类型,而不定长的切片和特性必须放在指针后面使其运行时长度已知, 比如&[T]Box<Trait>
fn add_one(x: i32) -> i32 { x + 1 }

fn apply<F>(f: F, y: i32) -> i32
    where F: Fn(i32) -> i32
{
    f(y) * y
}

fn factory(x: i32) -> Box<Fn(i32) -> i32> {
    Box::new(move |y| x + y)
}

fn main() {
    let transform: fn(i32) -> i32 = add_one;
    let f0 = add_one(2i32) * 2;
    let f1 = apply(add_one, 2);
    let f2 = apply(transform, 2);
    println!("{}, {}, {}", f0, f1, f2);

    let closure = |x: i32| x + 1;
    let c0 = closure(2i32) * 2;
    let c1 = apply(closure, 2);
    let c2 = apply(|x| x + 1, 2);
    println!("{}, {}, {}", c0, c1, c2);

    let box_fn = factory(1i32);
    let b0 = box_fn(2i32) * 2;
    let b1 = (*box_fn)(2i32) * 2;
    let b2 = (&box_fn)(2i32) * 2;
    println!("{}, {}, {}", b0, b1, b2);

    let add_num = &(*box_fn);
    let translate: &Fn(i32) -> i32 = add_num;
    let z0 = add_num(2i32) * 2;
    let z1 = apply(add_num, 2);
    let z2 = apply(translate, 2);
    println!("{}, {}, {}", z0, z1, z2);
}

声明式编程范式vs命令式编程范式

编程语言分为两类:声明式、命令。  事实上,除命令式以外的范式统称为声明式。下面有一张图划分。 声明式与命令式编程理念和风格  命令式编程是行动导向(Action-Oriented)的,因而算法是显性而目标是隐性的; ... 查看详情

再谈编程范式—程序语言背后的思想

编程范式托马斯.库尔提出“科学的革命”的范式论后,RobertFloyd在1979年图灵奖的颁奖演说中使用了编程范式一词。编程范式一般包括三个方面,以OOP为例:  1,学科的逻辑体系——规则范式:如类/对象、继承、... 查看详情

编程范式

编程范式Rust是一个多范式(multi-paradigm)的编译型语言。除了通常的结构化、命令式编程外,还支持以下范式。函数式编程Rust使用闭包(closure)来创建匿名函数:letnum=5;letplus_num=|x:i32|x+num;其中闭包plus_num借用了它作用域中的let绑定num... 查看详情

面向对象

编程范式:编程是程序员用“特定的语法+数据结构+算法组成的代码”来告诉计算机如何执行任务的过程。通过对不同的编程方式的特点的归纳总结出来的编程方式类别就是编程范式。不同的编程范式本质上代表队各种类型的任... 查看详情

系统复习--编程方式

编程范式(Programmingparadigm)编程范式指我们在编写程序解决问题的思路和视角。计算机编程中存在许多编程范式,如命令式编程、声明式编程、面向对象编程以及结构化编程等等。 命令式编程(Imperative) 强调程序代码... 查看详情

“面向对象编程”就是一种范式

https://zhidao.baidu.com/question/2009948362326949908.html所谓编程范式(programmingparadigm),指的是计算机编程的基本风格或典范模式。借用哲学的术语,如果说每个编程者都在创造虚拟世界,那么编程范式就是他们置身其中自觉不自觉采用... 查看详情

新手必须要注意的编程范式(代码片段)

新手必须要注意的编程范式目录🏳️‍🌈开讲啦!!!!🏳️‍🌈苏州程序大白🏳️‍🌈🌟博主介绍编程语言主要的范式过程试编程面向对象编程函数式编程面向切面编程主要三种... 查看详情

javascript函数式编程

编程范式编程范式是一个由思考问题以及实现问题愿景的工具组成的框架。很多现代语言都是聚范式(或者说多重范式):他们支持很多不同的编程范式,比如面向对象,元程序设计,泛函,面向过程,等等。函数式编程范式函... 查看详情

编程范式:响应式编程

 响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。例如,在命令式编程环境中,a:=b+c表示将表达式... 查看详情

第十二篇:编程范式(代码片段)

编程是程序员用特定的语法+数据结构+算法组成的代码来告诉计算机如何执行任务的过程,一个程序是程序员为了得到一个任务结果而编写的一组指令的集合,正所谓条条大路通罗马,实现一个任务的方式有很多种不同的方式或... 查看详情

面向对象:编程范式类对象(代码片段)

编程范式:1.面向过程编程:核心是“过程”,“过程”指的是解决问题的步骤;就相当于在设计一条流水线  优点:复杂问题流程化,进而简单化 缺点:可扩展性差,前一发动全身2.面向对象:核心是“对象”,对象... 查看详情

响应式编程是一种异步的声名式的面向数据流的编程范式

响应式编程是一种异步的、声名式的、面向数据流的编程范式。 异步:moand、observeable、handle;声名式:用逻辑表述的形式组织代码;使用函数式编程范式。数据流:将数据视作数据流的形式,并用pipeline的形式做处理。 ... 查看详情

声明式编程范式初探

声明式编程范式初探语言编程语言可以分成两类:命令式声明式事实上,凡是非命令式的编程都可归为声明式编程。因此,命令式、函数式和逻辑式是最核心的三种范式。为清楚起见,我们用一幅图来表示它们之间的关系。与命... 查看详情

面向对象编程(代码片段)

一、编程范式编程范式即编程的方法论,标识一种编程风格.三大编程范式:1.面向过程编程:面向过程是一种以事件为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步... 查看详情

傻瓜式编程范式,程序员的基本功

...bsp;@TuringTest的发言引用中,意外发现了一篇文章《傻瓜式编程范式:程序员基本功》,由彼得·范·罗伊写于2009年,描绘了设计编程语言的操作空间。如果你喜欢这篇文章,可能也会喜欢范·罗伊和哈利迪的书《Conce... 查看详情

面向对象

编程范式编程是程序员用特定的语法+数据结构+算法组成的代码来告诉计算器如何去执行任务的过程,一个程序员为了得到一个结果而编写的一组指令集合,当然,实现一个任务可以有很多种不同的方式,对这些不同的编程方法... 查看详情

三种编程范式

命令式编程(imperative)命令式是关于“howtodo”的,告诉计算机每一个步骤如何执行声明式编程(declarative)声明式是关于“whattodo”的,不关心实现的具体步骤,只告诉想要的结果,由计算机(底层程序)决定如何做(howtodo);比如说,我们... 查看详情

整洁架构之道--三种经典的编程范式(代码片段)

本文是《CleanArchitecture》--整洁架构之道中关于编程范式相关章节的笔记,首发于公众号「Go招聘」这和软件架构的三大关注重点不谋而合:功能性、组件独立性以及数据管理。的方式。回答此问题的同时另外还会搬出这三个词语... 查看详情