Rust 編程之道 - 函數與閉包

mian 函數表明程序的入口,對於二進制可執行文件來說,main 函數必不可少,對於庫函數來說,main 函數沒有必要。數組

函數定義

Rust 中函數經過 fn 定義。代碼示例安全

pub fn fizz_buzz(num: i32) -> String {
  if num % 15 == 0 {
    return "fizzbuzz".to_string();
  } else if num % 3 == 0 {
    return "fizz".to_string();
  } else if num % 5 == 0 {
    return "buzz".to_string();
  } else {
    return num.to_string();
  }
}

fn main () {
  assert_eq!(fizz_buzz(15), "fizzbuzz".to_string());
  assert_eq!(fizz_buzz(3), "fizzbuzz".to_string());
  assert_eq!(fizz_buzz(5), "buzz".to_string());
  assert_eq!(fizz_buzz(13), "13".to_string());
}
複製代碼

函數解析: fn 關鍵字定義了 fizz_buzz 函數,其函數簽名pub fn fizz_buzz(num: i32) -> String 反應函數的類型約定: 傳入 i32 類型,返回 String 類型。Rust 編譯器會嚴格遵照此類型的契約,若是傳入或返回類型不對,則編譯時報錯。markdown

做用域與生命週期

Rust 語言的做用域是靜態做用域,即詞法做用域(Lexocal Scope),由一對花括號來開闢,其做用域在詞法分析階段就已經肯定了,不會動態改變。示例以下閉包

fn main () {
  let v = "hello world!";
  assert_eq!(v, "hello world!");
  let v = "hello Rust!";
  assert_eq!(v, "hello Rust!");
  {
    let v = "Hello World!"
    assert_eq!(v, "Hello World!");
  }
  assert_eq!(v, "hello Rust!");
}
複製代碼

首先聲明 v 變量,賦值爲 hello world!, 此時斷言驗證其值,再次經過 let 聲明變量綁定 v 賦值爲 hello Rust! 這種連續定義同名變量的作法叫作變量遮蔽(Variable Shadow),最終變量 v 的值是由第二個變量定義所決定的。函數

函數指針

在 Rust 中,函數爲一等公民,意味着函數自身就能夠做爲函數的參數和返回值使用。示例:學習

fn sum(a: i32, b: i32) -> i32 {
  a + b
}

fn product(a: i32, b: i32) -> i32 {
  a * b
}

fn main () {
  let a = 2;
  let b = 3;
  assert_eq!(math(sum, a, b), 5);
  assert_eq!(math(product, a, b), 6);
}
複製代碼

在 main 函數中,調用了 math 函數兩次,分別傳入了 sum 和 product 做爲參數。而 sum 和 product 分別是用於求和求積的兩個函數,他們的類型分別是fn (i32, i32) -> i32 ,因此能夠做爲參數傳給 math 函數。這裏直接使用函數的名字來做爲函數指針。spa

fn is_true -> bool {true}
fn true_maker() -> fn() -> bool{is_true}
fn main() {
  assert_eq!(true_maker()(), true);
}
複製代碼

定義函數中 is_true, 返回true ,還定義了函數 true_maker,返回 fn() -> bool 類型,器函數體內直接將 is_true 函數指針返回,此處函數名稱做爲函數指針,若是加上括號,表明調用此函數。 在 main 函數的斷言中, true_maker()()調用至關於(true_maker())() 首先調用 true_maker() ,會返回 is_true 函數指針,而後再調用 is_true()函數,最終獲得 true指針

CTFE 機制

Rust 編譯器也能夠像 C++ 或者 D 語言同樣,擁有編譯時函數執行(Compile-time Function Execution, CTFE)能力。示例爲 const fncode

// #![feature(const_fn)]

const fn init_len() -> usize {
  return 5;
}

fn main () {
  let arr = [0, init_len()];
}

複製代碼

代碼中,使用了 const fn 來定義函數 init_len ,該函數返回一個固定值 5 。而且在 main 函數中,經過 [0; N] 的方式初始化初始值爲 0、長度爲 N 的數組,其中 N 是調用函數 init_len 來求得的。 Rust 固定長度數組必須在編譯期就知道長度,不然就會編譯出錯,因此 init_len必須在編譯期求值。這就是 CTFE 的能力。 使用 const fn 定義的函數,必須是可肯定值,不能存在歧義。與 fn 定義函數的區別在於,const fn 能夠強制編譯器在編譯期執行函數。其中關鍵字 const 通常用於定義全局變量。orm

