[Rust Easy to Learn Tutorial] Day 1: Rust basics, basic syntax

Previous section:[Rust Easy to Learn Tutorial] Preparation: Cargo, Hello

Today, we officially enter the basics of Rust. In the content of this article, I will introduce the following content to you:

  1. Basic Rust syntax: variables, scalar and composite types, enums, structures, references, functions and methods.
  2. Control flow structure: if、if let、while、while let、break 和 continue.
  3. Pattern matching: Destructuring enums, structures and arrays.

What is Rust?

Rust is a new programming language that released version 1.0 in 2015. I will let you know the significance of Rust from the following aspects:

  • Rust is astatically compiled language that functions similarly to c++. rustc uses LLVM as its compilation framework (for specific information about LLVM, you can visit its Official website: https://llvm.org/).
  • Rust supports many platforms and architectures. For example, x86, ARM, WebAssembly, ... and other architectures, as well as Linux, Mac, Windows, ... and other platforms.
  • Rust is used in a wide range of devices, such as firmware and bootloaders, smart display devices, mobile phones, desktops, servers, and more.

We found that Rust has the same features as c++:

  • High flexibility.
  • High degree of control.
  • Can be scaled down to very constrained devices such as microcontrollers.
  • There is no runtime or garbage collection.
  • Focus on reliability and security without sacrificing performance.

Hi,I am Rust

After understanding what Rust is, let us first experience the simplest program in Rust:

fn main() {
    
    
    println!("Hi, I am Rust!");
}

From the above code, we see that rust code has the following characteristics:

  • Function reason fn Induction.
  • Like C and c++, blocks are separated by curly braces.
  • mainThe function is the entry point of the program.
  • Rust has hygiene macros (hygienic macros), of which println! is an example.
  • Rust strings are UTF-8 encoded and can contain any Unicode characters.

What are health macros? The difference between sanitary macros and ordinary macrosis somewhat similar to the difference between lexical scope functions and dynamic scope functions. For example, if there is a name name1 at the place where the macro is called, and there is also a name name1 inside the macro, then when the health macro is expanded, it will rename its own internal name1 to name2; ordinary macros will not change the name and "capture" the external name.

To make it easier for you to understand, let me summarize the above content here:

  1. Rust is very much like other traditional languages ​​that follow the C/C++/Java paradigm.

  2. Rust is modern and fully supports things like Unicode .

  3. Rust Use macros when a variable number of arguments is required (function overloading is not allowed).
    Macros are "sanitary", meaning thatthey do not accidentally capture identities from the scope in which they are used Symbol. Rust Macros are actually only partially hygienic .

  4. Rust is multi-paradigm. For example, it has strong object-oriented programming features, and, although it is not a functional language, it contains a range of functional concepts.

Based on the summary above, can you also find some unique selling points of Rust:

  • Compile time memory safety. For example, Rust eliminates runtime errors for entire classes through borrowing checkers, resulting in performance like C and C++, but without the memory insecurity question. Additionally, you get a modern language with constructs like pattern matching and built-in dependency management.
  • Missing undefined runtime behavior.
  • Characteristics of Modern Languages. For example, you get fast and predictable performance like C and C++ (without a garbage collector) and access to low-level hardware.

Why Rust

Next, I will introduce to you from several aspects why Rust stands out among many languages. Let’s start with an example.

Example: How to do it in C language

First, let's look at an example in C language:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

int main(int argc, char* argv[]) {
    
    
	char *buf, *filename;
	FILE *fp;
	size_t bytes, len;
	struct stat st;

	switch (argc) {
    
    
		case 1:
			printf("Too few arguments!\n");
			return 1;

		case 2:
			filename = argv[argc];
			stat(filename, &st);
			len = st.st_size;
			
			buf = (char*)malloc(len);
			if (!buf)
				printf("malloc failed!\n", len);
				return 1;

			fp = fopen(filename, "rb");
			bytes = fread(buf, 1, len, fp);
			if (bytes = st.st_size)
				printf("%s", buf);
			else
				printf("fread failed!\n");

		case 3:
			printf("Too many arguments!\n");
			return 1;
	}

	return 0;
}

How many bugs did you find in the code above?

Although there are only 29 lines of code, at least 11 lines of this C example contain serious errors:

  • Assignment= instead of equality comparison==(line 28)
  • printfredundant parameters (line 23)
  • File descriptor leak (after line 26)
  • Multiple lines Forgot to use braces in if (line 22)
  • Forgot to interrupt in switch statement (line 32)
  • forgot buf null termination of string, causing buffer overflow (line 29)
  • Do not release malloc The allocated buffer causes a memory leak (line 21)
  • Out-of-bounds access (line 17)
  • Not checked Case in switch statement (line 11)
  • The return values ​​of stat and fopen are not checked (lines 18 and 26)

Shouldn't these errors be obvious even to a C compiler?
No, surprisingly, even in the latest GCC version (13.2 at the time of writing) , the code will also compile without warning at the default warning level.

Isn't this a very unrealistic example?
Absolutely not, this type of mistake has led to serious security vulnerabilities in the past. For example:

  • Assignment= instead of equality comparison==: 2003 Linux backdoor attempt vulnerability
  • Forgot to use braces in multiline if: Apple's goto fail bug
  • The forgotten interrupt in switch statements: Interrupts that interrupt sudo

So, you may ask, how good can Rust be here?
- Safe Rust Make all these bugs impossible , for example:

  • Assignments in if clauses are not supported.
  • Format strings are checked at compile time.
  • The resource is released at the end of the scope through the Drop attribute.
  • All if clauses require braces.
  • match(在Rust中相当于switch) will not fail, so developers cannot accidentally forget break.
  • Buffer slices carry their size and do not rely on the NULL` terminator.
  • Heap-allocated memory is released via the Drop attribute when the corresponding Box goes out of scope.
  • An out-of-bounds access results in panic, or can be checked via the slice's get method.
  • matchAll cases will be required to be processed.
  • Error-prone Rust functions return Result values ​​that need to be unwrapped to check for success. Additionally, the compiler will issue a warning if the return value of a function marked with #[must_use] is not checked.

Compile time verification

The static memory at compile time will be verified as follows:

  • Verify there are no uninitialized variables.
  • Verify there are no memory leaks.
  • I don't know what to do double-frees.
  • 验证use-after-free.
  • 验证NULL Finger stick.
  • Verification forgot to lock the mutex.
  • Verify there are no data races between threads.
  • Verify that the iterator is invalid.

Runtime verification

The following actions will be considered undefined behavior at runtime:

  • Check bounds on array access.
  • Defines integer overflow (panic or wrap-around).

Integer overflow is defined through the compile-time overflow check flag. If enabled, the program will crash, otherwise the developer will gain wraparound semantics. By default, you will get panic in debug mode (cargo build) and release mode (cargo build --release).

Bounds checking cannot be disabled using a compiler flag. It also cannot be disabled directly using the unsafe keyword. However, unsafety allows developers to call functions such as slice::get_unchecked which do not perform bounds checking.

Rust has the features of a modern language

Rust is built using all the experience accumulated over the past few decades, taking the best of several major languages ​​and improving upon them. In terms of language features, it has the following points:

  • Enumerations and pattern matching.
  • Generics.
  • Not available FFI.
  • Zero-cost abstraction.

In terms of tool support, it has the following points:

  • Good compiler error detection.
  • Built-in dependency manager.
  • Built-in test support.
  • Excellent language server protocol support.

To put it in more detail, the main points are as follows:

  • Zero-cost abstraction, similar to C++, means you don't have to "pay" for high-level programming constructs that use memory or the CPU. For example, writing a loop using For should produce roughly the same low-level instructions as using the .iter().fold() construct.

  • It's worth mentioning that Rust enumerations are "algebraic data types", also known as "sum types", which allow the type system to express things like Option<T>和Result<T, E>.

  • Alert developers to errors - Many developers have become accustomed to ignoring lengthy compiler output. The Rust compiler is significantly more talkative than other compilers. It often provides developers with actionable feedback, ready to be copied and pasted into your code.

  • Compared to languages ​​like Java, Python, and Go, the Rust standard library is small. Rust doesn't provide some things you might think are standard and necessary, such as:

    • A random number generator, but developers should refer to rand.
    • Supports SSL or TLS, but please visit rusttls.
    • For JSON support, developers can refer to serde_json. The reason behind this is that the functionality in the standard library cannot disappear, so it must be very stable. For the above examples, the Rust community is still working hard to find the best solutions - perhaps there is no single "best solution" for some of these things.

Rust comes with a built-in package manager in the form of Cargo which makes downloading and compiling third-party crate very easy. The result of this is that the standard library can be smaller. https://lib.rs This website can help you find more third-party libraries.

  • rust-analyzer Implemented support for major ide and text editors.

basic grammar

Much of Rust syntax will be familiar to C、c++或Java . For example:

  • Blocks and scopes are separated by curly braces.
  • line comments start with // and block comments start with /*…* /.
  • Keywords like if and while work the same way.
  • Variable assignment is completed with =, and comparison is completed with ==.

scalar type

type Example
signed integer i8, i16, i32, i64, i128, isize -10, 0, 1_000, 123_i64
unsigned integer 8, 16, 32, 64, 128, help 0, 123, 10_u16
floating point number f32, f64 3.14, -10.0e20, 2_f32
string &str “foo”, “two\nlines”
Unicode scalar value char 'a', 'a', '∞'
boolean bool true, false

In the table above, all underscores in numbers can be omitted; they are for readability only. Therefore, 1_000 can be written as 1000(或10_00), and 123_i64 can be written as 123i64.

Meanwhile, the width of the above type is as follows:

  • iN, uN and fN are N bits wide,
  • Isize and usize are the width of the pointer,
  • Char is 32 bits wide,
  • Bool is 8 bits wide.

In addition, raw strings allow developers to create an escaped value, such as: r"\n" == "\\n". You can embed double quotes by placing equal amounts of on both sides of the quotes#:

fn main() {
    
    
    println!(r#"<a href="link.html">link</a>"#);
    println!("<a href=\"link.html\">link</a>");
}

Byte strings allow you to create directly&[u8]value:

fn main() {
    
    
    println!("{:?}", b"abc");
    println!("{:?}", &[97, 98, 99]);
}

composite type

type Example
array [T; N] [20, 30, 40], [0; 3]
tuple (), (T,), (T1, T2), … (), (‘x’,), (‘x’, 1.2), …

Array assignment and access:

fn main() {
    
    
    let mut a: [i8; 10] = [42; 10];
    a[5] = 0;
    println!("a: {a:?}");
}
  • Array type [T;N] holds N (compile-time constant) elements of the same type t. Note that the length of an array is part of its type, which means [u8;3] and [u8;4] are considered two different types.
  • You can use literals to assign values ​​to arrays.
  • Added #, such as {a:#?}, can have "pretty output" formatting, which makes it easier to read.

Assignment and access of tuples:

fn main() {
    
    
    let t: (i8, bool) = (7, true);
    println!("t.0: {}", t.0);
    println!("t.1: {}", t.1);
}
  • Like arrays, tuples have a fixed length.

  • Tuples combine values ​​of different types into a composite type.

  • The fields of the tuple can be accessed by index of the period and value, e.g. t.0, t.1.

  • The empty tuple () is also called the "unit type". It is both a type and the only valid value of that type—that is, both the type and its value are represented as (). For example, it is used to represent a function or expression that has no return value.

reference type

Like C++, Rust also has reference types:

fn main() {
    
    
    let mut x: i32 = 10;
    let ref_x: &mut i32 = &mut x;
    *ref_x = 20;
    println!("x: {x}");
}

have to be aware of is:

  • must be dereferenced when assigning ref_x, similar to C and C++ pointers.
  • Rust will automatically dereference in some cases, especially when calling a method (e.g., ref_x.count_ones()).
  • A reference declared as mut can be bound to different values ​​during its lifetime.
  • Be sure to note the difference between let mut ref_x: &i32 and let ref_x: &mut i32. The first represents a mutable reference that can be bound to different values, while the second represents a mutable A reference to the value.
dangling reference

Rust will statically disable dangling references:

fn main() {
    
    
    let ref_x: &i32;
    {
    
    
        let x: i32 = 10;
        ref_x = &x;
    }
    println!("ref_x: {ref_x}");
}
  • You can think of a reference as "borrowing" the value it refers to.
  • Rust is tracking the lifetime of all references to make sure they live long enough.

Slices

Slices provide developers with views of larger collections:

fn main() {
    
    
    let mut a: [i32; 6] = [10, 20, 30, 40, 50, 60];
    println!("a: {a:?}");

    let s: &[i32] = &a[2..4];

    println!("s: {s:?}");
}

In the above code, we create the slice by borrowing a and specifying the start and end index in parentheses.

  • If the slice starts at index 0, Rust's range syntax allows us to remove the starting index, which means &a[0.. .len()] and &a[.. .. len()] are the same.

  • The same goes for for the last index, so a &a[2.. .len()] and a &a[2..] are the same.

So, to easily create a slice of the entire array, we can use&a[…].

S is a reference to the i32s slice. Note that the type of s (&[i32]) no longer mentions the array length. This allows us to perform calculations on slices of different sizes.

A slice is always borrowed from another object. In this case, a must remain "alive" (in scope) for at least as long as our slice.

String Vs Str

Now we can understand the two string types in Rust:

fn main() {
    
    
    let s1: &str = "World";
    println!("s1: {s1}");

    let mut s2: String = String::from("Hello ");
    println!("s2: {s2}");
    s2.push_str(s1);
    println!("s2: {s2}");
    
    let s3: &str = &s2[6..];
    println!("s3: {s3}");
}
  • &str: immutable reference to a string slice

  • String: mutable string buffer

  • &strIntroduced a string slice, which is an immutable reference to UTF-8 encoded string data stored in a block of memory. String literals ("Hello") are stored in the program's binary file.

  • Rust’s String type is a wrapper around a byte vector. Like Vec<T>, it is private.

  • Like many other types, String::from() creates a string from a string literal. String::new() Creates a new empty string to which string data can be added using the push() and push_str() methods.

  • The macro is a convenient way to generate a private string from a dynamic value. It accepts the same formatting specifications as println!().

  • You can borrow slices from String via & and optional range selection. &str

  • for c++ programmers: you can think of &str as in c++, but it always points to A valid string in memory. Rust is roughly equivalent to in c++ (main difference: it can only contain UTF-8 encoded bytes, and will never use small string optimizations ). const char*Stringstd:: String

Functions

Methods

Methods are functions associated with a type. The self parameter of a method is an instance of its associated type:

struct Rectangle {
    
    
    width: u32,
    height: u32,
}

impl Rectangle {
    
    
    fn area(&self) -> u32 {
    
    
        self.width * self.height
    }

    fn inc_width(&mut self, delta: u32) {
    
    
        self.width += delta;
    }
}

fn main() {
    
    
    let mut rect = Rectangle {
    
     width: 10, height: 5 };
    println!("old area: {}", rect.area());
    rect.inc_width(5);
    println!("new area: {}", rect.area());
}
  • Add a static method called Rectangle::new and call it from main:
fn new(width: u32, height: u32) -> Rectangle {
    
    
    Rectangle {
    
     width, height }
}
  • While Rust technically does not have custom constructors, static methods are often used to initialize structures. The actual constructorRectangle {width, height} can be called directly.

  • Add Rectangle::square(width: u32) constructor to illustrate that such static methods can accept arbitrary arguments.

function overloading

Overloading is not supported:

  • Each function has a separate implementation:

    • There is always a fixed number of parameters.
    • Always accepts a set of parameter types.
  • Default value not supported:

    • All call sites have the same number of parameters.
    • Macros are sometimes used as an alternative.

However, function parameters can be generic:

fn pick_one<T>(a: T, b: T) -> T {
    
    
    if std::process::id() % 2 == 0 {
    
     a } else {
    
     b }
}

fn main() {
    
    
    println!("coin toss: {}", pick_one("heads", "tails"));
    println!("cash prize: {}", pick_one(500, 1000));
}

When using generics, the standard library's Into can provide a limited polymorphism in parameter types. I'll go into more detail on this in a later section.

Guess you like

Origin blog.csdn.net/ImagineCode/article/details/134228206