Variables

Rust’s approach to variables is straight-forward, but strict in a way that requires clear understanding.

(Im)Mutability

Rust variables are immutable by default. This difference from other languages is relatively easy to comprehend. .NET developers are accustomed to the immutability of string, for example, so transitioning to Rust’s mutability model is pretty easy.

Variable mutability is declarative in Rust, a bit like accessibility in .NET. Developers use mut keyword to declare a variable to be mutable.

let x = 66;
let mut y = 7400;
y = x + y;  // y now contains 7466
x = x + y;  // error[E0384]: cannot assign twice to immutable variable `x`

Strict Data Types

Compiled Rust code is much more secure than alternative languages, partly due to being strict about data types. How many bytes does a C# long occupy? Can an Int32 be assigned to a long without casting? Ok, maybe these questions are a bit easy, but the point is that Rust is very strict with regard to data types and memory layout.

Each Rust variable is a specific type, and the language does not offer implicit casting.

let x: i16 = 66;
let mut y: i32 = 7400;
let sum: i32 = y + x;

The Rust compiler will not allow this simple cast (16-bit integer into 32-bit), giving

error[E0308]: mismatched types
 --> src/main.rs:5:24
  |
5 |     let sum: i32 = y + x;
  |                        ^ expected `i32`, found `i16`

Fortunately, Rust’s errors are typically specific on the cause of the error. In this case, the error information clearly indicates that x has the wrong type.

Null Not Allowed!

Yep, you read that correctly – Rust does not support null. How in the world can we write code without relying on null? As the Rust book explains, null has historically been used to indicate that a variable held no value, and gives the rationale for Option<T> (which relies on Rust’s rich enum capabilities) to provide an idiomatic replacement for null.

enum Option<T> {
    Some(T),
    None,
}

Which leads to code similar to null checking

let x: Option<i16> = None;

if x != None {
    println!("x has a value");
} else {
    println!("x has NO value");
}

Although this approach leads Rust code to be more verbose, it is hard to overstate the huge stability benefit – no more null related bugs such as accidentally dereferencing null!

Borrowing (temporary Ownership)

Rust’s Ownership model for data is a major departure from previous programming languages and approaches. This model is so fundamental to Rust, that it influences just about everything – syntax, libraries, etc. To some degree, everything about Rust is downstream of Ownership. For a quick primer on Ownership, see this post.

Ownership excludes the traditional shared access model to data, dramatically increasing data protection. Yet, Ownership imposes very significant restrictions. Developers, and the programs they craft, need the ability to pass data to shared code (e.g., functions) for processing. Ownership, by itself, makes this type of programming impossible, and would relegate Rust to the dustbin.

Borrowing is Rust’s critical companion to Ownership which maintains high data security while allowing code to utilize shared code, libraries, etc. For developers accustomed to passing data by reference, Rust’s syntax and behavior will be very familiar.

struct SomeThing {
    tiny: i8,
}

fn main() {
    let x = SomeThing { tiny: 74 };

    println!("\n- Get tiny's value by borrowing x");
    // Rust's Borrow operator, &, _is not_ a reference operator (e.g., C++)
    println!("\tvalue returned is {}", get_tiny_by_borrowing(&x));
}

// This function uses the Borrow operator to require callers to give it temporary ownership
fn get_tiny_by_borrowing(s: &SomeThing) -> i8 {
    return s.tiny;
}

Rust’s Borrow operator ‘&’ looks the same as C++’s Reference operator. What’s the difference? You guessed it – Ownership!

When we say that function get_tiny_by_borrow borrows SomeThing, we mean that Rust ensures that this function owns the SomeThing instance for the scope of the function. After the function complete, ownership reverts back to the prior owner (x in this case).

Using C++ as a comparison, Rust’s Borrowing differs in two major ways:

Shared vs Owned – C++ data may be referenced by multiple code simultaneously, for example by multiple threads. Data safety is in the hands of the programmer who must ensure the data is protected / thread safe. Rust’s compiler won’t allow this behavior, raising the bar for data / memory safety.

Data changes – What happens when a function changes the data passed to it? Rust and C++ provide the same behavior, if the data is mutable. C++ data is implicitly mutable, so any changes inside the function are carried outside the function. Rust data, however, is implicitly immutable for memory safety, and must be explicitly declared as mutable. In the code sample, SomeThing is borrowed and is not declared as mutable, so attempting to change its data will result in a compiler error.

Mutability is a major topic in itself, so we’ll cover it in a separate post.

Rust’s Big Differences