除了 const fn 以外,官方還在實現 const generics 特性。支持 const generics 特性,將能夠實現相似 impl <T, const N: unsize> Foo for[T;N] {…} 的代碼,能夠爲全部長度的數組實現 trait Foo。可以使得使用數組的體驗獲得很大提高。

Rust 中的 CTFE 是有 miri 來執行的。miri 是一個 MIR 解釋器,目前已經被集成到了 Rust 編譯器 rustc 中。Rust 編譯器目前能夠支持的常量表達式有: 字面量、元祖、數組、字段結構體、枚舉,只包含但行代碼的塊表達式、範圍等。Rust 想要擁有完善的 CTFE 支持,還須要不少工做要作。

閉包

閉包也稱匿名函數,閉包有一下幾個特色: * 能夠像函數同樣被調用。 * 能夠駁貨上下文環境中的自由變量。 * 能夠自動推斷輸入和返回的類型。

fn main () {
  let out = 42;
  // fn add (i: i32, j: i32) -> i32 {i + j + out}

  fn add (i: i32, j: i32) -> i32 { i + j}

  let closure_annotated = |i: i32, j: i32| -> i32 {i + j + out}

  let closure_inferred = |i, j| i + j + out;
  
  let i = 1;
  let j = 2;

  assert_eq!(3, add(i, j));
  assert_eq!(45, closure_annotated(i, j));
  assert_eq!(45, closure_inferred(i, j));
}

複製代碼

上述代碼中,main 函數中定義了另一個函數 add, 以及兩個閉包 closure_annotated 和 closure_inferred。 閉包調用和函數調用很是想,可是閉包合函數有一個重要的區別,就是閉包能夠捕獲外部變量,而函數不能。這與 JavaScript 有很大的差異。JavaScript 函數自己就能夠獲取外部變量,只是獲取到外部變量不必定就是閉包。 閉包也能夠做爲函數參數和返回值,可是用起來略有區別。

fn closure_math<F: Fn() -> i32>(op: F) -> i32 {
  op()
}

fn main () {
  let a = 2;
  let b = 3;

  assert_eq!(closure_math(|| a + b), 5);
  assert_eq!(closure_math(|| a * b), 6);
}
複製代碼

上述代碼中,定義了函數 closure_math,其參數是一個泛型 F, 而且該泛型 Fn() -> i32 trait 的限定,表明該函數只容許實現 Fn() -> i32 trait 的類型做爲參數。 Rust 中的閉包實際上就是由一個匿名結構體和 trait 來組合實現的。因此,在 main 函數調用 math 函數時,分別傳入 || a + b|| a * b 均可以實現 Fn() -> i32。 在 math 函數內部,直接調用傳入閉包。

閉包做爲返回值

fn tow_times_impl() -> impl Fn(i32) -> i32 {
  let i = 2;
  move |j| j * i
}
fn main () {
  let result = tow_times_impl();
  assert_eq!(result(2), 4);
}
複製代碼

在上述代碼中,使用了 impl Fn(i32) -> i32 做爲函數的返回值,他表示實現 Fn(i32) -> i32 的類型,在函數定義是並不知道具體的返回類型,可是在函數調用時,編譯器會推斷出來。這個過程是零成本抽象的,一切都發生在編譯期。

須要注意的是,two_times_impl 中最後返回閉包時使用了 move 關鍵字,這是由於通常狀況下,閉包默認會按引用捕獲變量,若是將閉包返回,則引用也會跟着返回,可是在整個函數調用完畢後,函數內的本地變量 i 就會被銷燬,那麼隨着閉包返回的變量 i 的引用,也就成了懸垂指針。 Rust 是注重內存安全的語言,若是不適用 move 關鍵字,編譯器會報錯。 使用 move 關鍵字,將捕獲變量 i 的全部權轉移到閉包中,就不會按引用進行捕獲變量,閉包纔可安全返回。

#Rust學習

相關文章
相關標籤/搜索