Basic types of

Rust has an exact data type for each value, and generally falls into two categories: basic and compound. Basic types mean that they tend to be a minimal atomic type that cannot be deconstructed into other types (in the general sense) and consist of the following:

  • Numeric type: signed integer (i8.i16.i32.i64.isize), unsigned integer (u8.u16.u32.u64.usize), floating point (f32.f64), and rational and complex numbers
  • String: String literals and string slices&str
  • Boolean type:trueandfalse
  • Character type: Represents a single Unicode character, stored in 4 bytes
  • Unit type: that is(a), and its unique value(a)

Numeric types

Integer types

An integer is a number that has no fractional part. The i32 type, used previously, represents a signed 32-bit integer (I is the first letter of integer, as opposed to u, which stands for an unsigned type). The following table shows the built-in integer types in Rust:

The length of the Signed type Unsigned type
eight i8 u8
16 i16 u16
32 – i32 u32
A 64 – bit i64 u64
128 – bit i128 u128
It depends on the architecture isize usize

Type definitions are of the form unsigned + type size (bits). An unsigned number means that a number can take only positive numbers, whereas a signed number means that a number can take both positive and negative numbers. It’s like writing numbers on paper: when symbols are emphasized, numbers can be preceded by a plus or minus sign; However, when it is obvious that the number is positive, a plus sign is not needed. Signed numbers are stored as complement.

The range of numbers specified for each signed type is -(2n-1) to 2n-1-1, where n is the bit length of the defined form. Therefore, i8 can store numbers in the range of -(27) to 27-1, that is, -128 to 127. The unsigned type can store digits in the range of 0 to 2n-1. Therefore, U8 can store digits in the range of 0 to 28-1, that is, 0 to 255.

In addition, the isize and usize types depend on the CPU type of the computer on which the program is running: 32-bit if the CPU is 32-bit, and 64-bit if the CPU is 64-bit.

Integer literals can be written as follows:

Numeric literal The sample
The decimal system 98 _222
hexadecimal 0xff
octal 0o77
binary 0b1111_0000
Bytes (limited tou8) b'A'

With so many types, is there a simple rule to use? The answer is yes, Rust plastic defaults to i32, such as let I = 1, so I is the i32 type, so you can choose it and it is often the best performing type. The main use case for isize and usize is as indexes for collections.

The integer overflow

Let’s say we have a U8 that can store values from 0 to 255. Then when you change it to a value outside the range, such as 256, an integer overflow occurs. Rust has some interesting rules about this behavior: When it compiles in Debug mode,Rust checks for integer overflows and causes the program to panic(crash, a term Rust uses to indicate that the program quit because of an error) at compile time.

Rust does not detect overflows when building release mode with the –release parameter. Conversely, when an integer overflow is detected, Rust follows the rule of two’s Complement wrapping. In short, a value greater than the maximum value of the type is converted by the complement to the minimum value of the corresponding number that the type can support. For example, in the case of u8, 256 becomes 0,257 becomes 1, and so on. The program does not panic, but the value of the variable may not be what you expect. Any code that relies on this default behavior should be considered bad code.

To explicitly handle possible overflows, use these methods provided by the standard library for raw numeric types:

  • usewrapping_*Method is handled according to the complement loop overflow rule in all modes, for examplewrapping_add
  • If you are usingchecked_*Method, returns when an overflow occursNone  值
  • useoverflowing_*Method returns the value and a Boolean value indicating whether an overflow exists
  • usesaturating_*The method minimizes or maximizes a value

Floating point types

Floating-point numbers are numbers with a decimal point, and there are also two basic types of floating-point numbers in Rust: F32 and F64, with 32-bit and 64-bit sizes, respectively. The default floating point type is F64, which on modern cpus is nearly as fast as F32, but with greater precision.

Here is an example of a floating point number:

fn main() {
    let x = 2.0; // f64
    let y: f32 = 3.0; // f32
}
Copy the code

Floating point numbers are implemented according to the IEEE-754 standard. The F32 type is single-precision floating point and the F64 type is double-precision.

Floating point trap

