【Rust】螃蟹一呀爪八個,兩頭尖尖,這麼大的個…

前言

雖然聽說Rust的語法很難,但是筆者為什麼會想要學呢?主要是看到了這個APP - BongoCat,是一個滿有趣的鍵盤互動功能,主要是利用Tauri - Rust + Web去改寫這個用C#寫的Bongo-Cat-Mver,讓它能跑在windows / linux / macOS上,事不宜遲,我們馬上行動…

作業環境

項目 版本
macOS Sequoia 15.5
Visual Studio Code 1.100.2
Rust 1.87

介紹

安裝

  • 首先當然是在Rust官網上下載安裝檔是最簡單的…
  • 在macOS上也可以使用homebrew去做安裝的動作…
  • 安裝完成後,也可以看看它的版本是多少…
brew install rust
rustc --version
cargo --version

Hello, World!

  • 學程式語言的第一課,好像約定俗成都一定要來個Hello, World!,那我們也來寫一個吧,就叫做hello.rs…
  • 可以用指令執行,或者在VSCode左上有個Run的按鍵,用它也可以執行…

/// hello.rs
fn main() {
    println!("Hello, world!");
}
rustc hello.rs
./hello

建立新專案

  • 當然,一般在用的話,一定不會是只有一個檔案的,這時候就要靠cargo來產生一個新專案了…
  • 就在桌面上建立一個叫rust_first的專案,然後用VSCode打開它…
cd ~/Desktop
cargo new rust_first
code rust_first
cargo run --bin rust_first
cargo new
cargo build
cargo check
cargo clean
cargo doc --open
cargo tree

做一個文字檔存取功能

255
  

250
  

250
  

snow
    

248
  

248
  

255
  

ghostwhite
.
.
.
  • 程式碼如下,內容就不細說了,語法還滿像C++的,就看看註解吧…
use std::fs::File;                          // 可以一個一個單獨use
use std::io::{BufRead, BufReader, Write};   // 也可以很多個use

fn main()  -> std::io::Result<()> {
    table_to_csv("file/Color.txt", "file/Color.csv")?;
    Ok(())
}

fn table_to_csv(txt_file: &str, csv_file: &str) -> std::io::Result<()> {

    let file = File::open(txt_file)?;           // 開啟檔案位置 (有錯就return Err()) => let 常數
    let reader = BufReader::new(file);          // 建立讀取器 (一行一行讀)
    let mut output = File::create(csv_file)?;   // 建立要存的檔案路徑 (要存在哪裡?) => let mut 變數
    let mut current_line = Vec::new();          // 把讀到的內容轉成 [255,250,250,snow]

    // 把讀到的內容轉成 [255,250,250,snow] 的過程 => 一行一行存到CSV中
    // (空的不要 / 是色碼數字就記起來 / 記下最後的顏色名稱)
    for line in reader.lines() {

        let line = line?.trim().to_string();

        if line.is_empty() { continue; }
        if line.parse::<u8>().is_ok() { current_line.push(line); continue; }
        if current_line.is_empty() { continue; }

        current_line.push(line);

        let color_values = current_line.join(",");
        writeln!(output, "{}", color_values)?;
        current_line.clear();
    }

    Ok(())
}
  • 比較要說明的是「Result的語法糖:?」,不過還真的是滿好用的…
  • 其實跟Swift的Result是一樣的,成功和失敗二選一…
// 就是 let file = File::open(txt_file)?
fn _result_demo_(txt_file: &str) -> std::io::Result<()> {

    let result = File::open(txt_file);

    match result {
        Err(error) => {
            print!("{}" , error);
            return Err(error)
        }
        Ok(file) => {
            let size = file.metadata()?.len();
            println!("檔案位址: {:p}, 檔案大小: {} bytes" , &file, size);
        }
    }

    // if result.is_err() {
    //     println!("無法開啟檔案:{}", txt_file);
    //     return Err(result.unwrap_err());
    // }

    // let new_file = result.unwrap();

    Ok(())
}

CSV to JSON

  • 接下來我們要把轉出來的CSV,再把它轉成JSON
  • 雖然CSV容易編譯,但是JSON還是比較容易在網路上傳遞,而且可以知道有沒有輸入到重複的名稱…
  • 我們把CSV以逗點分開,取得RGB數值與名稱,再使用serde / serde_json這個JSON套件,來包裝RGB數值,最後就存起來…
  • 記得在Cargo.toml要加上要使用的套件設定…
  • 也可以利用cargo tree指令來查看該專案裝了什麼套件…
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
cargo tree
  • 部分程式碼如下,其實語法也算是滿直覺的…
