ลอง Rust Basic (1)

/ 14 min read

Share on social media

rust-basic-1 สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video

Rust คืออะไร

Rust เป็นภาษาโปรแกรมระบบที่พัฒนาขึ้นโดย Mozilla ซึ่งเป็นภาษาที่เน้นความปลอดภัยด้านหน่วยความจำและประสิทธิภาพสูง เหมาะสำหรับการเขียนโปรแกรมที่ต้องการจัดการกับหน่วยความจำแบบละเอียด เช่น การพัฒนา embedded systems, web assembly หรือระบบที่ต้องการประสิทธิภาพสูงแบบ native application

โดย Key features ใหญ่ๆของ Rust มีดังนี้

  1. Memory Safety: Rust มีระบบการจัดการหน่วยความจำที่ปลอดภัย โดยใช้ “Ownership” และ “Borrowing” เพื่อป้องกันปัญหาที่พบบ่อยในภาษาอื่นๆ เช่น null pointer dereferencing หรือ data race ซึ่งช่วยให้ code มีความปลอดภัยโดยไม่จำเป็นต้องใช้ Garbage Collector (GC) ได้
  2. Zero-Cost Abstractions: Rust อนุญาตให้เราใช้ abstraction (การสร้าง Layer ของ code เพื่อให้ใช้งานง่ายและอ่านได้ง่ายขึ้น เช่น iterators และ generics) โดยไม่ต้องเสียประสิทธิภาพของการทำงานลงไป (เนื่องจาก Rust จะทำการ compile ให้มีประสิทธิภาพเทียบเท่ากับ low-level constructs) ซึ่งต่างจากบางภาษา เช่น Python หรือ JavaScript ที่ abstraction ระดับสูงอาจทำให้ code ทำงานช้าลงเพราะมีค่าใช้จ่ายในการแปลง abstraction เหล่านั้นให้กลายเป็น code ที่เครื่องอ่านได้
  3. Concurrency: Rust มีการจัดการ concurrency ที่ปลอดภัยจากปัญหาข้อมูลทับซ้อน (data race) โดยการใช้ ownership system เพื่อทำให้ code ที่เกี่ยวข้องกับ multithreading มีความปลอดภัยในการเข้าถึงข้อมูลพร้อมกัน

ทีนี้ Rust เองก็ได้เตรียม Resource มาให้เราได้เรียนรู้ไว้แล้วเช่นกัน ซึ่งในหัวข้อนี้เราก็จะมาลองเรียน Rust ตาม Rust Book กันโลดด (หลายเนื้อหาสามารถอ่านเพิ่มเติมจาก Rust Book ได้เช่นกัน เรียกว่าเขียนไว้ละเอียดยิบแล้วเรียบร้อย)

https://doc.rust-lang.org/book/title-page.html

โดยเพื่อให้เกิดความเข้าใจที่ถ่องแท้ในแต่ละประเด็น เราจะขอแบ่ง Rust ออกเป็น 2 Part ใหญ่ๆ (ทั้งในบทความนี้และฉบับ video) คือ

  • Basic 1 = พื้นฐานทั่วไปทางภาษาของ Rust (ทำให้เห็นภาพก่อนว่าภาษามันอารมณ์ไหน)
  • Basic 2 = เพิ่มคุณสมบัติพิเศษที่ทำให้ Rust implement ในรูปแบบต่างๆได้

Start Rust

https://doc.rust-lang.org/book/ch01-02-hello-world.html

เรามาเริ่มต้น hello world โดยการลองลง Rust กัน โดยสามารถลงผ่าน command นี้ได้

Terminal window
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

เมื่อลงเรียบร้อย ลอง run คำสั่ง rustc เพื่อตรวจสอบว่าสามารถใช้คำสั่ง compiler Rust ได้แล้วหรือไม่

Terminal window
rustc --version

ถ้าได้ผลแบบนี้ = ลงเสร็จแล้วเรียบร้อย

rust-01.webp

ลองสร้าง file main.rs แล้วเพิ่ม code นี้เข้าไป

fn main() {
println!("Hello, Mikelopster!");
}

ลอง run Rust ผ่าน command

Terminal window
# compile rust
rustc main.rs
# เมื่อ run เสร็จจะได้เป็น file executable file ที่ชื่อเดียวกันกับ
# ชื่อ file main.rs = main (หรือ main.exe ใน windows)
# run executable code
./main

ก็จะได้ผลลัพธ์ของ Rust ออกมาได้ = เท่านี้ก็ confirm ได้ว่าลง Rust เรียบร้อย และสามารถ compile code ได้ ไม่มีปัญหาอะไร

rust-02.webp

Note

  • file ที่ได้ออกมาคือ executable file คือไฟล์ที่บรรจุคำสั่งในรูปแบบ machine code ซึ่งจำเป็นสำหรับการรันโปรแกรม
  • Rust เป็นภาษาที่ใช้ การคอมไพล์ล่วงหน้า (ahead-of-time compiled language) หมายความว่า code จะถูก compile เป็น machine code ก่อนที่จะรันโปรแกรมจริง ซึ่งแตกต่างจากภาษา interpreted languages เช่น Ruby, Python หรือ JavaScript ซึ่ง code จะถูกตีความ (interpreted) และ run ทีละบรรทัดในระหว่าง runtime

Step ต่อมาเราจะลองใช้ Cargo new project กัน

รู้จักกับ Cargo

https://doc.rust-lang.org/book/ch01-03-hello-cargo.html

Cargo คือเครื่องมือ package manager และระบบ build สำหรับ Rust ที่ช่วยให้การสร้าง จัดการ dependencies และการทดสอบเป็นเรื่องง่ายขึ้น ในขณะที่ rustc (Rust compiler) เป็นตัว compile ที่แปลง code จาก Rust ไปเป็น binary file

ข้อดีของการใช้ Cargo

  • จัดการโครงการและสร้างโครงสร้างพื้นฐานอัตโนมัติ
  • จัดการ dependencies ได้ง่ายและอัตโนมัติ
  • Build และ run code ได้ในขั้นตอนเดียว
  • รองรับการทดสอบด้วยระบบในตัว
  • จัดการหลายเวอร์ชันของโปรเจกต์และ dependencies

เราสามารถเริ่ม project โดยการ run command cargo ได้ (ตัว cargo ได้ทำการลงไปพร้อมกันกับตอน rustc แล้วเรียบร้อย)