Floating-point numbers can be dangerous if not used carefully because of the peculiarities of the underlying format, for two reasons:

  1. Floating-point numbers tend to be approximations of numbers that you want. Floating-point types are implemented in binary, but the numbers that we want to calculate tend to be in decimal. For example, 0.1 doesn’t have an exact representation in binary, but it does in decimal. This mismatch leads to some ambiguity. Moreover, although floating-point numbers can represent real values, they are often limited by the accuracy of fixed-length floating-point numbers due to underlying formatting problems. If you want to express real numbers with perfect accuracy, you can only use floating-point numbers with infinite precision

  2. Floating-point numbers are counterintuitive in some ways like you think you can compare floating-point numbers, right? Yes, they can be compared using >, >=, etc., but in some cases, this intuitive comparison can work against you. Because the comparison on F32 and F64 implements STD :: CMP ::PartialEq (similar to interfaces in other languages), it does not implement STD :: CMP ::Eq, which is defined on other numeric types. Here’s an example:

    Rust’s HashMap data structure, which is a HashMap implementation of the KV type, has no specific type restriction on K, but requires that any type that can be used as K must implement STD :: CMP ::Eq. So this means that you can’t use floating-point numbers as keys for a HashMap to store key-value pairs, but for comparison, Rust’s integer, string, and Boolean types all implement this feature and can therefore be used as keys for a HashMap.

    To avoid the two pitfalls mentioned above, you need to follow these guidelines:

    • Avoid testing equality on floating-point numbers
    • Extra care is needed when the result may be mathematically undefined

    Here’s a quick example:

    fn main() {
       // assert that 0.1 + 0.2 is equal to 0.3
       assert!(0.1 + 0.2= =0.3);
    }
    Copy the code

You might think there is nothing wrong with this code, but in fact it will panic because of binary precision issues, so 0.1 + 0.2 is not exactly 0.3, they may be errors after N decimal places.

What if you had to compare? Consider this approach (0.1_f64 + 0.2-0.3). Abs () < 0.00001, depending on your accuracy requirements.

At this point, I believe you have basically understood why you should be careful when handling floating point numbers, but that’s not enough. Here’s a code that will shock your soul:

