COP 3515 - Lesson 28 - Final Review PDF

Document Details

UnmatchedJadeite2405

Uploaded by UnmatchedJadeite2405

University of South Florida

2025

Tags

C programming Rust programming computer science programming languages

Summary

This document is a review of C/Rust programming languages for a final exam. It covers topics like For Loops and iterators, and provides a look at what is included on a resume relevant to programming.

Full Transcript

An Introduction To The C/Rust Programming Languages Lesson #28: Review For Final Exam 1 What Goes On Your Resume / LinkedIn Profile? I have worked with the Visual Studio Code (VS Code), IDE, D...

An Introduction To The C/Rust Programming Languages Lesson #28: Review For Final Exam 1 What Goes On Your Resume / LinkedIn Profile? I have worked with the Visual Studio Code (VS Code), IDE, Docker, and GitHub. I have programmed in the C language. My experience with C includes creating multiple programs that used arrays, structures, and pointers. I have implemented password strength checking and parity check / checksum / two-dimensional parity checks in C. I have programmed in the Rust language. My experience with Rust includes creating multiple programs that used tuples, arrays, vectors, structures, and pointers along with packages and crates. – I have implemented a hamming code encoder / decoder in Rust. I have implemented the Rivest-Shamir-Adleman (RSA) algorithm in Rust. 2 Image Credit: http://clipart-library.com/resume-cliparts.html Spring Fall 2025 3 Tell Your Friends! Dr. Anderson will be teaching this class, CGS 2060 during the Spring Semester!!! It’s a big class – room for everyone! Share the love! 4 Student Assessment of Instruction 75% = 1 extra credit point 90% = 2 extra credit points 2% of 79 = 2 students 57 left to go to get to 75% Note: the email will be from "[email protected]" 5 Let's Talk About Quiz #4 / Final Exam This is a closed book test -- no notes! The final is in-class: Monday, 12/9 at 7:30am. You will have 75 minutes to take this exam. Bring your laptops! The test is worth 20 points. There are 25 questions on this test and each question is worth between 0.5 and 1 point. Don't be a cheater. Image Credit: https://www.clipartmax.com/so/computer-test-clipart/ The Secret Of The Red Dot 7 An Introduction To The C/Rust Programming Languages Lesson #21: For, Functions 8 For Loop The for loop is a conditional loop, which means it runs for a specific amount of time. The behavior of the for loop in the rust language differs slightly from that of other languages. The for loop executes until the condition is met. Example: fn main() { for x in 1..12 { print!(“{} “,x); } } An expression in the example can turn into an iterator that iterates across the elements of a data structure. An iterator is used to retrieve the value of each iteration. The loop ends when there are no more values to be fetched. Processing a Series of Items with Iterators The iterator pattern allows you to perform some task on a sequence of items in turn. An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished. When you use iterators, you don’t have to reimplement that logic yourself. In Rust, iterators are lazy, meaning they have no effect until you call methods that consume the iterator to use it up. For example, the following code creates an iterator over the items in the vector v1 by calling the iter method defined on Vec. This code by itself doesn’t do anything useful: let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); 10 Processing a Series of Items with Iterators The iterator is stored in the v1_iter variable. Once we’ve created an iterator, we can use it in a variety of ways. In the next example, we separate the creation of the iterator from the use of the iterator in the for loop. When the for loop is called using the iterator in v1_iter, each element in the iterator is used in one iteration of the loop, which prints out each value: let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {}", val); } The key thing to understand is that iterators are consumed: you can only move through them once before they are "used up" and are no longer useful. 11 for and range The for in construct can be used to iterate through an Iterator. One of the easiest ways to create an iterator is to use the range notation a..b. This yields values from a (inclusive) to b (exclusive) in steps of one. for n in 1..101 { // note that this only covers 1 - 100 Alternatively, a..=b can be used for a range that is inclusive on both ends. for n in 1..=100 { To iterate in reverse order we use the.rev() method: for i in (0..10).rev() 12 3 Ways To Loop Over A List The for in construct is able to interact with an Iterator in several ways. By default the for loop will apply the into_iter function to the collection. However, this is not the only means of converting collections into iterators. into_iter, iter and iter_mut all handle the conversion of a collection into an iterator in different ways, by providing different views on the data within. 13 3 Ways To Loop Over A List iter - borrows each element of the collection through each iteration. Thus leaving the collection untouched and available for reuse after the loop. let names = vec!["Bob", "Frank", "Ferris"]; for name in names.iter() { // names is unaffected by this loop into_iter - This consumes the collection so that on each iteration the exact data is provided. Once the collection has been consumed it is no longer available for reuse as it has been 'moved' within the loop. let names = vec!["Bob", "Frank", "Ferris"]; for name in names.into_iter() { // names is used up by this loop iter_mut - This mutably borrows each element of the collection, allowing for the collection to be modified in place. let mut names = vec!["Bob", "Frank", "Ferris"]; for name in names.iter_mut() { // names can be changed by this loop 14 Array Examples: Using The iter() Function The iter() method returns the values of all array items. Example: fn main(){ let arra:[i32;4]=[20,30,80,50]; println!(“array {:?}”,arra); println!(“array size :{}”,arra.len()); for vals in arra.iter(){ println!(“value :{}”,vals); } } FUNCTIONS Rust Functions Functions are the building blocks of understandable, manageable, and reusable code. A function is a collection of statements used to carry out a specified activity. Functions structure the program into logical pieces of code. Functions can invoke access code after they have been defined. As a result, the code is reusable. Furthermore, functions make the program’s code easier to comprehend and maintain. The name, return type, and function parameters are all specified in a function declaration. A function definition defines the body of the function. Benefits Of Using Functions 1. Reusability: The first reason is reusability. Once a function is defined, it can be used over and over and over again in your program. You can invoke the same function many times in your program, which saves you work. 2. Duplication: The second reason is because a single function can be used in several different programs. When you need to write a new program, you can go back to your old programs, find the functions you need, and duplicate those functions in your new program. 3. Abstraction: The third reason is abstraction. If you just want to use the function in your program, you don't have to know how it works inside! You don't have to understand anything about what goes on inside the function – it just works! 18 Functions Example: fn say_hello() { println!("Hello, world!"); } fn main() { say_hello(); } we define say_hello just like we define main. Then, inside the body for main, we call the function by putting the name, followed by parentheses, followed by a semicolon. This is very similar to how we call the println macro. Defining Functions A function definition describes what and how a given activity would carry out. The function body includes the code that the function should run. The guidelines for naming a function are the same as those for naming variables. The fn keyword is used to define functions. A function declaration may or may not include parameters/arguments. Function example: //Defining function fn fn hello(){ println!(“hello“); } Function Order In C, if you call a function before you reach it you have to create a function declaration. A function declaration is used to tell the complier about what value a C function will be returning and what values its parameters use. In Rust, the process of compiling works like this: – Scan all the code making note of all the functions found – Compile the code that may use functions This means that in Rust, functions can come in any order – you can call a function before that function's code has been reached. 21 Calling Functions In order to call a function, we put the name of the function, followed by an open parenthesis, followed by the parameter list, followed by a close parenthesis. A function call itself is a Rust expression. The argument list when calling a function is a comma-separated list of arguments. The argument names we use in calling a function have nothing to do with the names for the parameters inside the function. We can also call functions multiple times with different arguments. And each time we do that, the parameter variable in the function refers to a different value. Invoking A Function To run a function, it must be invoked. This is known as function invocation. The function that calls another function is referred to as the caller function. Example: fn main(){ //calling function fn_hello(); } //Defining function fn fn_hello(){ println!(“hello fn_hello “); } Function with the Parameters Parameters are a way for values to be sent to functions. The function’s signature includes parameters. During the function’s call, the argument values are supplied to it. Unless otherwise indicated, the number of values provided to a function must equal the number of arguments defined. One of the following methods can be used to send parameters to a function. – Pass by value – Pass by reference Function Parameters An argument is a value passed into a function from its caller. The function receives the sent argument and places it into a parameter. Parameters allow a function to do different things based on how they are called. Parameter Example: fn say_apples(apples: i32) { println!("I have {} apples", apples); } fn main() { say_apples(10); } Argument Function Parameters Our function say_apples takes a single parameter. We name that parameter apples, and it’s now a variable we can use inside the say_apples function. When we define a parameter to a function, we also need to give it a type. Here, we’ve said that the type of apples is a signed 32-bit integer, or an i32. Pass By Value A new storage location for each value argument is generated when a method is called. The real parameter values are transferred into them. As a result, changes to the parameter within the called method do not affect the argument. This example declares a variable no, which is initially set to 7. The variable is passed as a parameter (by value) to the method mutate_no_to_zero(), which transforms the value to zero. When control returns to the main method after the function call, the value will be the same. Pass By Value Example: fn main(){ let no:i32=7; mutate_no_to_zero(no); println!(“Value of no:{}”,no); } fn mutate_no_to_zero(mut param_no: i32) { param_no=param_no*0; println!(“param_no value:{}”,param_no); } Arrays as Parameters to Functions An array can be passed to functions as a value or as a reference. Example: Pass by Value fn main() { let arra=[20,40,80]; update(arra); print!(“Inside-main {:?}”,arra); } fn update(mut arra:[i32;3]){ for i in 0..3 { arra[i]=0; } println!(“Inside-update {:?}”,arra); } Function Results Every function produces a result value. By default, the result type of a function is the trusty old unit (). Remember that in C the default value was void. How do we say what the result type of a function is? We use an "arrow": fn main() -> () { println!("Hello, world!"); } Adding the -> () to our main function’s definition doesn’t change anything. The default result type is unit, and now we’ve explicitly said that it’s unit. Function Results Let’s write a function that doubles any number you give it. This is going to take an i32 and give you back an i32. We can talk about the function’s signature as what it looks like from the outside: fn double(x: i32) -> i32 The way we read this is "double is a function which takes one parameter, an i32, and gives you back an i32 as the result." But how do we produce a result from the function? Function Results Example: fn double(x: i32) -> i32 { x*2 } fn main() -> () { println!("3 * 2 == {}", double(3)); } Did you see a missing symbol in the body of double? That’s right, there’s no semicolon after x * 2. Remember a few important things: – A function body is a block – A block may optionally end with an expression – If a block ends with an expression, then evaluating the block results in the value of that expression If you want your function to result in a specific value, you provide an expression at the end with the value you want. In C we used return to specify this value. An Introduction To The C/Rust Programming Languages Lesson #22: Strings, Stack, Heap 33 Strings In Rust In Rust, the string data type is divided into the following categories: – String Literal(&str) – String Object(String) When value of a string is known at build time, string literals (&str) are utilized. String literals are a collection of characters that have been hardcoded into a variable [this means that they are unchangable] and they are stored on the stack. Assume company="Amazon" is an example. String literals can be found in the std::str package. String literals are also referred to as string slices. Example: fn main() { let company:&str="Amazon"; let location:&str="California"; println!("company : {} location :{}", company, location); } String Literals String literals are by default static. 'static indicates that the data pointed to by the reference lives for the entire lifetime of the running program. This ensures that string literals are guaranteed to be valid throughout the program. We may also specify the variable directly as static. Example: fn main() { let company:&’static str="Amazon"; let location:&’static str="California"; println!("company : {} location :{}", company,location); } String Object The String object type is available in the Standard Library. Unlike the string literal, the String object type is not part of the core language. The standard library pub struct String defines it as a public structure. The string is a collection that can expand. It is a mutable type with UTF-8 encoding. The String object type can be used to represent string values that are sent to the program at runtime. The heap is used to allocate a String object. Building Strings We may use any of the following syntax to build a String object: – String::new() This syntax generates an empty string. – String::from() – This syntax generates a string containing a default value passed as a parameter to the from() function. Example: fn main(){ let empty_string=String::new(); println!("length {}",empty_string.len()); let content_string=String::from("Amazon"); println!("length {}",content_string.len()); } Common String Methods String Memory And Allocation In languages with a garbage collector, the GC maintains track of and cleans up memory that is no longer in use, so we don’t have to bother about it. Without a GC, we must recognize when memory is no longer being utilized and call code to explicitly return it, as we did to request it. Historically, doing this right has been a challenging programming task. We will waste memory if we forget. We’ll have an invalid variable if do it too soon. It’s also a bug if we do it twice. We must match precisely one allocate to exactly one free. Rust has a different approach: when the variable that owns the memory exits scope, the memory is immediately returned. String Memory And Allocation Here’s a variant of our example that uses a String instead of a string literal: { let st=String::from(“helloo”); // st is valid from this point forward // do stuff with st } // this scope is now over, and st is no longer valid When st passes out of scope, we may naturally return the memory our String requires to the allocator. When a variable is no longer in scope, Rust invokes a specific function for us. Drop is the name of this method, and it is where the author of String may write the code to return the memory. Rust calls are dropped automatically at the final curly bracket. This pattern has a significant influence on how Rust code is written. It may appear easy today, but code behavior might be unexpected in more complex cases where we want several variables to consume the data we’ve placed on the heap The Stack & The Heap All data stored Data without a on the stack known size at must have a compile time or known fixed with a size that size at may change compile time. will be stored FIFO. on the heap. Rust stores variables on either its stack or its heap The behavior (speed, size, etc.) is different between the two options. 41 Image Credit: https://medium.com/@Miguel_Grillo/stack-vs-heap-and-the-virtual-memory-ea075ea6e3fc The Stack And The Heap Stack and Heap have to do with memory management. The stack and the heap are abstractions that help us determine when to allocate and deallocate memory. The stack is very fast and is where memory is allocated in Rust by default. But the allocation is local to a function call and is limited in size. The heap, on the other hand, is slower, and is explicitly allocated by our program. But it’s effectively unlimited in size and is globally accessible. 42 The Stack Since we know all the local variables we have ahead of time, we can grab the memory all at once. And since we’ll throw them all away at the same time as well, we can get rid of it very fast too. The downside is that we can’t keep values around if we need them for longer than a single function. We also haven’t talked about what the word, ‘stack’, actually means. 43 The Heap The stack works pretty well, but not everything can work like this. Sometimes, you need to pass some memory between different functions, or keep it alive for longer than a single function’s execution. For this, we can use the heap. In Rust, a Box is a data type that allows you to store a value on the heap. Here’s an example: fn main() { let x = Box::new(5); let y = 42; } 44 An Introduction To The C/Rust Programming Languages Lesson #23: Ownership, References, Borrowing 45 Understanding Ownership: Move, Clone, Copy What Is Ownership In Rust? The primary aspect of Rust is ownership. Although the characteristic is simple to describe, it has deep implications for the rest of the language. All programs must manage how they use memory while running on a computer. Some languages offer garbage collection [i.e. Java], which searches for no longer utilized memory while the program runs; in others, the programmer must actively allocate and delete memory [i.e. C]. Rust has a third approach: memory is controlled using an ownership system with rules that the compiler validates at compile time. While our software is running, none of the ownership aspects will slow it down. Ownership Concepts The “owner” can modify the ownership value of a variable based on its mutability. The ownership of a variable can transfer to another variable. In Rust, ownership is just a matter of semantics. In addition, the ownership concept ensures safety Rules Of Ownership In Rust, each value has a variable called its owner. At any one moment, there can only be one owner. When the owner exits the scope, the value is destroyed (also known as being freed). Variable Scope Let's look at the scope of several variables as a first illustration of ownership. A scope is the range of items that are valid within a program. Assume we have a variable that looks something like this: let st=“hello”; The variable st refers to a literal string, the value of which is hardcoded into the program’s text. The variable is valid from the time it is declared until the current scope expires. Variable Scope This example includes comments that indicate when the variable st is valid. // st is not valid here, it’s not yet declared { let st=“hello”; // st is valid from this point forward // do stuff with st } // this scope is now over, and st is no longer valid In other words, there are two critical time points here: 1. It is valid when st enters the scope. 2. It is still valid until it goes out of scope. The connection between the scope and when variables are valid is comparable to that of other programming languages at this stage. How Variables and Data Interact: Move In Rust, several variables can interact with the same data in various ways. We now look at an example with an integer. let a=8; let x=a; let b=x; “Bind the value 8 to a; then make a copy of the value in x and bind it to b.” We now have two variables, a and b, equal to 8. This is correct because integers are simple values with a known, defined size, and these two 8 values are placed into the stack. Let’s have a look at the String version: let st1=String::from(“hello”); let st2=st1; This code appears to be quite similar to the preceding code, so we can conclude that the function is the same: the second line would duplicate the value in st1 and bind it to st2. How Variables and Data Interact: Move There is one additional element to what occurs in this circumstance in Rust to ensure memory safety. Rust considers st1 invalid after letting st2 = st1. As a result, when st1 exits scope, Rust does not need to release anything. Examine what happens if we try to utilize st1 after st2 is generated; it will not work: let st1=String::from(“hello”); let st2=st1; println!(“{}, everyone”, st1); Ownership And Moving Blocks can also be owners. Example: fn main() { { let x: i32 = 5; println!("{}", x); } } main owns that block, and the block owns the value 5. And values can even own other values. Remember that you can only have one owner for a value at a time. No Ownership Problem: Copy Example: fn count(apples: i32) { println!("You have {} apples", apples); } fn price(apples: i32) -> i32 { apples * 8 } fn main() { let apples: i32 = 10; count(apples); let price = price(apples); println!("The apples are worth {} cents", price); } Why Don't We Have A Problem? Copy is a trait in Rust that says "this thing is so incredibly cheap to make a copy of, that each time you try to move it, its fine to just make a copy and move that new copy instead." And i32 is an example of a type which is so cheap. Therefore, in our code here, count(apples) doesn’t move the value into count. Instead, it makes a copy of the value 10, and moves that copy into count. But the original 10 inside the apples variable remains unchanged. Stack-Only Data: Copy The Rust annotation Copy trait may be applied to types like integers stored on the stack. If a type has the Copy trait, an older variable can still be used after the assignment has been performed. Rust will not allow us to annotate a type with the Copy trait if the type or any of its components has the Drop trait implemented. If we add the Copy annotation to a type that requires anything specific to happen when the value is out of scope, we will get a compile-time error. Variables and Data Interactions: Clone We may use the clone method to thoroughly duplicate the String’s heap data rather than merely the stack data. [also called a "deep copy"] Here’s an example of how to use the clone method: let st1=String::from(“hello”); let st2=st1.clone(); println!(“st1={}, st2={}”, st1, st2); This works well and generates the behavior in the previous example, where the heap data is explicitly copied: both st1 and st2 now contain the string "hello". Performing a clone call might be costly to perform depending on the variable that is being duplicated. Return Values and Scope Ownership can also be transferred by returning values. Every time, the ownership of a variable follows the same pattern: assigning a value to another variable changes it. When a variable that includes heap data exits scope, the value is destroyed unless the data has been transferred to be held by another variable. Example: Return Values and Scope fn main() { let st1=gives_ownership(); // gives_ownership moves its return value into st1 let st2=String::from(“hello”); // st2 comes into the scope let st3=takes_and_gives_back(st2); // st2 is moved into takes_and_gives_back, which also moves its return value into st3. } // Here, st3 goes out of the scope and is dropped. st2 was moved, so nothing happens. // st1 goes out of the scope and is dropped. fn gives_ownership() ->String { // gives_ownership will move its return the value into function that calls it let some_string=String::from(“yours”); // the some_string comes into scope some_string // the some_string is returned and moves out to calling function } // This function takes String and returns one fn takes_and_gives_back(a_string: String) ->String {// a_string comes into scope a_string // a_string is returned and moves out to the calling a function } Return Values and Scope Taking ownership and then restoring ownership with each function is time- consuming. What if we want a function to utilize a value but not own it? It is inconvenient because whatever we send data to a function, in addition to any data originating from the function’s body that we might want to return, it must be sent back if we want to use it again. A tuple can be used to return many values: fn main() { let st1=String::from(“hello”); let (st2, len)=calculate_length(st1); println!(“length of ‘{}’ is {}.”, st2, len); } fn calculate_length(st: String) ->(String, usize) { let length=st.len(); // len() returns the length of a String (st, length) } References & Borrowing References And Borrowing In Rust A reference is an address passed as an argument to a function. Borrowing is similar to when we borrow something and then return it after we are through with it. Borrowing and references are mutually exclusive, which means that when a reference is released, the borrowing also ends. References - Borrow Example: fn increase_fruit(mut numFruit: Fruit) -> Fruit { numFruit *= 2; numFruit } fn print_fruit(numFruit: Fruit) -> Fruit { println!("You have {} pieces of fruit", numFruit.apples+numFruit.bananas); numFruit } fn main() { let fruit = 10; let fruit = print_fruit(fruit); let fruit = increase_fruit(fruit); print_fruit(fruit); } Problem: we have to create another fruit variable because we have to move the value of fruit both in and out of the routine print_fruit. Borrowed References The problem with this code: let fruit = print_fruit(fruit); We don’t want to have to move the value in and back out. Instead, we’d like to be able to let print_fruit borrow the value we own in main, without moving it completely. Good news - Rust supports exactly that! Instead of passing print_fruit the fruit value itself, we need to pass it a borrowed reference. There’s a new unary operator to learn for this: &. Borrowed References It turns out that when you borrow a value of type Fruit, you don’t get back a Fruit. Instead, you get a &Fruit. That & at the beginning of the type means "a reference to." In other words, & has two different but related meanings: – When on a value: borrow a reference to this value – When on a type: a reference to this type Right now, the type of the parameter to print_fruit is Fruit. This requires that the value be moved into print_fruit. Instead, let’s change that so that it’s a reference to a Fruit, or &Fruit: fn print_fruit(numFruit: &Fruit) -> Fruit Borrowed References Error: the only reason we were returning a Fruit in the first place was to deal with moving and ownership. But we don’t actually need that anymore! So instead, let’s get rid of the return value entirely: fn print_fruit(numFruit: &Fruit) { println!("You have {} pieces of fruit", numFruit.apples+numFruit.bananas); } We now replace: let fruit = print_fruit(&fruit); with: print_fruit(&fruit); References Just like in C, every value in Rust lives somewhere in your computer’s memory. And every place in computer memory has an address. It’s possible to use println and the special {:p} syntax to display the address itself: fn main() { let x: i32 = 5; println!("x == {}, located at {:p}", x, &x); } References Just like in C, a reference can be thought of as a pointer: it’s an address pointing at a value that lives somewhere else. That’s also why we use the letter p in the format string to print the address. When you have a variable like let y: &i32 = &x, what this means is: – y is an immutable variable – That variable holds an address – That address points to an i32 – The reference is immutable, so we can’t change the value of y On the other hand, let y: &mut i32 = &mut x is almost exactly the same thing, except for the last point. Since the reference is mutable, we can change the y value. Dereferences Example: fn main() { let x: i32 = 5; let mut y: i32 = 6; let z: &mut i32 = &mut y; z -= 1; assert_eq!(x, y); println!("Success"); } Does not work, The problem is that we’re trying to use the -= operator on a &mut i32 value. The reference is really just an address, not an i32. We don’t want to subtract 1 from an address. We want to subtract 1 from the value behind the reference. Dereferences Just like in C, Rust provides another unary operator to talk about the thing behind a reference. It’s called the deref—short for dereference —operator, and is *. Example: fn main() { let x: i32 = 5; let mut y: i32 = 6; let z: &mut i32 = &mut y; *z -= 1; assert_eq!(x, y); println!("Success"); } Dangling References In pointer-based languages, it’s possible to construct a dangling pointer, which refers to a place in memory that may have been passed to someone else, by releasing some memory while retaining a pointer to that region. In contrast, the compiler in Rust ensures that references are never dangling: if we have a reference to some data, the compiler will ensure that the data does not go out of the scope before the reference to the data does. Let’s attempt making a dangling reference, which Rust will reject with a compile- time error: Example: fn main() { let reference_to_nothing=dangle(); } fn dangle() ->&String { let st=String::from(“hello”); &st } An Introduction To The C/Rust Programming Languages Lesson #24: Structures, Slices 73 STRUCTS Fruit Let’s say we sell fruit. We need to know how many apples and bananas we have. We write a program to tell us: fn count_fruit(apples: i32, bananas: i32) { println!("I've got {} apples and {} bananas", apples, bananas); } fn main() { count_fruit(10, 5); } Fruit Now we realize we need to check how much our fruit is worth. It turns out we can sell an apple for 8 cents, and a banana for 12 cents. So we write a helper function called price_fruit that returns the price of all our fruit: fn price_fruit(apples: i32, bananas: i32) -> i32 { apples * 8 + bananas * 12 } Call it in the main function: fn main() { count_fruit(10, 5); let price = price_fruit(10, 5); println!("I can make {} cents", price); } Problems with the Fruit Program To make things better we need to be able to: – Have the ability to see easily which number means apples, and which means bananas – Have the ability to reuse the combination of apples and bananas – Create some way to just increase that combined information about apples and bananas Struct A struct in Rust allows you to create a new data type made up of values from other data types (just like in C). We can combine these primitives into larger, custom types. In this case, we want to define a new data type called Fruit, which tells us how many apples and bananas we have. Let’s see what that looks like: struct Fruit { apples: i32, bananas: i32 } struct starts a declaration, similar to how fn starts a declaration. Note that a struct occurs outside of any function. Struct After the type name, we have an open curly brace, followed by a comma separated list of fields. For example, apples: i32, is a field. The field has a name, a colon, and then a type. Note that each field except for the last one ends with a "," This is really similar to what a parameter list in a function looks like. Using Structs Here we get to introduce a new kind of expression: a field access expression. When you have a value of some struct, you can access it with value.field syntax. Let’s see how we would implement our count_fruit function, now powered by a struct: fn count_fruit(fruit: Fruit) { println!("I've got {} apples and {} bananas", fruit.apples, fruit.bananas); } Our parameter list now receives a single parameter, named fruit, with type Fruit. The println! has the same format string as before. But instead of giving it the parameters apples and bananas, we have to access those fields on the fruit value. So we use fruit.apples and fruit.bananas. 3 Ways To Create A Struct There are three different ways to create a struct: A struct in which each of the components is given a unique name: struct User { username: String, } Tuple–like structs that identify the struct values in the order in which they appear: struct User(string); A Unit like struct: struct User; // useful when dealing with Traits in Rust 81 Using Structs You can get the value of a field by querying it via dot notation. let x = fruit.apples; let y = fruit.bannanas; You may wonder: what’s the syntax to get the value of a specific field of a tuple struct (a struct in which the individual elements don't have names)? Fortunately, Rust has chosen the simplest way possible, by indexing tuple components starting from zero. let x = fruit.0; let y = fruit.1; It’s possible to change the value of a field using dot notation, but the struct variable must be defined as mutable. let mut my_fruit = Fruit{apples: 15,bananas:20}; my_fruit.apples = 10; 82 Struct Expression We need to know how to call the count_fruit function. And in order to do so we’ll need to learn one more expression type: a struct expression. This resembles the struct declaration itself quite closely, and looks like this: let fruit = Fruit { apples: 10, bananas: 5, }; Struct Expression We give the name of the struct, followed by an opening curly brace, followed by a comma-separated list of struct field expressions. In the struct declaration, we used name: type, such as apples: i32. Now that we’re constructing a value, we instead use name: value, such as apples: 10. Finally, calling the count_fruit function looks just like passing in any other parameter to a function: count_fruit(fruit); 84 Slices A slice, written [T] without specifying the length, is a region of an array or vector. Since a slice can be any length, slices can’t be stored directly in variables or passed as function arguments. Slices are always passed by reference. A reference to a slice is a fat pointer: a two-word value comprising a pointer to the slice’s first element, and the number of elements in the slice. Suppose we run the following code: let v: Vec = vec![0.0, 0.707, 1.0, 0.707]; let a: [f64; 4] = [0.0, -0.707, -1.0, -0.707]; let sv: &[f64] = &v; let sa: &[f64] = &a; 85 Slices On the first two lines, Rust automatically converts a &Vec reference and a &[f64; 4] reference to slice references that point directly to the data. Whereas an ordinary reference is a non-owning pointer to a single value, a reference to a slice is a non-owning pointer to several values. This makes slice references a good choice when you want to write a function that operates on any homogeneous data series, regardless of whether it’s stored in an array or a vector, stack or heap. For example, here’s a function that prints a slice of numbers, one per line: fn print(n: &[i32]) { for elt in n { println!("{}", elt); } } print(&v); // works on vectors 86 print(&a); // works on arrays Using Slices Similarly, we can remove the trailing number if our slice includes the String’s last byte. That is to say, they are both equal: let st=String::from(“hello”); let len=st.len(); let slice=&st[3..len]; let slice=&st[3..]; Alternatively, we can drop both values to get a slice of the complete string. As a result, these are equal: let st=String::from(“hello”); let len=st.len(); let slice=&st[0..len]; let slice=&st[..]; An Introduction To The C/Rust Programming Languages Lesson #26: Packages & Crates, Methods, Enums 88 Methods Methods are similar to functions They differ because methods are defined with the context of a struct, enum, or trade object. Methods always have their first parameter as "self" which is representing the instance of the struct being called on. Methods are declared in the same way functions are: with the fn keyword and their name. They may have parameters as well as a return value. They include code that is executed when they are called from another location. Methods are started with the "impl" keyword which is short for implementation. 89 Defining Methods Let’s create an area method defined on the Rectangle struct, which takes a Rectangle instance as a parameter. Example: Note that the name of #[derive(Debug)] the method is the same struct Rectangles { as the name of the width: u32, struct height: u32, } 1st parameter is "self" which refers impl Rectangles { to the rect1 struct variable that fn area(&self) ->u32 { called the area method self.width * self.height } } fn main() { let rect1=Rectangles { width: 40, height: 60, }; println!(“Area of the rectangle {} square pixels.”,rect1.area()); } Defining Methods We start an impl (implementation) block for Rectangles to specify the function within the context of Rectangles. The Rectangles type will apply to everything in this impl block. Then, within the impl curly brackets, we transfer the area function and change the first parameter to self in the signature and throughout the body. Instead of calling the area function with rect1 as an argument in main, we may use method syntax to call the area method on our Rectangles instance. After an instance, the method syntax is added: a dot, followed by the method name, parentheses, and any parameters. Enums Let’s look at a circumstance we would want to represent in code and see why enums are more useful and appropriate in this case than structs. Let’s pretend we have to operate with IP addresses. Two primary standards are now in use for IP addresses: version four (IPv4) and version six (IPv6). These are the only IP address options our program will encounter: we can enumerate all conceivable versions of the term “enumeration.” Any IP address can be either version four or version six, but not both at once. The enum data structure is appropriate because enum values can only be one of the several types of IP addresses. 92 Enums Because both version four and version six addresses are fundamentally IP addresses, they should be handled as the same type when the code is dealing with situations involving any sort of IP address. This concept can be expressed in code by defining an IpAddrKind enumeration, which contains the many types of IP addresses that can exist, such as V4 and V6. These are the enum’s variants: enum IpAddrKind { V4, V6, } We can now utilize IpAddrKind as a custom data type in other parts of our code. 93 Enum Values We may make instances of each of the two IpAddrKind versions as follows: let present=IpAddrKind::V4; let future=IpAddrKind::V6; Note that the enum’s variations are namespaced under its identifier, and a double colon separates the enum type from its value. This is advantageous because both IpAddrKind::V4 and IpAddrKind::V6 are now of the same type: IpAddrKind. Then, for example, we may write a function that accepts any IpAddrKind. fn route(ip_kind: IpAddrKind) {} And we can call this function one of two ways: route(IpAddrKind::V4); route(IpAddrKind::V6); There are many more benefits to using enums. 94 An Introduction To The C/Rust Programming Languages Lesson #27: Pointers, Smart Pointers, Reference Safety 95 Rust Pointer Types Rust has several types that represent memory addresses. This is a big difference between Rust and most languages with garbage collection. In Java, if class Tree contains a field Tree left;, then left is a reference to another separately-created Tree object. Objects never physically contain other objects in Java. Rust is different. The language is designed to help keep allocations to a minimum. Values nest by default. 96 Image Credit: https://www.freepik.com 3 Types Of Smart Pointers In Rust A pointer is a general concept for a variable that contains a memory address which points to some data. In Rust, the most common type of pointer are references (variables that use the &). Reference pointers do not give us any additional capabilities. However, Smart pointers are Rust pointers that give us additional capabilities. Rust supports three different types of smart pointers: 1. Reference Counting (RC) – reference counting type that allows multiple ownership 2. Box – allocates values on the heap 3. Ref / Ref Mut (unsafe) – accessed through ref cell which enforces the borrowing rules at runtime instead of compile time. 97 Image Credit: https://www.freepik.com Pointer Types The value ((0, 0), (1440, 900)) is stored as four adjacent integers. If you store it in a local variable, you’ve got a local variable four integers wide. Nothing is allocated in the heap. This is great for memory efficiency, but as a consequence, when a Rust program needs values to point to other values, it must use pointer types explicitly. The good news is that the pointer types used in safe Rust are constrained to eliminate undefined behavior, so pointers are much easier to use correctly in Rust than in C and C++. We’ll discuss the three traditional pointer types here: references, boxes, and unsafe pointers. 98 Image Credit: https://www.freepik.com References A value of type &String is a reference to a String value, an &i32 is a reference to an i32 value, and so on. It’s easiest to get started by thinking of references as Rust’s basic pointer type. A reference can point to any value anywhere, stack or heap. The address-of operator, &, and the deref operator, * are used to both load and then use addresses in Rust, just as their counterparts in C work on pointers. And like a C pointer, a reference does not automatically free any resources when it goes out of scope. Rust references are never null. 99 Image Credit: https://www.freepik.com References One difference is that Rust references are immutable by default: &T - immutable reference, like const T* in C &mut T - mutable reference, like T* in C Another major difference is that Rust tracks the ownership and lifetimes of values, so many common pointer-related mistakes are ruled out at compile time. References must never outlive their referents. Rust requires it to be apparent simply from inspecting the code that no reference will out-live the value it points to. Rust refers to creating a reference to some value as “borrowing” the value: what you have borrowed, you must eventually return to its owner. 100 Image Credit: https://www.freepik.com Boxes The simplest way to allocate a value on the heap is to use Box::new. let t = (12, "eggs"); let b = Box::new(t); // allocate a tuple in the heap The type of t is (i32, &str), so the type of b is Box. Box::new() allocates enough memory to contain the tuple on the heap. When b goes out of scope, the memory is freed immediately, unless b has been moved—by returning it from a function, for example. 101 Image Credit: https://www.freepik.com Box Smart Pointer A Box pointer allows us to allocate data on the heap instead of on the stack. Note that the pointer to the data on the heap will remain on the stack. Example: fn main() { let t = (12,"eggs"); let b = Box::new(t); println!("{:?}",b); } b contains a pointer to t t was stored on the stack. b was stored on stack. b pointed to storage that was on the heap. 102 Image Credit: https://www.freepik.com Box Smart Pointer Pointers also come with a dereference operator ("*") Example: let x1 = 5; let y1 = &x1; assert_eq!(5,x1); assert_eq!(5, *y1); Deallocation gives us the value that is stored at a given memory address. The same thing can be done using a Box: let x2 = 5; let y2 = Box::new(x2); assert_eq!(5,x2); assert_eq!(5, *y2); 103 Image Credit: https://www.freepik.com Raw Pointers Rust also has the raw pointer types *mut T and *const T. Raw pointers really are just like pointers in C and C++. Using a raw pointer is unsafe, because Rust makes no effort to track what a raw pointer points to. For example, the pointer may be null; it may point to memory that has been freed or now contains a value of a different type. All the classic pointer mistakes of C and C++ are offered for your enjoyment in unsafe Rust. 104 Image Credit: https://www.freepik.com RefCell A RefCell is another way to change values without needing to declare mut Interior mutability (used by RefCell) is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data: normally, this action is disallowed by the borrowing rules. To do so, the pattern uses unsafe code inside a data structure to bend Rust’s usual rules that govern mutation and borrowing. This is typically not allowed. This does use unsafe code. RefCell rules are enforced at runtime which means that the compiler will typically not catch any errors for us. If an error is present at runtime, then the program will panic and terminate. 105 Image Credit: https://www.freepik.com Panic! There are two types of errors that a Rust program can encounter. The first is called a "recoverable" error - most errors aren’t serious enough to require the program to stop entirely. Sometimes when a function fails it’s for a reason that you can easily interpret and respond to. For example, if you try to open a file and that operation fails because the file doesn’t exist, you might want to create the file instead of terminating the process. The second is called a "unrecoverable" error. Sometimes bad things happen in your code, and there’s nothing you can do about it. An example would be if we take an action that causes our code to panic 106 (such as accessing an array past the end). Image Credit: Microsoft Bing Image Creator Reference Safety As we’ve presented them so far, references look pretty much like ordinary pointers in C or C++. But those are unsafe; how does Rust keep its references under control? Perhaps the best way to see the rules in action is to try to break them. 107 Image Credit: https://www.freepik.com Borrowing A Local Variable Here’s a pretty obvious case. You can’t borrow a reference to a local and take it out of the local’s scope: let r; { let x = 1; r = &x; } assert_eq!(*r, 1); // bad: reads memory `x` used to occupy 108 Image Credit: https://www.freepik.com Borrowing A Local Variable The Rust compiler rejects this program, with a detailed error message: 109 Borrowing A Local Variable Here, the error messages talk about the lifetime of x. The compiler’s complaint is that the reference r is still live when its referent x goes out of scope, making it a dangling pointer— which is not permitted. While it’s obvious to a human reader that this program is broken, it’s worth looking at how Rust itself reached that conclusion. Even this simple example shows the logical tools Rust uses to check much more complex code. Rust tries to assign each reference in your program a lifetime that meets the constraints imposed by how the reference is used. A lifetime is some stretch of your program for which a reference could live: a lexical block, a statement, an expression, the scope of some variable, or the like. 110 Image Credit: https://www.freepik.com That’s All! Final 111

Use Quizgecko on...
Browser
Browser