Terminal window
# command สร้าง project
cargo new <ชื่อ project>
# ตัวอย่าง
cargo new mikelopster_cargo

ผล command

rust-03.webp

เมื่อเข้าไปใน folder ที่สร้าวขึ้นมา ตัว cargo ก็จะ start project มาด้วย structure เริ่มต้นตามนี้

rust-04.webp

อย่างที่เห็นว่า มันจะมี file Cargo.toml ซึ่งเป็น file รวม dependency (ถ้าใครที่มาจากสาย javascript มันจะอารมณ์เหมือนๆกับ package.json) และจุดเริ่มต้นของ Rust จะเริ่มต้นที่ src/main.rs

[package]
name = "mikelopster_cargo"
version = "0.1.0"
edition = "2021"
[dependencies]

คำสั่งสำหรับ run ด้วย cargo

Terminal window
# คำสั่งสำหรับ build
cargo build
# คำสั่งสำหรับ run
cargo run
rust-05.webp

ก็จะได้ผลลัพธ์ที่เหมือนกันออกมาได้ โดยสามารถลง package ได้ผ่านคำสั่ง cargo install เช่น เราจะลองลง library ที่สามารถ watch file ได้ (file เปลี่ยน = build ใหม่ให้ auto)

Terminal window
# ลง library
cargo install cargo-watch
# ลอง run
cargo watch -x run

ผลลัพธ์

rust-06.webp

Common Programming Concept

https://doc.rust-lang.org/book/ch03-00-common-programming-concepts.html

เรามาเริ่มพื้นฐานแรกของ Rust กันนั่นคือ Core Programming Concept ที่มีอยู่ในทุกภาษาอย่าง Variable, Data Type, Control Flow กันว่า ภาษา Rust มีหลักการพื้นฐานทางภาษาอย่างไรบ้าง

Variable

Variable ใน Rust คือค่าที่ใช้เก็บข้อมูล โดยค่าในตัวแปรสามารถเปลี่ยนแปลงได้หากเราระบุให้ตัวแปรนั้นเป็น mutable (mut) มิฉะนั้นตัวแปรจะถือว่าเป็น “immutable (ค่าคงที่) โดยค่าเริ่มต้น”

โดย เหตุผลที่ Rust ใช้ immutable เป็นค่าเริ่มต้น นั้น เกิดจาก concept ของ Rust ที่กำหนดไว้ว่า

  1. เพิ่มความปลอดภัยของ code ป้องกันไม่ให้โปรแกรมทำการเปลี่ยนแปลงค่าของตัวแปรโดยไม่ตั้งใจ ซึ่งอาจทำให้เกิดข้อผิดพลาดที่ยากจะตรวจพบ
  2. ป้องกันการเข้าถึงข้อมูลพร้อมกันที่ไม่ปลอดภัย (Data Race) โดย Rust ให้ความสำคัญกับการเขียน code ที่รองรับการทำงานพร้อมกัน (concurrency) การใช้ตัวแปร immutable ทำให้มั่นใจได้ว่าหลาย ๆ thread สามารถอ่านค่าของตัวแปรได้พร้อมกัน โดยไม่ต้องกังวลว่าจะมีการเปลี่ยนแปลงค่าจาก thread อื่น

ถ้างั้น คำถามก็คือ สำหรับ Rust แล้ว “เมื่อไรที่ควรใช้ตัวแปรแบบ mutable ?”

ในกรณีที่เราต้องการเปลี่ยนค่าของตัวแปร เช่น การเก็บค่าใน loop หรือตัวแปรที่ต้องอัพเดตค่าใน runtime คุณสามารถใช้ mut เพื่อระบุว่าตัวแปรนั้นสามารถเปลี่ยนแปลงค่าได้

นี่คือตัวอย่างการประกาศ Variable แบบ Immutable และ Mutable

  • ตัวแปรแบบ Immutable
let x = 5;
println!("ค่าของ x: {}", x);
  • ตัวแปรแบบ Mutable (เปลี่ยนค่าได้)
let mut y = 10;
println!("ค่าเริ่มต้นของ y: {}", y);
y = 15;
println!("ค่าใหม่ของ y: {}", y);

Data Type

Data Type ใน Rust หมายถึงประเภทของข้อมูลที่ใช้เก็บค่าในตัวแปร ซึ่งเป็นสิ่งที่ช่วยกำหนดประเภทของข้อมูล เช่น ตัวเลข, ตัวอักษร, Boolean ฯลฯ Rust มีการจัดการประเภทข้อมูลอย่างเข้มงวด ซึ่งช่วยเพิ่มความปลอดภัยและประสิทธิภาพ

ประเภทของตัวแปร ใน Rust มีการแบ่งในแต่ละกลุ่ม โดยจะมี 2 กลุ่มหลักๆคือ

  1. Scalar Types (ประเภทข้อมูลเดี่ยว) เป็นประเภทข้อมูลที่แทนค่าหนึ่งค่า Rust มี 4 ชนิด คือ Integers, Floating-Point Numbers, Booleans, และ Characters
  • ตัวแปรชนิด Integer (จำนวนเต็ม) = Rust รองรับตัวแปร integer หลายประเภท เช่น i32, u8, i64 เป็นต้น
let a: i32 = 100;
let b: u8 = 255;
println!("a: {}, b: {}", a, b);
  • ตัวแปรชนิด Floating Point (จำนวนทศนิยม) เช่น f32, f64
let pi: f64 = 3.14159;
println!("ค่าของ pi: {}", pi);
  • ตัวแปรชนิด Boolean
let is_rust_fun: bool = true;
println!("Rust สนุกไหม: {}", is_rust_fun);
  • ตัวแปรชนิด Character
let letter: char = 'R';
println!("ตัวอักษร: {}", letter);
  1. Compound Types ประเภทข้อมูลที่ใช้จัดกลุ่มค่าหลาย ๆ ค่าเข้าด้วยกัน Rust มี Compound Types หลัก 2 ประเภท คือ Tuples และ Arrays
  • ตัวแปรชนิด Tuple = กลุ่มของค่า “หลายประเภท” สามารถเก็บค่าหลายตัวพร้อมกัน
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // สามารถ Destructuring ได้
println!("ค่าจาก tuple: {}, {}, {}", x, y, z);
  • ตัวแปรชนิด Array = เก็บค่าหลายค่าในลำดับเดียวกัน โดยต้องเป็น “ประเภทเดียวกัน”