fn main()  -> std::io::Result<()> {
    table_to_csv("file/Color.txt", "file/Color.csv")?;
    csv_to_json("file/Color.csv","file/Color.json")?;
    Ok(())
}

fn table_to_csv(txt_file: &str, csv_file: &str) -> std::io::Result<()> {}

fn csv_to_json(csv_file: &str, json_file: &str) -> std::io::Result<()> {

    let file = File::open(csv_file)?;
    let reader = BufReader::new(file);
    let mut color_map = serde_json::Map::new(); // 儲存{<名稱>:<GRB顏色>}
    let mut output_file: File = File::create(json_file)?;

    for line in reader.lines() {
        
        let line = line?;
        let parts: Vec<&str> = line.split(',').collect();   // 255,250,250,snow => [255,250,250,snow]
        
        if parts.len() != 4 { continue; }

        let (red, green, blue, name) = match (parts.get(0), parts.get(1), parts.get(2), parts.get(3)) {
            (Some(r), Some(g), Some(b), Some(n)) => (r, g, b, n), 
            _ => continue
        };

        // 儲存RGB數值,不是0~255就轉成0
        let rgb = json!({
            "red": red.parse::<u8>().unwrap_or(0),
            "green": green.parse::<u8>().unwrap_or(0),
            "blue": blue.parse::<u8>().unwrap_or(0)
        });

        color_map.insert(name.to_string(), rgb);
    }

    write!(output_file, "{}", serde_json::to_string_pretty(&color_map)?)?;  // 寫入 JSON 檔案
    
    println!("JSON 檔案已建立:{}", json_file);
    Ok(())
}
  • 比較要解說的是Optional與match的配合用法…
  • 其實也是跟Swift的Optional很相像,天下文章都是一大抄嘛…
fn _optional_demo_(array: Vec<&str>) -> std::io::Result<&str> {

    let red = match array.get(0) {
        Some(red) => red,
        None => return Err(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "陣列是空的"
        ))
    };

    return Ok(red);
}

JSON to HexString

  • 最後我們來將RGB色碼,轉成16位元的色碼…
  • 為什麼要這麼做呢?因為在JSON Editor Online這個網路IDE上,它可以顯示16位元色碼的顏色,非常的方便、直覺啊…
  • 部分程式碼如下…
fn main()  -> std::io::Result<()> {
    table_to_csv("file/Color.txt", "file/Color.csv")?;
    csv_to_json("file/Color.csv","file/Color.json")?;
    json_to_hex("file/Color.json", "file/HexColor.json")?;
    Ok(())
}

fn table_to_csv(txt_file: &str, csv_file: &str) -> std::io::Result<()> {}

fn csv_to_json(csv_file: &str, json_file: &str) -> std::io::Result<()> {}

fn json_to_hex(input_json: &str, output_json: &str) -> std::io::Result<()> {

    let json_str = std::fs::read_to_string(input_json)?;
    let colors: Value = serde_json::from_str(&json_str)?;
    let mut hex_colors = serde_json::Map::new();

    let object = match colors.as_object() {
        Some(obj) => obj,
        None => return Ok(())
    };

    for (name, color) in object {

        let red = color["red"].as_u64().unwrap_or(0) as u8;
        let green = color["green"].as_u64().unwrap_or(0) as u8;
        let blue = color["blue"].as_u64().unwrap_or(0) as u8;        
        let hex_string = format!("#{:02X}{:02X}{:02X}", red, green, blue);

        hex_colors.insert(name.to_string(), json!(hex_string));
    }
    
    // 寫入新的 JSON 檔案
    let mut output_file = File::create(output_json)?;
    write!(output_file, "{}", serde_json::to_string_pretty(&hex_colors)?)?;
    
    println!("已轉換為 16 進位色碼格式:{}", output_json);
    Ok(())
}

製作常用工具集

  • 身為WW系列的作者,常做工具集也是件很正常的事情嘛…
  • 而且Rust有impl功能,很像Swift的Extension,可以一個區塊一個區塊的去分類,非常的方便啊…
  • 在要轉成mod的資料夾之下,加一個mod.rs的檔案,這個資料夾就變成「模組」了,跟Python的init.py滿像的…
  • 然後再mod.rs中,加入要成為mod的檔名…