fn main() {
    let abc: (f32.f32.f32) = (0.1.0.2.0.3);
    let xyz: (f64.f64.f64) = (0.1.0.2.0.3);


    println!("abc (f32)");
    println!("0.1 + 0.2: {:x}", (abc.0 + abc.1).to_bits());
    println!("0.3: {: x}", (abc.2).to_bits());
    println!(a);println!("xyz (f64)");
    println!("0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits());
    println!("0.3: {: x}", (xyz.2).to_bits());
    println!(a);assert!(abc.0 + abc.1 == abc.2);
    assert!(xyz.0 + xyz.1 == xyz.2)
Copy the code

Run the program and the output is as follows:

abc (f32)
   0.1 + 0.2: 3e99999a
         0.3: 3e99999a

xyz (f64)
   0.1 + 0.2: 3fd3333333333334
         0.3: 3fd3333333333333

thread 'main' panicked at 'assertion failed: xyz.0 + xyz.1 == xyz.2', ➥ ch2 - add - floats. Rs. Rs:14:5
note: run with `RUST_BACKTRACE=1'Environment variable to display ➥a backtraceCopy the code

If you look closely, when adding f32, 0.1 + 0.2 gives 3E99999A and 0.3 is 3E99999A, so 0.1 + 0.2 == 0.3 under F32 passes the test, but when it comes to F64, the result is different. Because the F64 is much more accurate, there is a slight change very far behind the decimal point, 0.1 + 0.2 ending in 4, but 0.3 ending in 3. This slight difference causes the f64 test to fail and throw an exception.

NaN

For mathematically undefined results, such as taking the square root of negative numbers -42.1.sqrt(), a special result is produced: Rust’s floating-point type uses NaN (not a number) to handle these cases.

All operations that interact with NaN will return a NaN, and NaN cannot be compared. The following code will crash:

fn main() {
  let x = (-42.0 _f32).sqrt();
  assert_eq!(x, x);
}
Copy the code

For defensive programming purposes, use methods like is_nan(), which can be used to determine if a value is NaN:

fn main() {
    let x = (-42.0 _f32).sqrt();
    if x.is_nan() {
        println!("Undefined mathematical behavior.")}}Copy the code

Sequence (Range)

Rust provides a very compact way to generate continuous values, such as 1.. 5, generates consecutive numbers from 1 to 4, excluding 5; 1.. =5, produces a sequence of numbers from 1 to 5, including 5, which is simple and often used in loops:

for i in 1..=5 {
    println!("{}",i);
}
Copy the code

Final program output:

1
2
3
4
5
Copy the code

Sequences are only allowed for numeric or character types because they can be contiguous and the compiler can check at compile time to see if the sequence is null. Character and numeric values are the only types in Rust that can be used to determine whether the sequence is null. Here is an example of using a sequence of character types:

for i in 'a'..='z' {
    println!("{}",i
Copy the code

Rational numbers and complex numbers

Rust’s standard library has higher barriers to entry than other languages, so rational numbers and complex numbers are not included in the library:

  • Rational numbers and complex numbers
  • Integers of any size and floating point numbers of any precision
  • Fixed precision decimal decimal, often used in currency related scenarios

Fortunately, the community has developed a high-quality Rust numerical library: NUM. Follow these steps to import the NUM library:

  1. Cargo new complex-num && CD complex-num
  2. Add num = “0.4.0” under [dependencies] in Cargo. Toml
  3. Replace the main function in the SRC /main.rs file with the following code
  4. Run the cargo run
use num::complex::Complex;

fn main() {
   let a = Complex { re: 2.1, im: -1.2 };
   let b = Complex::new(11.1.22.2);
   let result = a + b;

   println!("{} + {}i", result.re, result.im)
}
Copy the code

Character type (char)

Characters may not be easy to understand for beginners without other programming experience (anyone who has no programming experience would dare to learn Rust), but you can think of Rust as letters in English or characters in Chinese.

The following code shows a few exotic characters:

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let g = 'the';
    letHeart_eyed_cat = '😻'; }Copy the code

If you’re coming from a programming language with a sense of age, you might yell: is this a character? Yes, these are all characters in the Rust language. Characters in Rust are not just ASCII. All Unicode values can be used as Rust characters, including individual Chinese, Japanese, Korean, emoji, etc., which are all legal character types. Unicode values range from U+0000 to U+D7FF and U+E000 to U+10FFFF. However, “character” is not a concept in Unicode, so the intuitive understanding of “character” is not consistent with Rust’s concept of characters.

Since Unicode is a four-byte encoding, character types also take up four bytes:

fn main() {
    let x = 'in';
    println!("Memory size of {} bytes occupied by 'in character'", std::mem::size_of_val(&x));
}
Copy the code

The output is as follows:

$ cargo run Compiling ... character'in'Take up4Memory size in bytesCopy the code

Note that we haven’t started talking about strings yet, but unlike some languages, Rust characters can only be represented by “”, “” is reserved for strings

Boolean (bool)

The Boolean type in Rust has two possible values: true and false, and booleans occupy 1 byte of memory:

fn main() {
    let t = true;

    let f: bool = false; // Use the type annotation to explicitly specify the type of f if f {
        println!("This is a meaningless piece of code."); }}Copy the code

Scenarios that use Booleans focus on process control, such as the if in the code above.

Unit type

Unit type is (), yes, you read that correctly, is (), the only value is (), some readers may not want to read this, you are too perfunctory. call this type?

Let’s just say that every little thing has a purpose, and you’ve seen the fn main() function used many times over the course of this class, right? So what does this function return?

That’s right, main returns this unit type (). You can’t say that main returns nothing, because Rust has a separate definition for a function that does not return a value: a diverging function, as the name implies, a function that does not converge.

For example, the common println! The return value of () is also the unit type ().

For example, you can use () as the map value to indicate that we don’t care about the specific value, just the key. Struct {} ** is similar to the Go language and can be used as a placeholder value, but does not occupy any memory at all.

Type derivation and annotation

Unlike dynamic languages like Python and Javascript, Rust is statically typed, meaning that the compiler has to know the types of all our variables at compile time, but that doesn’t mean you need to specify a type for each variable, because Rust compilers are smart, It can automatically deduce the type of a variable based on its value and how it is used in the context, but the compiler is also not clever enough to deduce the type of a variable in some cases and needs to manually assign a type annotation, as first impressions of Rust show.

Take a look at this code:

let guess = "42".parse().expect("Not a number!");
Copy the code

Ignore the parse (). Expect.. Partially, the purpose of this code is to parse the string “42”, and the compiler cannot deduce the type we want here: integer? Floating point Numbers? The string? So the compiler will report an error:

Compiling playground v0.0.1 (/playground)
error[E0282]: type annotations needed
 --> src/main.rs:4:5
  |
4 | let guess = "42".parse().expect("Not a number!");
  |     ^^^^^ consider giving `guess` a type

For more information about this error, try `rustc --explain E0282`.
error: could not compile `playground` due to previous erro
Copy the code

So we need to provide the compiler with more information, such as an explicit type annotation for the GUESS variable: Let Guess: i32 =… Or “42” parse: : < i32 > ().