let arr: [i32; 5] = [1, 2, 3, 4, 5];
println!("ค่าของ array: {}", arr[0]);

นอกจากประเภทข้อมูลพื้นฐานที่กล่าวมา Rust ยังมีประเภทข้อมูลเพิ่มเติมที่มาจาก Standard Library ซึ่งมักถูกใช้ในโปรแกรม เช่น Strings, Slice และ Vectors

  1. String Types ประเภทข้อความสำหรับเก็บอักขระหลายตัว ประเภท String มาจาก Standard Library ของ Rust ไม่ได้ถูกเขียนในภาษาโดยตรง
  • String เป็นประเภทข้อความที่ Growable (สามารถเพิ่มความยาวได้), Mutable (เปลี่ยนแปลงได้) และรองรับการเข้ารหัสแบบ UTF-8 ซึ่งเหมาะสำหรับการจัดเก็บข้อความที่ไม่สามารถทราบความยาวได้ในขณะ Compile Time
let mut s = String::from("Hello");
s.push_str(", World!");
  • &str (String Slice) เป็นการอ้างอิง String แบบความยาวคงที่ ไม่สามารถเปลี่ยนค่าได้ เช่น
// Slice = เป็นการอ้างอิง String แบบยาวคงที่ ไม่สามารถเปลี่ยนค่าได้
let greeting: &str = "Hello, Rust!";
  1. Slice (การอ้างอิงบางส่วนของ array) Slice เป็นการอ้างอิงบางส่วนของข้อมูล เช่นการอ้างอิงช่วงของ array
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..3]; // จะอ้างถึง [2, 3]
  1. Vector ใช้สำหรับจัดเก็บค่าหลายค่าที่สามารถปรับเปลี่ยนจำนวนได้
  • โดย Vec<T> เป็นประเภทข้อมูลใน Standard Library ที่รองรับค่าทุกประเภท หาก Vector ใดเก็บค่าประเภทเฉพาะ จะต้องระบุประเภทข้อมูลนั้นภายในเครื่องหมาย < > ตัวอย่างเช่น Vec<i32> หมายถึง Vector ที่เก็บค่าเฉพาะประเภท i32
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
for i in &v {
println!("{}", i);
}

Operator

Operator ใน Rust คือสัญลักษณ์ที่ใช้ในการดำเนินการกับตัวแปรหรือค่าต่าง ๆ เพื่อทำการคำนวณหรือตรวจสอบบางอย่าง เช่น การบวก การเปรียบเทียบ หรือการตรรกะ เป็นต้น Rust มีหลายประเภทของ operators เพื่อจัดการกับข้อมูลหลากหลายประเภท

ประเภทของ Operators ใน Rust

  1. Arithmetic Operators (ตัวดำเนินการทางคณิตศาสตร์) ใช้ในการคำนวณทางคณิตศาสตร์ระหว่างตัวเลข
  • + : บวก
  • - : ลบ
  • * : คูณ
  • / : หาร
  • % : หารเอาเศษ (modulo)
let sum = 5 + 10; // ผลลัพธ์: 15
let difference = 95.5 - 4.3; // ผลลัพธ์: 91.2
let product = 4 * 30; // ผลลัพธ์: 120
let quotient = 56.7 / 32.2; // ผลลัพธ์: 1.76
let remainder = 43 % 5; // ผลลัพธ์: 3
  1. Comparison Operators (ตัวดำเนินการเปรียบเทียบ) ใช้เปรียบเทียบค่าระหว่างตัวแปรหรือค่าต่าง ๆ และให้ผลลัพธ์เป็น true หรือ false
  • == : เท่ากับ
  • != : ไม่เท่ากับ
  • > : มากกว่า
  • < : น้อยกว่า
  • >= : มากกว่าหรือเท่ากับ
  • <= : น้อยกว่าหรือเท่ากับ
let a = 5;
let b = 10;
let is_equal = a == b; // ผลลัพธ์: false
let is_greater = b > a; // ผลลัพธ์: true
  1. Logical Operators (ตัวดำเนินการทางตรรกะ) ใช้ในการทำงานกับค่าบูลีน (true หรือ false)
  • && : และ (AND)
  • || : หรือ (OR)
  • ! : ไม่ (NOT)
let t = true;
let f = false;
let result_and = t && f; // ผลลัพธ์: false
let result_or = t || f; // ผลลัพธ์: true
let result_not = !t; // ผลลัพธ์: false
  1. Bitwise Operators (ตัวดำเนินการระดับบิต) ใช้ในการทำงานกับข้อมูลในระดับบิต
  • & : AND บิต
  • | : OR บิต
  • ^ : XOR บิต
  • << : เลื่อนบิตไปทางซ้าย
  • >> : เลื่อนบิตไปทางขวา
let a = 2; // 10 ในฐาน 2
let b = 3; // 11 ในฐาน 2
let result_and = a & b; // ผลลัพธ์: 2 (10 ในฐาน 2)
let result_or = a | b; // ผลลัพธ์: 3 (11 ในฐาน 2)
let result_xor = a ^ b; // ผลลัพธ์: 1 (01 ในฐาน 2)
  1. Assignment Operators (ตัวดำเนินการกำหนดค่า) ใช้ในการกำหนดค่าลงในตัวแปร
  • = : กำหนดค่า
  • += : บวกและกำหนดค่า
  • = : ลบและกำหนดค่า
  • = : คูณและกำหนดค่า
  • /= : หารและกำหนดค่า
  • %= : หารเอาเศษและกำหนดค่า
let mut x = 5;
x += 2; // ตอนนี้ x มีค่าเป็น 7
x *= 3; // ตอนนี้ x มีค่าเป็น 21
  1. Range Operators (ตัวดำเนินการช่วง) ใช้ในการกำหนดช่วงตัวเลข
  • .. : ไม่รวมค่าปลายทาง
  • ..= : รวมค่าปลายทาง
for i in 0..5 {
println!("{}", i); // ผลลัพธ์: 0, 1, 2, 3, 4
}
for i in 0..=5 {
println!("{}", i); // ผลลัพธ์: 0, 1, 2, 3, 4, 5
}

Function