pub mod file;
pub mod string;
  • 我們做一個文字小工具,使用struct,加入一個RGB色碼轉16進位色碼的功能…
  • 其中有靜態方法是像是一般的fn一樣,用::呼叫的…
  • 而實例方法的參數會多出一個&self,用.呼叫,這也跟Python滿像的…
pub struct StringManager;

// MARK: - static
impl StringManager {

    /// RGB色碼轉16進位色碼 => (255, 248, 240) -> #F0F8FF
    /// ## 參數
    /// - `red` - 紅色色碼 (0~255)
    /// - `green` - 綠色色碼 (0~255)
    /// - `blue` - 藍色色碼 (0~255)
    /// ## 回傳
    /// - `String` - 16進位色碼
    pub fn hex_string(red: u8, green: u8, blue: u8) -> String {
        format!("#{:02X}{:02X}{:02X}", red, green, blue)
    }
}

// MARK: - class
impl StringManager {

    /// RGB色碼轉16進位色碼 => (255, 248, 240) -> #F0F8FF
    /// ## 參數
    /// - `&self` - 實例方法
    /// - `red` - 紅色色碼 (0~255)
    /// - `green` - 綠色色碼 (0~255)
    /// - `blue` - 藍色色碼 (0~255)
    /// ## 回傳
    /// - `String` - 16進位色碼
    pub fn _hex_string(&self, red: u8, green: u8, blue: u8) -> String {
        return format!("#{:02X}{:02X}{:02X}", red, green, blue);
    }
}
  • 大家可以試試看,製作自己的工具集…
mod utility;
use utility::string::StringManager;

fn main() {

    let manager = StringManager;
    let static_hex = StringManager::hex_string(0, 0, 0);
    let hex_string = manager._hex_string(255, 240, 235);

    print!("{} {}", static_hex, hex_string);
}

同場加映 - Extension

  • 有時想想,如果Rust能像Swift一般,在一般的類別加上fn就好了啊…
  • 當然有~~~,就是使用trait
  • 做法上跟使用struct差不多,也是要加入mod.rs,只是換成trait關鍵字…
  • 其中where跟Swift的很像,就是要符合後面的Protocol,果然晚出生的語言就是有這個好處,語法上很現代化…
  • 我們這邊要為&str加上更多的功能,所以要加入FromStr這個trait…
  • 話不多說,我們就加上「是不是數字?」跟「轉成數字」的功能…
use std::str::FromStr;
use std::fmt::Debug;

pub trait StringExtension {

    fn is_number<T>(&self) -> bool where T: FromStr + Debug + PartialOrd + Copy;
    fn _number<T>(&self) -> Option<T> where T: FromStr + Debug + PartialOrd + Copy;
}

impl<S> StringExtension for S where S: AsRef<str> {

    /// 判斷文字是否能轉成數字?
    /// ## 回傳
    /// - `bool` - 是否能轉成數字
    fn is_number<T>(&self) -> bool where T: FromStr + Debug + PartialOrd + Copy {
        
        let this = self.as_ref();

        if this.is_empty() { return false; }
        
        match this.parse::<T>() {
            Ok(_) => true,
            Err(_) => false
        }
    }

    /// 文字 => 數字
    /// ## 回傳
    /// - `Option<T>` - 數字
    fn _number<T>(&self) -> Option<T> where T: FromStr + Debug + PartialOrd + Copy {

        let this = self.as_ref();

        if this.is_empty() { return None; }

        match this.parse::<T>() {
            Ok(num) => Some(num),
            Err(_) => None
        }
    }
}
  • 再整理一下程式碼,完工…
use std::fs::File;                          // 可以一個一個單獨use
use std::io::{BufRead, BufReader, Write};   // 也可以很多個use
use serde_json::{json, Value};              // 引用外部套件

mod utility;
use utility::string::StringManager;

mod extension;
use extension::string::StringExtension;

fn main()  -> std::io::Result<()> {
    table_to_csv("file/Color.txt", "file/Color.csv")?;
    csv_to_json("file/Color.csv","file/Color.json")?;
    json_to_hex("file/Color.json", "file/HexColor.json")?;
    Ok(())
}

