From a0f5ca9b2397f219045f19c55caea08a7e0f0be4 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 19:48:52 +0200 Subject: [PATCH 01/14] `let s = &mut String::from("hello");` -> `let mut s = String::from("hello");` --- src/language/strings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/strings.md b/src/language/strings.md index dae3cb3..9260d18 100644 --- a/src/language/strings.md +++ b/src/language/strings.md @@ -17,7 +17,7 @@ There are differences in working with strings in Rust and .NET, but the equivalents above should be a good starting point. One of the differences is that Rust strings are UTF-8 encoded, but .NET strings are UTF-16 encoded. Further .NET strings are immutable, but Rust strings can be mutable when declared -as such, for example `let s = &mut String::from("hello");`. +as such, for example `let mut s = String::from("hello");`. There are also differences in using strings due to the concept of ownership. To read more about ownership with the String Type, see the [Rust Book][ownership-string-type-example]. From 39b5e936d9b731aca4fde6495a53073f5eae9718 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 19:49:53 +0200 Subject: [PATCH 02/14] `Box::new("")` creates `Box<&str>`, not `Box<str>`. It needs to be `let str: Box<str> = Box::from("")` Or `let str = Box::<str>::from("")` --- src/language/strings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/strings.md b/src/language/strings.md index 9260d18..fcc3cee 100644 --- a/src/language/strings.md +++ b/src/language/strings.md @@ -44,7 +44,7 @@ Rust: ```rust let span: &str = "Hello, World!"; -let str = Box::new("Hello World!"); +let str: Box<str> = Box::from("Hello World!"); let mut sb = String::from("Hello World!"); ``` From 8d8e9ab3c37ece3b04198e4ed4db0445bb87c850 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 19:52:29 +0200 Subject: [PATCH 03/14] Note that `format!()` only supports bare identifiers in the string There is place to expand more on the possibilities with it (e.g `{0}` or `{name}, name = `, but it can be seen in the documentation. --- src/language/strings.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/language/strings.md b/src/language/strings.md index fcc3cee..09a76f7 100644 --- a/src/language/strings.md +++ b/src/language/strings.md @@ -116,6 +116,9 @@ let age = 42; let str = format!("Person {{ name: {name}, age: {age} }}"); ``` +Note that `format!` only supports embedding variable names in the string; more complex +expressions are spelled like `format!("1 + 1 = {}", 1 + 1)`. + Custom classes and structs can also be interpolated in C# due to the fact that the `ToString()` method is available for each type as it inherits from `object`. From 96a17ef323e642a13d821188b4487269263e7d57 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 19:53:10 +0200 Subject: [PATCH 04/14] Mention the `:#?` format specifier --- src/language/strings.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/language/strings.md b/src/language/strings.md index 09a76f7..f7e5455 100644 --- a/src/language/strings.md +++ b/src/language/strings.md @@ -186,6 +186,8 @@ println!("{person:?}"); > Note: Using the :? format specifier will use the `Debug` trait to print the > struct, where leaving it out will use the `Display` trait. +> +> You can also use the `:#?` specifier to pretty-print the debug format. See also: From 22fdebd832db46a16ebfd7ef25d358886c921416 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 19:55:02 +0200 Subject: [PATCH 05/14] Mention `to_string()` --- src/language/strings.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/language/strings.md b/src/language/strings.md index f7e5455..1725497 100644 --- a/src/language/strings.md +++ b/src/language/strings.md @@ -162,6 +162,10 @@ let person = Person { println!("{person}"); ``` +For converting values to string using `Display` without formatting, you can use +the `std::string::ToString` trait. Its `to_string()` method is equal to the `ToString()` +method in .NET, and implemented automatically whenever you implement `Display`. + Another option is to use the `std::fmt::Debug` trait. The `Debug` trait is implemented for all standard types and can be used to print the internal representation of a type. The following example shows how to use the `derive` From 7f46afe5c4c15669aa8a2d02da38936f9e373d83 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 19:58:41 +0200 Subject: [PATCH 06/14] Mention properties are rare in Rust, as opposed to in C# --- src/language/custom-types/members.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/language/custom-types/members.md b/src/language/custom-types/members.md index 0616770..820b223 100644 --- a/src/language/custom-types/members.md +++ b/src/language/custom-types/members.md @@ -162,6 +162,11 @@ impl Rectangle { } ``` +> Note: While in C# it is idiomatic to expose a property for every field and keep +> the fields private, in Rust it is more common to expose the fields directly when possible, +> since there is no syntax sugar for accessor methods and they also have complexities +> with the borrow checker. + ## Extension Methods Extension methods in C# enable the developer to attach new statically-bound From 4b5728dcf0dd101a07ef93d4b736652f7d67b3e9 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 19:59:13 +0200 Subject: [PATCH 07/14] Fix typo --- src/language/custom-types/members.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/custom-types/members.md b/src/language/custom-types/members.md index 820b223..40ac745 100644 --- a/src/language/custom-types/members.md +++ b/src/language/custom-types/members.md @@ -277,7 +277,7 @@ The table below is an approximation of the mapping of C# and Rust modifiers: ## Mutability -When designing a type in C#, it is the responsiblity of the developer to +When designing a type in C#, it is the responsibility of the developer to decide whether the a type is mutable or immutable; whether it supports destructive or non-destructive mutations. C# does support an immutable design for types with a _positional record declaration_ (`record class` or `readonly From 7f39a45052eaf74c0674911e8f2a18790540ab9a Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 20:06:47 +0200 Subject: [PATCH 08/14] Mention `std::ptr::eq()` as the equivalence to reference equality in Rust --- src/language/equality.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/language/equality.md b/src/language/equality.md index c242598..385cc34 100644 --- a/src/language/equality.md +++ b/src/language/equality.md @@ -70,6 +70,21 @@ fn main() { } ``` +There is no direct equivalence to reference equality in Rust, since not everything is a reference. +However, when you have two references, you can check for their reference equality with `std::ptr::eq()` + +```rust +fn main() { + let a = 1; + let b = 1; + println!("{}", a == b); // true + println!("{}", std::ptr::eq(&a, &b)); // false + println!("{}", std::ptr::eq(&a, &a)); // true +} +``` + +Another way to compare for reference equality is to convert the references to raw pointers and compare them using `==`. + See also: - [`Eq`][eq.rs] for a stricter version of `PartialEq`. From a68e7ff83f459d5c67f2f0cf61e5c8ab6de79e64 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 20:10:08 +0200 Subject: [PATCH 09/14] Mention the `T: Trait` shortcut to `where` clauses --- src/language/generics.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/language/generics.md b/src/language/generics.md index 9de6041..b6967e3 100644 --- a/src/language/generics.md +++ b/src/language/generics.md @@ -73,12 +73,25 @@ impl<T> Timestamped<T> { } impl<T> PartialEq for Timestamped<T> - where T: PartialEq { +where + T: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.value == other.value && self.timestamp == other.timestamp + } +} +``` + +A shortcut for `where` clauses that exists in Rust is constraining the parameters +directly at their declaration: +```rust +impl<T: PartialEq> PartialEq for Timestamped<T> { fn eq(&self, other: &Self) -> bool { self.value == other.value && self.timestamp == other.timestamp } } ``` +Although `where` clauses are somewhat more powerful, since they can constrain arbitrary types (e.g. `i32: PartialEq<T>`). Generic type constraints are called [bounds][bounds.rs] in Rust. From 23e97963119efe58ca43c5a3c7fcc53aa4734cdd Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 20:12:57 +0200 Subject: [PATCH 10/14] Mention `std::error::Error` isn't strictly required --- src/language/exception-handling.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/language/exception-handling.md b/src/language/exception-handling.md index 2fa9595..48a9602 100644 --- a/src/language/exception-handling.md +++ b/src/language/exception-handling.md @@ -54,6 +54,9 @@ The equivalent to the .NET `Exception.InnerException` property is the implementation for `Error::source()`, the blanket (default) implementation returns a `None`. +> Note: Unlike in C#, in Rust it is not strictly required for an error to implement `std::error::Error` +> (it can be used in `Result` even without that). But it is strongly encouraged, especially for public errors. + ## Raising exceptions To raise an exception in C#, throw an instance of the exception: From 6d06e527fa177878401dba2c0b20c619c43484ad Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 20:27:27 +0200 Subject: [PATCH 11/14] Demonstrate how `and_then()` is a replacement to C#'s `?` operator The example is a bit complicated, but I haven't found a better one. --- src/language/nullability-and-optionality.md | 29 ++++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/language/nullability-and-optionality.md b/src/language/nullability-and-optionality.md index bee1de7..0006963 100644 --- a/src/language/nullability-and-optionality.md +++ b/src/language/nullability-and-optionality.md @@ -48,21 +48,41 @@ if let Some(max) = max { ## Null-conditional operators The null-conditional operators (`?.` and `?[]`) make dealing with `null` in C# -more ergonomic. In Rust, they are best replaced by using the [`map`][optmap] -method. The following snippets show the correspondence: +more ergonomic. In Rust, they are best replaced by using either the [`map`][optmap] +method or the [`and_then`][opt_and_then] method, depending on the nesting of the `Option`. +The following snippets show the correspondence: ```csharp string? some = "Hello, World!"; string? none = null; -Console.WriteLine(some?.Length); // 13 +Console.WriteLine(some?.Length); // Hello, World! Console.WriteLine(none?.Length); // (blank) + +record Name(string FirstName, string LastName); +record Person(Name? Name); + +Person? person1 = new Person(new Name("John", "Doe")); +Console.WriteLine(person1?.Name?.FirstName); // John +Person? person2 = new Person(null); +Console.WriteLine(person2?.Name?.FirstName); // (blank) +Person? person3 = null; +Console.WriteLine(person3?.Name?.FirstName); // (blank) ``` ```rust let some: Option<String> = Some(String::from("Hello, World!")); let none: Option<String> = None; -println!("{:?}", some.map(|s| s.len())); // Some(13) +println!("{:?}", some.map(|s| s.len())); // Some("Hello, World!") println!("{:?}", none.map(|s| s.len())); // None + +struct Name { first_name: String, last_name: String } +struct Person { name: Option<Name> } +let person1: Option<Person> = Some(Person { name: Some(Name { first_name: "John".into(), last_name: "Doe".into() }) }); +println!("{:?}", person1.and_then(|p| p.name.map(|name| name.first_name))); // Some("John") +let person1: Option<Person> = Some(Person { name: None }); +println!("{:?}", person1.and_then(|p| p.name.map(|name| name.first_name))); // None +let person1: Option<Person> = None; +println!("{:?}", person1.and_then(|p| p.name.map(|name| name.first_name))); // None ``` ## Null-coalescing operator @@ -98,4 +118,5 @@ there is no need to use a substitute for it. [option]: https://doc.rust-lang.org/std/option/enum.Option.html [optmap]: https://doc.rust-lang.org/std/option/enum.Option.html#method.map +[opt_and_then]: https://doc.rust-lang.org/std/option/enum.Option.html#method.and_then [unwrap-or]: https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap_or From dc23171bc186c475e8957c0426f29d2b1483ff9b Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 20:29:44 +0200 Subject: [PATCH 12/14] Mention the `?` operator in Rust --- src/language/nullability-and-optionality.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/language/nullability-and-optionality.md b/src/language/nullability-and-optionality.md index 0006963..c810cda 100644 --- a/src/language/nullability-and-optionality.md +++ b/src/language/nullability-and-optionality.md @@ -85,6 +85,16 @@ let person1: Option<Person> = None; println!("{:?}", person1.and_then(|p| p.name.map(|name| name.first_name))); // None ``` +The `?` operator (mentioned in the previous chapter), can also be used to handle `Option`s. +It returns from the function with `None` if a `None` is encountered, or else continues with the +`Some` value: +```rust +fn foo(optional: Option<i32>) -> Option<String> { + let value = optional?; + Some(value.to_string()) +} +``` + ## Null-coalescing operator The null-coalescing operator (`??`) is typically used to default to another From 11b2b010a2156fc3b2206585cfb20631b596edb6 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 20:31:53 +0200 Subject: [PATCH 13/14] Mention `unwrap()` and `expect()` --- src/language/nullability-and-optionality.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/language/nullability-and-optionality.md b/src/language/nullability-and-optionality.md index c810cda..89c3948 100644 --- a/src/language/nullability-and-optionality.md +++ b/src/language/nullability-and-optionality.md @@ -124,9 +124,14 @@ lazily initialize the default value. The null-forgiving operator (`!`) does not correspond to an equivalent construct in Rust, as it only affects the compiler's static flow analysis in C#. In Rust, -there is no need to use a substitute for it. +there is no need to use a substitute for it. [`unwrap`][opt_unwrap] is close, +though: it panics if the value is `None`. [`expect`][opt_expect] is similar but allows +you to provide a custom error message. Note that as previously said, panics should +be reserved to unrecoverable situations. [option]: https://doc.rust-lang.org/std/option/enum.Option.html [optmap]: https://doc.rust-lang.org/std/option/enum.Option.html#method.map [opt_and_then]: https://doc.rust-lang.org/std/option/enum.Option.html#method.and_then [unwrap-or]: https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap_or +[opt_unwrap]: https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap +[opt_expect]: https://doc.rust-lang.org/std/option/enum.Option.html#method.expect From fa0ade54c99b35c6085edc1a24d86349b9d2c039 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman <chayimfr@gmail.com> Date: Wed, 10 Jan 2024 20:38:38 +0200 Subject: [PATCH 14/14] Mention scoped threads --- src/threading/index.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/threading/index.md b/src/threading/index.md index 42778db..7e116fc 100644 --- a/src/threading/index.md +++ b/src/threading/index.md @@ -119,6 +119,11 @@ A few things to note: `data` must be copied or cloned (depending on what the type of the value supports). + Since Rust 1.63.0, it is possible to use [scoped threads] to use non-static data + (including references to not-`move`d values) in threads. The trade-off is that + since the data must remain alive until the thread's end, it is forcibly joined + by the end of the scope. + - Rust thread can return values, like tasks in C#, which becomes the return value of the `join` method. @@ -126,3 +131,5 @@ A few things to note: Rust example, but the C# version does not need to worry about ownership since the memory behind the data will be reclaimed by the GC once no one is referencing it anymore. + +[scoped threads]: https://doc.rust-lang.org/stable/std/thread/fn.scope.html