Function ใน Rust คือกลุ่มของ code ที่ถูกรวมไว้ด้วยกันเพื่อทำงานบางอย่าง โดยสามารถรับค่า input (parameter) และส่งผลลัพธ์ออกมาได้ (return value) การใช้ function ช่วยในการจัดระเบียบ code ลดการทำงานซ้ำ และทำให้ code อ่านง่ายขึ้น

การประกาศ function ใน Rust ใช้คำสั่ง fn ตามด้วยชื่อ function ตามด้วยพารามิเตอร์ (ถ้ามี) และประเภทของค่าที่จะส่งกลับ (ถ้ามี)

fn function_name(parameter1: type1, parameter2: type2) -> return_type {
// code ภายใน function
return_value
}
  • fn: คำสั่งที่ใช้ในการประกาศ function
  • **ชื่อ function **: ต้องเป็นตัวพิมพ์เล็กและใช้ underscore สำหรับการแบ่งคำ เช่น calculate_sum
  • Parameters : รับค่าเข้ามาใน function กำหนดชนิดของข้อมูลให้ชัดเจน
  • > return_type: ประเภทของค่าที่ function จะส่งกลับ (ถ้ามี)
  • ค่าที่จะส่งกลับ: ค่าที่ถูกส่งออกจาก function สามารถระบุได้ในบรรทัดสุดท้ายของ function โดยไม่ต้องใช้คำสั่ง return (ในบรรทัดสุดท้ายไม่จำเป็นต้องใส่ ;)

ตัวอย่างในการประกาศและการใช้งาน Function

  • Function ไม่มีพารามิเตอร์และไม่ส่งค่ากลับ
fn greet() {
println!("Hello, world!");
}
fn main() {
greet(); // เรียกใช้ function
}
  • Function รับพารามิเตอร์
fn print_sum(a: i32, b: i32) {
println!("ผลรวมของ {} และ {} คือ {}", a, b, a + b);
}
fn main() {
print_sum(5, 10); // ผลลัพธ์: ผลรวมของ 5 และ 10 คือ 15
}
  • Function ที่มีการส่งค่ากลับ
fn square(x: i32) -> i32 {
x * x // ไม่มีเครื่องหมาย ; ในบรรทัดสุดท้าย เพราะต้องการส่งค่ากลับ
}
fn main() {
let result = square(4); // result จะมีค่าเป็น 16
println!("ผลลัพธ์: {}", result);
}
  • Function ที่มีหลายพารามิเตอร์และส่งค่ากลับ
fn add(a: i32, b: i32) -> i32 {
return a + b; // ใช้ return อย่างชัดเจน (ไม่จำเป็นต้องใช้ในบรรทัดสุดท้าย)
}
fn main() {
let sum = add(3, 7);
println!("ผลรวมคือ {}", sum); // ผลลัพธ์: ผลรวมคือ 10
}
  • Function ที่ไม่มีค่าที่ส่งกลับ (ใช้ () หรือ “unit”) หาก function ไม่ส่งค่ากลับ function นั้นจะมีประเภทการส่งกลับเป็น () หรือที่เรียกว่า “unit type” ใน Rust
fn no_return() {
println!("This function does not return a value.");
}

คุณสมบัติของ Function ใน Rust

  • ความปลอดภัยในหน่วยความจำ: Rust ตรวจสอบความถูกต้องของการส่งคืนค่าและการจัดการหน่วยความจำ ทำให้มั่นใจได้ว่า function ทำงานได้อย่างถูกต้อง
  • ไม่มีการจัดการค่าที่เป็น Null: Rust ไม่อนุญาตให้มี null ในการส่งค่ากลับจาก function ซึ่งช่วยลดข้อผิดพลาดจากการใช้ค่าที่เป็น Null
  • สามารถใช้ function แบบ Recursion ได้: function สามารถเรียกตัวเองซ้ำได้ (recursive functions) เพื่อแก้ปัญหาบางประเภทที่ซับซ้อน

Control Flow

Control Flow ใน Rust เป็นโครงสร้างที่ช่วยให้โปรแกรมของคุณตัดสินใจเลือกแนวทางการทำงานตามเงื่อนไขต่าง ๆ ตัวอย่างที่นิยมใช้คือ if, else if, else และ match โดยแต่ละโครงสร้างมีรูปแบบการใช้งานแตกต่างกันเล็กน้อย

  • if, else if, และ else
fn main() {
let number = 7;
if number < 5 {
println!("The number is less than 5");
} else if number == 5 {
println!("The number is equal to 5");
} else {
println!("The number is greater than 5");
}
}

จาก code นี้ โปรแกรมจะตรวจสอบว่า number มีค่าเท่าไร ถ้า number < 5 จะพิมพ์ข้อความแรก ถ้าเท่ากับ 5 จะพิมพ์ข้อความที่สอง และถ้ามากกว่า 5 จะพิมพ์ข้อความสุดท้าย

  • match เป็นโครงสร้างควบคุมที่ทรงพลังใน Rust มันทำงานคล้ายกับ switch ในภาษาอื่น ๆ แต่สามารถจัดการเงื่อนไขได้หลากหลายแบบมากกว่า
fn main() {
let number = 3;
match number {
1 => println!("The number is 1"),
2 => println!("The number is 2"),
3 => println!("The number is 3"),
_ => println!("The number is something else"),
}
}

match จะตรวจสอบค่า number และดำเนินการตามเงื่อนไขที่ตรงกัน ถ้าไม่ตรงกับกรณีใด ๆ จะใช้ _ เป็นตัวแทนของ “ค่าอื่น ๆ” และพิมพ์ข้อความสุดท้าย

รวมถึง ใน Rust เองเราสามารถใช้ match เพื่อ return ค่ากลับมาจากแต่ละเงื่อนไขได้ แล้วนำค่าที่ return มาเก็บไว้ในตัวแปรใหม่ เช่น

fn main() {
let number = 2;
let result = match number {
1 => "one",
2 => "two",
3 => "three",
_ => "other",
};
println!("The result is: {}", result);
}

Loop

loop เป็นคำสั่งที่ช่วยให้เราทำการวนซ้ำ (loop) การทำงานใน แนกำ โดยมีรูปแบบการใช้งานที่หลากหลาย เช่น loop, while, และ for แต่ละแบบมีจุดประสงค์ที่แตกต่างกันเล็กน้อย

  • loop เป็นการวนซ้ำอย่างไม่สิ้นสุดจนกว่าจะมีคำสั่ง break เพื่อออกจากลูป