/// # 將txt => csv
/// ## 參數
/// - `txt_file` - txt檔案路徑
/// - `csv_file` - csv檔案路徑
/// ## 回傳
/// - `std::io::Result<()>` - 檔案內容或錯誤
fn table_to_csv(txt_file: &str, csv_file: &str) -> std::io::Result<()> {

    let file = File::open(txt_file)?;           // 開啟檔案位置 (有錯就return error) => let 常數
    let reader = BufReader::new(file);          // 建立讀取器 (一行一行讀)
    let mut output = File::create(csv_file)?;   // 建立要存的檔案路徑 (要存在哪裡?) => let mut 變數
    let mut current_line = Vec::new();          // 把讀到的內容轉成 [255,250,250,snow]

    // 把讀到的內容轉成 [255,250,250,snow] 的過程 => 一行一行存到CSV中
    // (空的不要 / 是色碼數字就記起來 / 記下最後的顏色名稱)
    for line in reader.lines() {

        let line = line?.trim().to_string();

        if line.is_empty() { continue; }
        if line._is_number::<u8>() { current_line.push(line); continue; }
        if current_line.is_empty() { continue; }

        current_line.push(line);

        let color_values = current_line.join(",");
        writeln!(output, "{}", color_values)?;
        current_line.clear();
    }

    Ok(())
}

/// # 將csv => json
/// ## 參數
/// - `csv_file` - csv檔案路徑
/// - `json_file` - json檔案路徑
/// ## 回傳
/// - `std::io::Result<()>` - 檔案內容或錯誤
fn csv_to_json(csv_file: &str, json_file: &str) -> std::io::Result<()> {

    let file = File::open(csv_file)?;
    let reader = BufReader::new(file);
    let mut color_map = serde_json::Map::new(); // 儲存{<名稱>:<GRB顏色>}
    let mut output_file: File = File::create(json_file)?;

    for line in reader.lines() {
        
        let line = line?;
        let parts: Vec<&str> = line.split(',').collect();   // 255,250,250,snow => [255,250,250,snow]
        
        if parts.len() != 4 { continue; }

        let (red, green, blue, name) = match (parts.get(0), parts.get(1), parts.get(2), parts.get(3)) {
            (Some(r), Some(g), Some(b), Some(n)) => (r, g, b, n), 
            _ => continue
        };

        // 儲存RGB數值,不是0~255就轉成0
        let rgb = json!({
            "red": red._number::<u8>().unwrap_or(0),
            "green": green._number::<u8>().unwrap_or(0),
            "blue": blue._number::<u8>().unwrap_or(0)
        });

        color_map.insert(name.to_string(), rgb);
    }

    write!(output_file, "{}", serde_json::to_string_pretty(&color_map)?)?;  // 寫入 JSON 檔案
    
    println!("JSON 檔案已建立:{}", json_file);
    Ok(())
}

/// # 將json重組成有hex顏色的json
/// ## 參數
/// - `input_json` - json檔案路徑
/// - `output_json` - json檔案路徑
/// ## 回傳
/// - `std::io::Result<()>` - 檔案內容或錯誤
fn json_to_hex(input_json: &str, output_json: &str) -> std::io::Result<()> {

    let json_str = std::fs::read_to_string(input_json)?;
    let colors: Value = serde_json::from_str(&json_str)?;
    let mut hex_colors = serde_json::Map::new();

    let object = match colors.as_object() {
        Some(obj) => obj,
        None => return Ok(())
    };

    for (name, color) in object {

        let red = color["red"].as_u64().unwrap_or(0) as u8;
        let green = color["green"].as_u64().unwrap_or(0) as u8;
        let blue = color["blue"].as_u64().unwrap_or(0) as u8;        
        let hex_string = StringManager::hex_string(red, green, blue);

        hex_colors.insert(name.to_string(), json!(hex_string));
    }

    // 寫入新的 JSON 檔案
    let mut output_file = File::create(output_json)?;
    write!(output_file, "{}", serde_json::to_string_pretty(&hex_colors)?)?;
    
    println!("已轉換為 16 進位色碼格式:{}", output_json);
    Ok(())
}

範例程式碼下載

後記

  • 會做這篇主要是要做這個套件 - WWGMTColor,因為色碼對前端人員來說,非常的常用,但是常常很難幫顏色取名字,什麼海棠紅啊,蒂芬妮藍啊,湖水綠啊,完全是個特色上的命名,很難去區別色碼,所以就做了一個公規的顏色工具,這樣以後跟色彩大師也有個基本的通溝基礎…
  • 現在正值AI興起的年代,會在討論區或社團問問題或討論的人越來越少了,彷彿AI是全知全能的神,學生也不太聽課了,老師好像可有可無,雖然能快速取得解答,但少了一點人情味,好像有點「抖音腦」的感覺,3秒取得解答,讓人們越來越欠缺思考,越來越沒耐心,什麼都說是AI告訴我的,也許…這也算是一種洗腦吧?