All developers will find substantial differences with Rust. In one aspect, Ownership, Rust is different that just about every language before it. The following outline gives a brief overview of Rust’s most significant (most difficult?) differences.

  • Variables – immutable by default, structs + methods (no classes), None > null, no implicit casting
  • Ownership – only one pointer at a time
  • Borrowing – sole pointer can be loaned / borrowed
  • Traits – not quite .NET’s interface

Ownership

Rust’s concept of memory ownership is its most substantial difference from other languages. Almost all languages employ an accessor model for memory. Accessors (aka, pointers, references, etc.) can be copied, passed to other code, etc., which results in multiple accessors to the same bit of memory.

Ownership explicitly replaces an accessor model with the ownership model. Ownership, and its companion Borrowing, underlie Rust’s focus on memory safe code.

For developers who have worked with pointers in languages like C, C++, etc., the easiest way to think about ownership is that only one pointer may point to (own) a memory location. Copying a pointer transfers ownership to the new pointer, and the previous pointer becomes invalid.

More recent OOP languages like Java and C# hide much of the complexity of dealing with pointers directly, but developers still deal with concepts value types (stack) vs object types (heap), passing by value (a copy) vs by reference (a pointer).

Regardless, the ownership model really twists your noodle!

See Ownership for more explanation and examples of ownership.

Ownership

Ownership is probably the highest hurdle for most developers investigating Rust. Rust is the first language to gain mainstream acceptance that fundamentally changes how memory is used relative to the last five or six decades. Ownership is tectonic shift in how developers think as they write code.

To understand why ownership is so different, let’s consider the historic approach to programs using memory. Since the early days of programming, memory has been a bit like cooks in a kitchen. One cook in the kitchen can do whatever they like and doesn’t have to worry about another cook “corrupting” things. Two cooks in the kitchen can become surprisingly complicated, but they can make it work if they are each very careful – much more careful than when they have the kitchen to themselves. A little carelessness can be immediately disastrous (i.e., grease fire), or can take a bit of time (i.e., water dripping into a plugged sink).

Multiple cooks’ shared access to the kitchen is analogous to multiple accessors to the same bit of memory in a program. Consider this C# code:

   string s1 = "Hello!";
   string s2 = s1;

   Console.WriteLine($"\tString value on heap, s1 = {s1}");
   Console.WriteLine($"\tString value on heap, s2 = {s2}");
   Console.WriteLine($"\ts1 & s2 reference the same object? {object.ReferenceEquals(s1, s2)}");

Output

    String value on heap, s1 = Hello!
    String value on heap, s2 = Hello!
    s1 & s2 reference the same object? True

The two variables s1 & s2 both reference the same memory location which contains “Hello!” Printing the value of either variable produces the same “Hello!”

The problem is that either variable can change the data at the memory location, and the other only cannot do anything to protect the value or itself. (We’re ignoring copy on write behavior in this case)

Continuing the analogy, Rust only allows a single cook in the kitchen. The developer can instruct one cook to take ownership of the kitchen from another. The previous cook no longer has any access to the kitchen at all. Directly translating the C# code to Rust yields:

    let s1 = String::from("Hello!");
    let s2 = s1;

    println!("\tString value (heap) s1 = {}", s1);
    println!("\tString value (heap) s2 = {}", s2);

Attempting to compile this code will fail with

error[E0382]: borrow of moved value: `s1`

This code looks very similar to the C# example. What’s different? Ownership!

On line 1, s1 is set to “Hello!” After line 1, s1 owns the memory location holding “Hello!” But then on line 2, s2 takes ownership from s1. That’s the situation the Rust compiler has detected, and gives an error telling us that s1 no longer has access to the data. Admittedly, the message is a little cryptic, but it will make more sense after we cover Borrowing.

By commenting out line 4, compilation completes successfully because s1 is no longer attempting to access memory owned by s2. Running the code results in:

String value (heap) s2 = Hello!

Why Rust for .NET?

Rust is a new open-source systems programming language created by Mozilla and a community of volunteers, designed to help developers create fast, secure applications which take full advantage of the powerful features of modern multi-core processors.

https://developer.mozilla.org/en-US/docs/Mozilla/Rust

Developers interested in fast, secure, parallel execution must take a serious look into Rust. In terms of developer control, Rust provides low-level primitives (like C), and strong security and resiliency (unlike C). Rust is dramatically different in very fundamental ways – even contrary to decades of prior art. Some may perceive the learning curve too high, but grasping several key concepts reduce the curve substantially.

While many resources exist for learning Rust, this blog aims to help C# / .NET developers understand Rust’s differences in order that they will:

  • Become proficient with Rust more quickly and reliably
  • Understand why Rust’s features are attracting strong interest
  • Raise their polyglot-ness!

Begin the Rust4.NET journey at Rust’s Big Differences.