fn main() {
let mut count = 0;
loop {
count += 1;
println!("Count is: {}", count);
if count == 5 {
break;
}
}
println!("Loop finished");
}

ผลลัพธ์

Count is: 1
Count is: 2
Count is: 3
Count is: 4
Count is: 5
Loop finished
  • while ทำการวนลูปจนกว่าเงื่อนไขที่กำหนดจะเป็นเท็จ
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("Liftoff!");
}

ผลลัพธ์

3!
2!
1!
Liftoff!
  • for ใช้เพื่อวนลูปผ่านค่าหรือช่วงของตัวเลข
fn main() {
for number in 1..4 {
println!("The number is: {}", number);
}
println!("Done!");
}

ผลลัพธ์

The number is: 1
The number is: 2
The number is: 3
Done!

Ownership, Borrowing, and References

https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html

ทีนี้ หลายคนก็อาจจะสงสัยว่า “Garbage Collector คืออะไร และทำไม Rust ไม่ต้องใช้ Garbage Collector ได้”

  • Garbage Collector (GC) คือโปรแกรมที่ทำหน้าที่จัดการการจัดสรรและคืนหน่วยความจำในภาษาโปรแกรมต่าง ๆ โดยในภาษาที่มี GC นักพัฒนาไม่จำเป็นต้องจัดการคืนหน่วยความจำเองหลังจากใช้งานเสร็จ เนื่องจาก GC จะทำการระบุและเรียกคืนหน่วยความจำที่ไม่ได้ใช้งานโดยอัตโนมัติ ซึ่งช่วยป้องกันปัญหา memory leaks (การสูญเสียหน่วยความจำโดยไม่จำเป็น)
  • แต่ อย่างไรก็ตาม Rust ไม่มีระบบ Garbage Collector แต่ใช้กลไกที่เรียกว่า ownership ซึ่งเป็นคุณสมบัติเฉพาะตัวของ Rust ที่ช่วยรับประกันความปลอดภัยของหน่วยความจำโดยไม่ต้องพึ่งพา GC
  • การทำงานของระบบ Ownership ใน Rust จะมีไอเดียหลักประมาณนี้
    • แต่ละค่าจะมีตัวแปรหนึ่งตัวที่เป็นเจ้าของ (owner)
    • ค่าหนึ่ง ๆ สามารถมี owner ได้เพียงหนึ่งตัวเท่านั้นในเวลาใดเวลาหนึ่ง
    • เมื่อ owner หลุดออกจาก scope ค่านั้นจะถูกจัดการ (dropped)
  • เมื่อรวมกับกฎการยืมหน่วยความจำ (borrowing rules) ระบบนี้ช่วยป้องกันปัญหาต่าง ๆ เช่น dangling pointers (พอยน์เตอร์ที่ชี้ไปยังหน่วยความจำที่ไม่ได้ใช้งานแล้ว) และ data races (การเข้าถึงข้อมูลพร้อมกันโดยไม่ปลอดภัย) เอาไว้ได้

ระบบ Ownership, Borrowing, และ References เป็นกลไกหลักที่จัดการหน่วยความจำ (memory) เพื่อให้แน่ใจว่าโปรแกรมจะปลอดภัยจากปัญหาต่าง ๆ เช่น null pointer หรือ data race โดยไม่จำเป็นต้องใช้ garbage collector แนวคิดเหล่านี้ช่วยให้ Rust มีประสิทธิภาพในการจัดการหน่วยความจำขณะที่ยังคงความปลอดภัยในเวลาคอมไพล์ (compile-time)

Ownership

ใน Rust ทุกค่าจะมี “เจ้าของเดียว” และเมื่อเจ้าของนั้นสิ้นสุด scope หน่วยความจำที่จัดสรรไว้สำหรับค่าดังกล่าวจะถูกปล่อย (freed) ทันทีโดยอัตโนมัติ นี่คือหลักการของ Ownership

กฎพื้นฐานของ Ownership

  • ในแต่ละเวลาจะมี “เจ้าของ” สำหรับค่าหนึ่ง ๆ ได้เพียงตัวเดียว
  • เมื่อเจ้าของถูกย้าย (moved) ค่าเดิมจะไม่สามารถถูกใช้งานได้อีก
  • เมื่อ scope ของเจ้าของหมด หน่วยความจำที่ใช้จะถูกปล่อยทันที

เช่นตัวอย่างนี้

fn main() {
let s1 = String::from("Hello");
let s2 = s1; // s1 ถูกย้ายไปยัง s2
// println!("{}", s1); // Error! s1 ไม่สามารถใช้งานได้อีก
println!("{}", s2);
}
  • s1 เป็นเจ้าของ String เมื่อ s2 = s1; Rust จะย้าย (move) ความเป็นเจ้าของไปที่ s2 ทำให้ s1 ไม่สามารถใช้งานได้อีก

Borrowing

เพื่อหลีกเลี่ยงการย้ายความเป็นเจ้าของ เราสามารถ “ยืม” ค่าโดยใช้การอ้างอิง (references) การยืมทำให้เราสามารถใช้ค่าหนึ่งได้โดยไม่ต้องย้าย ownership

กฎพื้นฐานของ Borrowing

  • เราสามารถยืมค่าผ่าน reference ได้โดยใช้ &
  • การยืมค่าจะมีสองแบบ: ยืมแบบอ่าน (&T) และยืมแบบแก้ไข (&mut T)
  • ยืมแบบอ่านสามารถมีหลายครั้งพร้อมกันได้ แต่ “ยืมแบบแก้ไขต้องมีได้เพียงครั้งเดียว” ในเวลาเดียวกัน

Borrow แบบไม่สามารถแก้ไขได้

fn main() {
let s1 = String::from("Hello");
let len = calculate_length(&s1); // ยืม s1 แต่ไม่ได้ย้าย
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len() // เราสามารถใช้ s ได้โดยไม่ต้องย้ายความเป็นเจ้าของ
}
  • ในตัวอย่างนี้ เราใช้ &s1 เพื่อยืมค่า s1 โดยไม่ย้าย ownership และยังสามารถใช้งาน s1 ได้ภายหลัง

References

