the-rust-programming-language

所属分类:电子书籍
开发工具:Rust
文件大小:0KB
下载次数:0
上传日期:2023-12-14 14:22:04
上 传 者sh-1993
说明:  《Rust编程语言》(the Rust Programming Language)被亲切地昵称为“这本书”,它将从基本原理中概述该语言。你会...
(Affectionately nicknamed “the book,” The Rust Programming Language will give you an overview of the language from first principles. You’ll build a few projects along the way, and by the end, you’ll have a solid grasp of the language.)

# the-rust-programming-language Affectionately nicknamed “the book,” The Rust Programming Language will give you an overview of the language from first principles. You’ll build a few projects along the way, and by the end, you’ll have a solid grasp of the language. ## Notes ### 1. Getting Stated #### Hello, Cargo! ##### Note about the chapter recap Let’s recap what we’ve learned so far about Cargo: We can create a project using cargo new. We can build a project using cargo build. We can build and run a project in one step using cargo run. We can build a project without producing a binary to check for errors using cargo check. Instead of saving the result of the build in the same directory as our code, Cargo stores it in the target/debug directory. ##### Note about Building for Release When your project is finally ready for release, you can use cargo build --release to compile it with optimizations. ### 2. Programming a Guessing Game #### Note about the Result type values Values of the Result type, like values of any type, have methods defined on them. An instance of Result has an expect method that you can call. If this instance of Result is an Err value, expect will cause the program to crash and display the message that you passed as an argument to expect. If the read_line method returns an Err, it would likely be the result of an error coming from the underlying operating system. If this instance of Result is an Ok value, expect will take the return value that Ok is holding and return just that value to you so you can use it. In this case, that value is the number of bytes in the user’s input. If you don’t call expect, the program will compile, but you’ll get a warning: #### Note about the using a crate Remember that a crate is a collection of Rust source code files. The project we’ve been building is a binary crate, which is an executable. The rand crate is a library crate, which contains code that is intended to be used in other programs and can’t be executed on its own. #### Note about the using a dependency When we include an external dependency, Cargo fetches the latest versions of everything that dependency needs from the registry, which is a copy of data from Crates.io. Crates.io is where people in the Rust ecosystem post their open source Rust projects for others to use. After updating the registry, Cargo checks the [dependencies] section and downloads any crates listed that aren’t already downloaded. In this case, although we only listed rand as a dependency, Cargo also grabbed other crates that rand depends on to work. After downloading the crates, Rust compiles them and then compiles the project with the dependencies available. #### Note about the generation of documentation locally for your app Note: You won’t just know which traits to use and which methods and functions to call from a crate, so each crate has documentation with instructions for using it. Another neat feature of Cargo is that running the cargo doc --open command will build documentation provided by all your dependencies locally and open it in your browser. If you’re interested in other functionality in the rand crate, for example, run cargo doc --open and click rand in the sidebar on the left. #### Note about the Ordering enum type used with cmp (comparison) and the match statement The Ordering type is another enum and has the variants Less, Greater, and Equal. These are the three outcomes that are possible when you compare two values. ``` match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } ``` A match expression is made up of arms. An arm consists of a pattern to match against, and the code that should be run if the value given to match fits that arm’s pattern. Rust takes the value given to match and looks through each arm’s pattern in turn. Patterns and the match construct are powerful Rust features: they let you express a variety of situations your code might encounter and they make sure you handle them all. These features will be covered in detail in Chapter 6 and Chapter 18, respectively. When the code compares 50 to 38, the cmp method will return Ordering::Greater because 50 is greater than 38. The match expression gets the Ordering::Greater value and starts checking each arm’s pattern. It looks at the first arm’s pattern, Ordering::Less, and sees that the value Ordering::Greater does not match Ordering::Less, so it ignores the code in that arm and moves to the next arm. The next arm’s pattern is Ordering::Greater, which does match Ordering::Greater! The associated code in that arm will execute and print Too big! to the screen. The match expression ends after the first successful match, so it won’t look at the last arm in this scenario. ### 3. Common Programming Concepts #### Data Types ##### Note about the syntax of numbers in thousands Number literals can also use _ as a visual separator to make the number easier to read, such as 1_000, which will have the same value as if you had specified 1000. #### Functions ##### Note about the naming conventions Rust code uses snake case as the conventional style for function and variable names, in which all letters are lowercase and underscores separate words. Here’s a program that contains an example function definition: ``` fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); } ``` ##### Note about the explicit data type definition In function signatures, you must declare the type of each parameter. This is a deliberate decision in Rust’s design: requiring type annotations in function definitions means the compiler almost never needs you to use them elsewhere in the code to figure out what type you mean. The compiler is also able to give more helpful error messages if it knows what types the function expects. ### 4. Understanding Ownership #### What Is Ownership? ##### Note about the Rust's goal Rust's goal is to compile programs into efficient binaries that require as few runtime checks as possible. Therefore Rust does not check at runtime whether a variable is defined before being used. Instead, Rust checks at compile-time. ##### Note about the place where variables live Frames are organized into a stack of currently-called-functions. For example, at L2 the frame for main sits above the frame for the called function plus_one. After a function returns, Rust deallocates the function's frame. (Deallocation is also called freeing or dropping, and we use those terms interchangeably.) This sequence of frames is called a stack because the most recent frame added is always the next frame freed. ``` fn main() { let n = 5; let y = plus_one(n); println!("The value of y is: {y}"); } fn plus_one(x: i32) -> i32 { x + 1 } ``` #### References and Borrowing ##### Note about the Simultaneous Aliasing and Mutation Pointers are a powerful and dangerous feature because they enable aliasing. Aliasing is accessing the same data through different variables. On its own, aliasing is harmless. But combined with mutation, we have a recipe for disaster. One variable can "pull the rug out" from another variable in many ways, for example: - By deallocating the aliased data, leaving the other variable to point to deallocated memory. - By mutating the aliased data, invalidating runtime properties expected by the other variable. - By concurrently mutating the aliased data, causing a data race with nondeterministic behavior for the other variable. ##### Note about the Pointer Safety Principle ``` let mut v: Vec = vec![1, 2, 3]; let num: &i32 = &v[2]; v.push(4); println!("Third element is {}", *num); ``` Initially, v points to an array with 3 elements on the heap. Then num is created as a reference to the third element, as seen at L1. However, the operation v.push(4) resizes v. The resize will deallocate the previous array and allocate a new, bigger array. In the process, num is left pointing to invalid memory. Therefore at L3, dereferencing *num reads invalid memory, causing undefined behavior. In more abstract terms, the issue is that the vector v is both aliased (by the reference num) and mutated (by the operation v.push(4)). So to avoid these kinds of issues, Rust follows a basic principle: Pointer Safety Principle: data should never be aliased and mutated at the same time. Data can be aliased. Data can be mutated. But data cannot be both aliased and mutated. For example, Rust enforces this principle for boxes (owned pointers) by disallowing aliasing. Assigning a box from one variable to another will move ownership, invalidating the previous variable. Owned data can only be accessed through the owner — no aliases. ##### Note about the summary of References and Borrowing References provide the ability to read and write data without consuming ownership of it. References are created with borrows (& and &mut) and used with dereferences (*), often implicitly. However, references can be easily misused. Rust's borrow checker enforces a system of permissions that ensures references are used safely: - All variables can read, own, and (optionally) write their data. - Creating a reference will transfer permissions from the borrowed path to the reference. - Permissions are returned once the reference's lifetime has ended. - Data must outlive all references that point to it. #### Fixing Ownership Errors ##### Summary When fixing an ownership error, you should ask yourself: is my program actually unsafe? If yes, then you need to understand the root cause of the unsafety. If no, then you need to understand the limitations of the borrow checker to work around them. #### The Slice Type ##### Note about what are slice types Slices are a special kind of reference that refer to sub-ranges of a sequence, like a string or a vector. At runtime, a slice is represented as a "fat pointer" which contains a pointer to the beginning of the range and a length of the range. One advantage of slices over index-based ranges is that the slice cannot be invalidated while it's being used. ##### Note about the A more experienced Rustacean would write the signature shown in Listing 4-9 instead because it allows us to use the same function on both &String values and &str values. ``` fn first_word(s: &str) -> &str { // ... } ``` instead of ``` fn first_word(s: &String) -> &str { // ... } ``` ### 5. Using Structs to Structure Related Data #### Defining and Instantiating Structs ##### Note about the Structs ``` struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { let mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com"); } ``` Note that the entire instance must be mutable; Rust doesn’t allow us to mark only certain fields as mutable. As with any expression, we can construct a new instance of the struct as the last expression in the function body to implicitly return that new instance. ##### Note about the Struct Tuples ``` struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0); } ``` Note that the black and origin values are different types because they’re instances of different tuple structs. Each struct you define is its own type, even though the fields within the struct might have the same types. For example, a function that takes a parameter of type Color cannot take a Point as an argument, even though both types are made up of three i32 values. Otherwise, tuple struct instances are similar to tuples in that you can destructure them into their individual pieces, and you can use a . followed by the index to access an individual value. ##### Note about Adding Useful Functionality with Derived Traits It’d be useful to be able to print an instance of Rectangle while we’re debugging our program and see the values for all its fields. Listing 5-11 tries using the println! macro as we have used in previous chapters. This won’t work, however. ``` struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {}", rect1); } ``` The println! macro can do many kinds of formatting, and by default, the curly brackets tell println! to use formatting known as Display: output intended for direct end user consumption. The primitive types we’ve seen so far implement Display by default because there’s only one way you’d want to show a 1 or any other primitive type to a user. But with structs, the way println! should format the output is less clear because there are more display possibilities: Do you want commas or not? Do you want to print the curly brackets? Should all the fields be shown? Due to this ambiguity, Rust doesn’t try to guess what we want, and structs don’t have a provided implementation of Display to use with println! and the {} placeholder. #### Method Syntax ##### Note about Methods in Rust Methods are similar to functions: we declare them with the fn keyword and a name, they can have parameters and a return value, and they contain some code that’s run when the method is called from somewhere else. Unlike functions, methods are defined within the context of a struct (or an enum or a trait object, which we cover in Chapter 6 and Chapter 17, respectively), and their first parameter is always self, which represents the instance of the struct the method is being called on. ##### Note about the alias &self ``` #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { // same as // fn area(self: &Self) -> u32 { // or // fn area(self: &Rectangle) -> u32 { // or fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); } ``` In the signature for area, we use &self instead of rectangle: &Rectangle. The &self is actually short for self: &Self. Within an impl block, the type Self is an alias for the type that the impl block is for. Methods must have a parameter named self of type Self for their first parameter, so Rust lets you abbreviate this with only the name self in the first parameter spot. Note that we still need to use the & in front of the self shorthand to indicate that this method borrows the Self instance, just as we did in rectangle: &Rectangle. Methods can take ownership of self, borrow self immutably, as we’ve done here, or borrow self mutably, just as they can any other parameter. ##### Note about the struct method names ``` #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } } ``` Here, we’re choosing to make the width method return true if the value in the instance’s width field is greater than 0 and false if the value is 0: we can use a field within a method of the same name for any purpose. In main, when we follow rect1.width with parentheses, Rust knows we mean the method width. When we don’t use parentheses, Rust knows we mean the field width. Often, but not always, when we give a method the same name as a field we want it to only return the value in the field and do nothing else. Methods like this are called getters, and Rust does not implement them automatically for struct fields as some other languages do. Getters are useful because you can make the field private but the method public, and thus enable read-only access to that field as part of the type’s public API. We will discuss what public and private are and how to designate a field or method as public or private in Chapter 7. ##### Note about Multiple impl blocks Each struct is allowed to have multiple impl blocks. For example, Listing 5-15 is equivalent to the code shown in Listing 5-16, which has each method in its own impl block. ``` impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } ``` There’s no reason to separate these methods into multiple impl blocks here, but this is valid syntax. We’ll see a case in which multiple impl blocks are useful in Chapter 10, where we discuss generic types and traits. ##### Note about Method Calls being a Syntactic Sugar for Function Calls Using the concepts we've discussed so far, we can now see how method calls are syntactic sugar for function calls. For example, let's say we have a rectangle struct with an area method and a set_width method: ``` impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn set_width(&mut self, width: u32) { self.width = width; } } ``` And let's say we have a rectangle r. Then the method calls r.area() and r.set_width are equivalent to this: ``` let mut r = Rectangle { width: 1, height: 2 }; let area1 = r.area(); let area2 = Rectangle::area(&r); assert_eq!(area1, area2); r.set_width(2); Rectangle::set_width(&mut r, 2); ``` The method call r.area() becomes Rectangle::area(&r). The function name is the associated function Rectangle::area. The function argument is the &self parameter. Rust automatically inserts the borrowing operator &. ### Enums and Pattern Matching #### The match Control Flow Construct ##### Note about the match statement The match arms’ patterns must cover all possibilities. Consider this version of our plus_one function, which has a bug and won’t compile: This code does not compile! ``` fn plus_one(x: Option) -> Option { match x { Some(i) => Some(i + 1), } } ``` We didn’t handle the None case, so this code will cause a bug. Luckily, it’s a bug Rust knows how to catch. If we try to compile this code, we’ll get this error: ``` match x { ^ pattern `None` not covered ``` Rust knows that we didn’t cover every possible case, and even knows which pattern we forgot! Matches in Rust are exhaustive: we must exhaust every last possibility in order for the code to be valid. Especially in the case of Option, when Rust prevents us from forgetting to explicitly handle the None case, it protects us from assuming that we have a value when we might have null, thus making the billion-dollar mistake discussed earlier impossible. ##### Note about the ##### Note about the ##### Note about the ##### Note about the ##### Note about the ##### Note about the ##### Note about the ##### Note about the

近期下载者

相关文件


收藏者