References คือการยืมค่าผ่านการอ้างอิง โดยเราสามารถใช้เครื่องหมาย & เพื่อสร้าง reference สำหรับค่าที่เราต้องการใช้ชั่วคราวโดยไม่ต้องย้ายความเป็นเจ้าของ

  • Immutable References (&T): ยืมแบบอ่านได้อย่างเดียว เราไม่สามารถเปลี่ยนแปลงค่าของข้อมูลที่ถูกยืมได้
  • Mutable References (&mut T): ยืมแบบที่สามารถเปลี่ยนแปลงค่าได้ แต่สามารถยืมแบบนี้ได้เพียงครั้งเดียวในเวลาเดียวกันเท่านั้น

ข้อจำกัดของ References

  • เราไม่สามารถยืม mutable reference และ immutable reference พร้อมกันได้
fn main() {
let mut s = String::from("Hello");
let r1 = &s; // immutable reference
let r2 = &s; // immutable reference
// let r3 = &mut s; // Error! ไม่สามารถมี mutable reference ร่วมกับ immutable references
println!("{} and {}", r1, r2);
}

สรุปทั้งหมด 3 concept

  • Ownership: แต่ละค่าจะมีเจ้าของที่ชัดเจน เมื่อ scope ของเจ้าของสิ้นสุด ค่านั้นจะถูกปล่อยจากหน่วยความจำ
  • Borrowing: การยืมค่าทำได้ผ่านการอ้างอิง โดยเราสามารถยืมได้ทั้งแบบ immutable และ mutable แต่ต้องปฏิบัติตามกฎของการยืม
  • References: คือการยืมค่าชั่วคราวโดยไม่ต้องย้ายความเป็นเจ้าของ

Structs

https://doc.rust-lang.org/book/ch05-00-structs.html

Structs เป็นโครงสร้างข้อมูลที่ช่วยให้เรากำหนดประเภทข้อมูลที่ซับซ้อนมากกว่าการใช้ชนิดข้อมูลพื้นฐาน เช่น สามารถใช้เก็บหลาย ๆ ข้อมูลภายใต้โครงสร้างเดียวกัน คล้ายกับ “class” ในภาษาอื่น ๆ แต่ไม่มี method ภายในตัวโดยตรง (method จะถูกกำหนดแยกต่างหาก)

Rust มี Structs สามประเภท

  1. Classic Structs: โครงสร้างทั่วไปที่มีฟิลด์หลายแบบ
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
username: String::from("johndoe"),
email: String::from("[email protected]"),
sign_in_count: 1,
active: true,
};
println!("Username: {}", user1.username);
}

จาก code

  • Struct User ถูกประกาศด้วยฟิลด์ 4 ฟิลด์: username, email, sign_in_count, และ active
  • สร้างตัวแปร user1 ที่มีข้อมูลเหล่านี้
  • เราสามารถเข้าถึงฟิลด์ใน Struct ผ่านเครื่องหมายจุด (.) เช่น user1.username
  1. Tuple Structs: Struct แบบ Tuple ที่สามารถเข้าถึงฟิลด์ได้ผ่าน index
struct Color(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
println!("Black color RGB: {}, {}, {}", black.0, black.1, black.2);
}

จาก code

  • Color เป็น Tuple Struct ที่มีฟิลด์ 3 ฟิลด์คือ i32
  • การเข้าถึงฟิลด์ใช้เลข index เช่น black.0, black.1, และ black.2
  1. Unit Structs: Struct ที่ไม่มีฟิลด์ใด ๆ
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
  • AlwaysEqual เป็น Unit Struct ที่ไม่มีฟิลด์ สามารถใช้งานเมื่อเราต้องการสร้างประเภทข้อมูลใหม่ที่ไม่ต้องการเก็บข้อมูลใด ๆ

Enums

https://doc.rust-lang.org/book/ch06-00-enums.html

Enums เป็นประเภทข้อมูลที่สามารถเก็บค่าในหลายรูปแบบได้ภายใต้โครงสร้างเดียวกัน คล้ายกับ Union ในภาษาอื่น ๆ โดย Enums ช่วยให้เราเก็บค่าในรูปแบบที่แตกต่างกันแต่เป็นประเภทข้อมูลเดียวกันได้อย่างชัดเจนและปลอดภัย

ตัวอย่าง code

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg1 = Message::Quit;
let msg2 = Message::Move { x: 10, y: 20 };
let msg3 = Message::Write(String::from("Hello"));
let msg4 = Message::ChangeColor(255, 255, 0);
print_message(msg3);
}
fn print_message(msg: Message) {
match msg {
Message::Quit => println!("Quit message"),
Message::Move { x, y } => println!("Move to coordinates: {}, {}", x, y),
Message::Write(text) => println!("Write message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to RGB: {}, {}, {}", r, g, b),
}
}

จาก code

  • Enum Message ประกอบด้วย 4 รูปแบบ: Quit, Move, Write, และ ChangeColor
    • Quit ไม่มีข้อมูลใด ๆ
    • Move มีฟิลด์ {x, y} แบบ object
    • Write เก็บค่า String
    • ChangeColor เก็บข้อมูล 3 ค่าในรูปแบบ i32
  • ใช้ match เพื่อจัดการกับค่าที่แตกต่างกันใน Message

Error Handling

https://doc.rust-lang.org/book/ch09-00-error-handling.html

Error Handling ใน Rust เป็นแนวคิดที่สำคัญในการจัดการกับสถานการณ์ที่ไม่คาดคิดหรือการทำงานผิดพลาดในโปรแกรม โดย Rust ใช้รูปแบบการจัดการข้อผิดพลาดที่ชัดเจนและปลอดภัยจากการทำงานผิดพลาดที่อาจเกิดขึ้นใน runtime ซึ่งจะทำให้โปรแกรมมีความเสถียรและสามารถจัดการกับสถานการณ์เหล่านั้นได้อย่างถูกต้อง

Rust ใช้โครงสร้างข้อมูลหลัก 2 ชนิดในการจัดการข้อผิดพลาด

  1. Result<T, E>: ใช้สำหรับข้อผิดพลาดที่สามารถคาดการณ์ได้
  2. panic!: ใช้สำหรับข้อผิดพลาดที่ไม่สามารถกู้คืนได้

Result<T, E>

Result เป็น enum ที่มีสองค่าหลัก

  • Ok(T) สำหรับกรณีที่การทำงานสำเร็จ
  • Err(E) สำหรับกรณีที่เกิดข้อผิดพลาด

รูปแบบของ Result<T, E>

enum Result<T, E> {
Ok(T),
Err(E),
}

ตัวอย่าง code

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0.0 {
Err(String::from("Cannot divide by zero"))
} else {
Ok(numerator / denominator)
}
}
fn main() {
let result = divide(10.0, 0.0);
match result {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e),
}
}

จาก code

  • function divide จะคืนค่า Result ซึ่งอาจเป็น Ok(f64) ในกรณีที่การหารสำเร็จ หรือ Err(String) หากมีการหารด้วยศูนย์
  • ใน function main เราใช้ match เพื่อตรวจสอบค่าผลลัพธ์ว่าเป็น Ok หรือ Err และจัดการกับกรณีที่เกิดข้อผิดพลาด

วิธีการจัดการ Result

นอกจากใช้ match แล้ว Rust ยังมีวิธีที่สะดวกในการจัดการกับ Result เช่น:

  1. unwrap: ใช้เพื่อดึงค่าจาก Ok ถ้าเป็น Err จะทำให้โปรแกรมเกิด panic (เหมาะสำหรับกรณีที่มั่นใจว่าจะไม่มีข้อผิดพลาด)
  2. expect: คล้ายกับ unwrap แต่เราสามารถกำหนดข้อความข้อผิดพลาดเองได้
  3. ? operator: ใช้เพื่อส่งต่อข้อผิดพลาดขึ้นไปยัง function ที่เรียกใช้งาน (เหมาะสำหรับการเขียน code แบบสั้นและเรียบง่าย)
  • ตัวอย่าง unwrap
fn main() {
let result = divide(10.0, 2.0);
let value = result.unwrap(); // จะ panic ถ้าผลลัพธ์เป็น Err
println!("Value: {}", value);
}
  • ตัวอย่าง ? operator
fn read_file_content(filename: &str) -> Result<String, std::io::Error> {
let content = std::fs::read_to_string(filename)?; // ส่งต่อข้อผิดพลาดถ้าเกิดขึ้น
Ok(content)
}
fn main() {
match read_file_content("example.txt") {
Ok(content) => println!("File content: {}", content),
Err(e) => println!("Error reading file: {}", e),
}
}
  • เครื่องหมาย ? ถูกใช้เพื่อตรวจสอบว่าค่าที่คืนมาจาก std::fs::read_to_string เป็น Ok หรือ Err
  • ถ้าเป็น Err, function จะคืนข้อผิดพลาดไปยัง function ที่เรียกใช้งานโดยอัตโนมัติ

panic

panic! เป็นวิธีการจัดการข้อผิดพลาดที่ไม่สามารถกู้คืนได้ เช่น เมื่อเกิดข้อผิดพลาดร้ายแรงที่ไม่สามารถดำเนินการต่อได้ โปรแกรมจะหยุดทำงานและทำการ “panic” โดยแสดงข้อความข้อผิดพลาดและ trace stack

ตัวอย่าง

fn main() {
let v = vec![1, 2, 3];
// จะ panic เพราะ index ที่เรียกเกินขอบเขตของเวกเตอร์
println!("{}", v[99]);
}
  • ในตัวอย่างนี้ เมื่อพยายามเข้าถึง index ที่เกินขอบเขตของเวกเตอร์ โปรแกรมจะเกิด panic และหยุดการทำงาน

Note: ควรหลีกเลี่ยงการใช้ panic! ในกรณีที่เราสามารถจัดการกับข้อผิดพลาดได้ เพราะ Rust ต้องการให้โปรแกรมจัดการข้อผิดพลาดด้วยการใช้ Result เพื่อให้โปรแกรมมีความทนทานต่อข้อผิดพลาดที่อาจเกิดขึ้นได้

บทสรุป Error Handling

  • Result<T, E>: ใช้สำหรับจัดการข้อผิดพลาดที่สามารถคาดการณ์ได้ เช่น การอ่านไฟล์ การทำงานกับ I/O หรือการคำนวณที่อาจมีข้อผิดพลาด
  • panic!: ใช้ในกรณีที่เกิดข้อผิดพลาดร้ายแรงที่ไม่สามารถกู้คืนได้ และควรใช้ในกรณีที่โปรแกรมไม่สามารถดำเนินการต่อได้

feature การจัดการข้อผิดพลาดของ Rust ถูกออกแบบมาเพื่อช่วยให้คุณเขียน code ที่มีความเสถียรมากขึ้น panic! macro เป็นสัญญาณบอกว่าโปรแกรมของคุณอยู่ในสถานะที่ไม่สามารถจัดการได้ และช่วยให้คุณสั่งให้กระบวนการหยุดทำงานแทนที่จะพยายามดำเนินการต่อด้วยค่าที่ไม่ถูกต้องหรือผิดพลาด ส่วน Result enum ใช้ระบบประเภทของ Rust เพื่อบ่งบอกว่าการดำเนินการบางอย่างอาจล้มเหลว ซึ่ง code ของคุณอาจกู้คืนได้ คุณสามารถใช้ Result เพื่อบอก code ที่เรียก code ของคุณว่าจำเป็นต้องจัดการความเป็นไปได้ของความสำเร็จหรือความล้มเหลว การใช้ panic! และ Result ในสถานการณ์ที่เหมาะสมจะทำให้ code ของคุณมีความน่าเชื่อถือมากขึ้นเมื่อเกิดปัญหาที่หลีกเลี่ยงไม่ได้

Option

Option<T> เป็นประเภทข้อมูลที่ใช้จัดการกับค่าที่อาจจะมีหรือไม่มี โดยมีความคล้ายคลึงกับ Result<T, E> แต่ถูกออกแบบมาเพื่อใช้กับสถานการณ์ที่ไม่จำเป็นต้องจัดการกับข้อผิดพลาดอย่างเป็นทางการ เช่น ในกรณีที่เราต้องการแสดงให้เห็นว่าค่าหนึ่งอาจจะ “มีค่า” หรือ “ไม่มีค่า” (null-like) ซึ่งทำให้หลีกเลี่ยงปัญหาของการใช้งานค่า null ที่ไม่ปลอดภัยในภาษาอื่น ๆ

รูปแบบของ Option<T>

enum Option<T> {
Some(T),
None,
}
  • Some(T) หมายถึงมีค่า T ที่ถูกต้องอยู่
  • None หมายถึงไม่มีค่าใด ๆ (คล้าย null)

ตัวอย่างการใช้งาน

fn find_square_root(number: f64) -> Option<f64> {
if number >= 0.0 {
Some(number.sqrt()) // คืนค่าคำตอบถ้าคำนวณได้
} else {
None // คืนค่า None ถ้าหาคำตอบไม่ได้ (เช่นเลขติดลบ)
}
}
fn main() {
let result = find_square_root(9.0);
match result {
Some(value) => println!("Square root is: {}", value),
None => println!("No square root found."),
}
}

จาก code

  • function find_square_root จะคืนค่า Option<f64> ถ้าคำนวณ square root ได้ก็จะคืนค่า Some(value) หากเป็นเลขติดลบซึ่งไม่สามารถคำนวณ square root ได้ จะคืนค่า None
  • เราใช้ match เพื่อจัดการกับกรณีทั้ง Some และ None

ความแตกต่างระหว่าง Option กับ Result

  • Option<T>: ใช้สำหรับค่าที่อาจจะมีหรือไม่มี ไม่ได้มีข้อผิดพลาดชัดเจน
    • เช่น การหา element ใน list ถ้าไม่เจอก็คืนค่า None
  • Result<T, E>: ใช้สำหรับการจัดการกับข้อผิดพลาดที่สามารถคาดการณ์ได้ โดยจะมีข้อมูลเกี่ยวกับข้อผิดพลาดในกรณีที่การทำงานไม่สำเร็จ
    • เช่น การอ่านไฟล์ ถ้าไฟล์ไม่สามารถเปิดได้จะคืนค่า Err(e)

ตัวอย่างอื่นๆเพิ่มเติม

  • ตัวอย่างการใช้ Option<T> ในสถานการณ์ทั่วไป
    • เป็นตัวอย่าง การดึงค่าจากเวกเตอร์
    • v.get(1) คืนค่า Option<&i32> ถ้าดึงค่าได้จะเป็น Some ถ้า index เกินขอบเขตจะเป็น None
fn main() {
let v = vec![10, 20, 30];
let maybe_value = v.get(1); // ดึงค่าจาก index 1
match maybe_value {
Some(value) => println!("Found: {}", value),
None => println!("No value found at that index"),
}
}
  • การใช้ unwrap กับ Option<T>
    • หากเรามั่นใจว่าค่าที่ได้จะไม่เป็น None เราสามารถใช้ unwrap() เพื่อดึงค่าออกมาได้โดยตรง
    • ข้อควรระวัง: ถ้าใช้ unwrap() กับ None โปรแกรมจะเกิด panic ทันที ดังนั้นควรใช้เฉพาะกรณีที่มั่นใจว่าจะมีค่าเสมอ
fn main() {
let some_number = Some(5);
let number = some_number.unwrap(); // ดึงค่า 5 ออกมาได้
println!("The number is: {}", number);
}
  • การใช้ ? กับ Option<T>
    • เครื่องหมาย ? ใช้ได้กับ Option เช่นเดียวกับ Result เพื่อช่วยส่งค่า None กลับออกไปเมื่อไม่พบค่า โดยไม่ต้องเขียน match หรือ if let เอง
fn get_value(maybe_value: Option<i32>) -> Option<i32> {
let value = maybe_value?; // ถ้าเป็น None จะ return None ทันที
Some(value + 10) // ถ้ามีค่า ก็จะเพิ่ม 10 และคืนค่าใหม่
}
fn main() {
let result = get_value(Some(5));
println!("Result: {:?}", result);
}

สรุประหว่าง Result<T, E> และ Option<T>

  • Option<T>: ใช้เมื่อค่าที่เรากำลังจัดการอาจจะ “มี” หรือ “ไม่มี” แต่ไม่มีข้อผิดพลาดเฉพาะเจาะจง
  • Result<T, E>: ใช้เมื่อเราจำเป็นต้องจัดการข้อผิดพลาดที่อาจเกิดขึ้นจากการทำงานของโปรแกรม

สรุป

นี่คือหัวข้อแรกของ Rust ที่เราหยิบมาแชร์กับทุกคน ตอนนี้ทุกคนก็จะรู้จักและว่า ภาษา Rust คืออะไร run ยังไงบ้าง ให้ผลลัพธ์อย่างไรบ้าง รวมถึง main concept ของ Rust อย่าง Variable, Control Flow, Function, Ownership, Struct, Enum, Error Handling ซึ่งในหัวข้อนี้ เราจะทำให้ทุกคนเห็นภาพ look & feel ภาษา Rust กันก่อน

หัวข้อถัดไป เราจะเริ่มมาเจาะลึกในประเด็นที่ลึกซึ้งขึ้นของ Rust กัน ทั้งเรื่อง Pointer, Generic Type, OOP, Package Mangement ใน Rust เพื่อให้เกิดภาพที่สมบูรณ์ของภาษา Rust ก่อนที่เราจะเริ่มหยิบมาทำ application กันนะครับ


Related Post
  • มาเรียน Canvas ผ่าน Pong game กัน
    มี Video
    มารู้จักกับ HTML Canvas ว่ามันคืออะไร ใช้ทำอะไรบ้าง และมารู้จักพื้นฐานเบื้องต้นของการทำ Animation บน Browser ว่ามีหลักการเป็นประมาณไหน
  • หาจุดซื้อขายในโลกของ Bot Trade เขาทำกันยังไง ?
    มี Video มี Github
    เรื่องนี้สืบเรื่องจากที่ผมไปนั่งฟังเรื่องการทำ AI มา แล้วก็มีคนมาสอบถามผมประมาณว่า ทำ bot trade เนี้ยจริงๆเขาทำกันยังไง ผมก็คิดว่าก็ไม่น่ายากนะ
  • Redux และ React
    มี Video มี Github
    รู้จักกับ Redux state management ที่ช่วยทำให้ application จัดการ state ได้สะดวกสบายยิ่งขึ้นกัน
  • มาเรียนรู้การทำ Frontend Testing ผ่าน React กัน
    มี Video
    แนะนำพื้นฐานการทำ Component Testing สำหรับการทำ Unit testing ฝั่ง Frontend

Share on social media