Reference to primitive.. Why?

I understand this:

let a = 1;
let b = &a;

Hard to understand:

let a:&u8 ;
let b:&str;
let c:&String;

It sounds for me like this:

let a = & 1 ;

When we need to use such kind of syntax?

References to (primitive and any other) types are called pointer types or reference type.

When you want to explicitly write types (especially in item declaration), you need the syntax:

let a: &'static str = "";
const S: &str = "";
fn f(_: &[u8]) { }
1 Like

This is mostly the same as:

let a: i32 = 1;
let b: &i32 = &a;

(Except that the compiler might also choose a different integer type instead of i32, like i64, usize or others, depending on where you use a and b.)

Here you only declare the variables, but you don't assign a value to them. Using the colon (:) means you tell the type of the variables. Here

  • a is a shared reference to an u8 (which is stored elsewhere),
  • b is a shared reference to a string slice (which is stored elsewhere or a constant in memory),
  • c is a shared reference to a String (which is stored elsewhere).

None of these references will be set to a value (i.e. there will be no actual reference, you just tell the compiler that these variables can be assigned a reference later in your code).

This is yet something different. It's short for something like:

let hidden = 1;
let a = &hidden;

(using temporary lifetime extension edit: constant-promotion, see @quinedot's response below)

2 Likes

You can declare but not initialize non-references too.

let s: String;

It can be useful when you need to declare something at an outer scope, but assign it in an inner scope, or when you want to declare a bunch of variables at once. I couldn't think up a good example offhand, but the nonsense program here doesn't compile due to var231 going out of scope at the end of the block. This change:

+    let mut var231: i32;
     {
-        let mut var231: i32 = 1735471491i32;
+        var231 = 1735471491i32;

Lets the code compile. A better example is probably when you need to assign an outer variable somewhere deep inside some loops or similar.

That particular case is eligible for constant promotion; i.e. you can get a &'static i32.

3 Likes

Thank You.. Excuse me for my non-clear explanation.. It is strange to me types, not values...

So, my Q. is what is the difference between:

let a: 'static str = "";
const S: str = "";
fn f(_: [u8]) { }

Do we try to "own" the type u8 (and the type str) and no one will not use the type u8 (and the type str) ?

You omitted the &'s? Was that an accident or did you deliberately remove them? Without the &'s, the code will not compile.

2 Likes

This is a syntax error. In &'static str the syntax is & followed by an optional lifetime name, followed by a type. You can't use a lifetime name without the &, because the lifetime is saying something about the reference, not the referent — with no &, there's no place for the lifetime to get involved.

const S: str = "";
fn f(_: [u8]) { }

These are both errors because dynamically-sized values cannot be handled directly as values to be passed around — they must be “behind a pointer”, whether a reference or Box or something else. (Also, the const line won't work anyway because the type of "" is &str, not str.)

It'd be possible in principle for the language to support both of these in certain cases, but it currently doesn't.

3 Likes

You don't "assign a type" to a variable. The variable has a type (i.e. it doesn't "contain" the type like it contains the value), and you can assign values of the same type to the variable. That's one of the primary purposes of having static types.

It is not clear what you mean by this, please elaborate.

1 Like

It's not possible to directly "own" an [u8]. But you can own an u8:

fn takes_u8(x: u8) {
    println!("I got: {x}");
}

fn takes_ref_to_u8(x: &u8) {
    println!("I got: {x}");
}

fn takes_ref_to_u8_slice(x: &[u8]) {
    println!("I got: {x:?}"); // we use `:?` for debug output
}

fn main() {
    let n: u8 = 12;
    let v: Vec<u8> = vec![1, 2, 3];
    takes_u8(n); // actually a copy of the `u8`!
    takes_ref_to_u8(&n); // a ref to the `u8`
    takes_ref_to_u8_slice(&v); // implicitly converts `&Vec<u8>` to `&[u8]`
}

(Playground)

Output:

I got: 12
I got: 12
I got: [1, 2, 3]

I hope this didn't make things more complex.

P.S.: Note that println! is a macro and can cope with x being either an u8 or an &u8, it doesn't matter for println!.

1 Like

As others have mentioned, none of these lines will successfully compile. That’s because str and [u8] are dynamically-sized¹ types: The number of bytes required to store them (usually) cannot be known at compile time, which restricts how they can be used. In particular, they must always be hidden behind some kind of reference type, like &, &mut, Box, or Arc.

¹ Sometimes called unsized types.

1 Like

I thought those are the same and I was asking why we use &:

let a:&u8 =  2 ;
let b: u8 =  2 ;

No, not the same, I have to use

let a:&u8 = &2 ;

And when I was talking:

let a = & 1 ;

..thinking it is an extra stupid syntax.. No! Not stupid! It works! I have shocked !!!

Thank You All for patiently explanations. I myself would not have had the such patience.

1 Like

If you take a step back, a "type" like Vec<T> is really a type constructor: it's not a concrete type until you tell it what the generic T is. We sometimes say things like "the Vec type is generic over [some type parameter] T", which is a bit of a sloppy shortcut as Vec is a type constructor, not a type.

There are other type constructors that take lifetimes in addition to (or instead of) type parameters. [1] For example, slice::Iter<'a, T> takes both a lifetime and type parameter in order to construct a concrete type.

& is a languge built-in type constructor that also takes a lifetime and a type as parameters. Therefore, just like Iter<'_, i32> is a very distinct type from an i32, a &i32 is also a very distinct type.

& has a lot of other special language behavior too, besides just the syntax, for sure. And one of them is to take a reference to a value; if you apply the (non-override-able) & operator to a value with type T, the result has type &T. You still may be able to fit this in your mental model by again comparing to the construction of other types with public fields:

let i = &7;
let i: &i32 = i;

// https://doc.rust-lang.org/std/cmp/struct.Reverse.html
let r = std::cmp::Reverse(0);
let r: std::cmp::Reverse<i32> = r;

// https://doc.rust-lang.org/std/ops/struct.Range.html
// (Though these have a special syntax too)
let r = std::ops::Range { start: -4, end: 2 };
let r: std::ops::Range<_> = r;

// https://doc.rust-lang.org/std/result/enum.Result.html
let r: Result<(), _> = Result::Err("Arrrrrrrr");

// https://doc.rust-lang.org/std/primitive.array.html
let arr = [0, 1, 2];
let arr: [i32; 3] = arr;

Other things of note:

  • str is a concrete type, that is, it is not a lifetime-taking type constructor, so a 'static str doesn't make sense [2]
  • Similarly, the slice type constructor [T] takes a type parameter, but no lifetime parameter
  • Incidentally, a str is a [u8] underneath [3]
  • References and other pointers to unsized types like str and [T] are special in that they also store the length as part of the reference/pointer (they are "wide" or "fat" pointers); on their own, the types have nowhere to store that information

  1. Phrased more casually: types that are also generic over lifetimes. ↩︎

  2. perhaps think of this as being because there's no & or anything else to hang the lifetime off of ↩︎

  3. with the additional invariant that the bytes are UTF8, and thus can't be publically exposed as bytes in safe mutable or construction code ↩︎

4 Likes

No, why would it be? A reference is not the same as the thing it points to. The very point of references (and other pointer-like types) is to provide indirection so you can use a value without actually consuming (moving) or copying/cloning the value.

In the piece of code below:

let a: &u8 = &2;

the &u8 part on the left-hand side is the type: it says that the variable a has type &u8, i.e. it is a reference pointing to a byte. Meanwhile, the &2 part on the right-hand side is the value: a reference to the integer number 2. Since the variable was declared with the type "reference to byte", the value you assign to it must also have the same type, i.e. reference to byte.

I am confused as to why you would explicitly expect this not to work. What makes you think it shouldn't work?

1 Like

In a sense, &u32 is useless, in that there's nothing you can do with it that you can't do with u32.

But sometimes you end up needing one due to generic code, for example HashMap<u32, String> will actually take keys of type &u32 when looking up values, because the code is generic, so it doesn't know that the key type is cheap to copy.

In those cases it's very convenient to simply create a &u32 with &0.

3 Likes

@rustart It can sometimes help to know which background someone has, in order to explain things better. Have you been working in other programming languages before, or is Rust the first programming language you are learning?

1 Like

HashMap keys are a good example.

let mut map = HashMap::new();
map.insert(5,100);
map.get(&5).map(|val| println!("found {}",val); );

Basically your generics says insert-with-ownership, query-with-readonly-ref. So even though 5 could be copied, that would complicate the API of HashMap.

1 Like

I just hit a better example of when you want to declare on one level and define on another level: when you need to sometimes use a provided reference, but sometimes allocate anew and use a reference to that. You don't want to allocate unnecessarily and you can't drop a temporary.

fn foo(path: &Path) -> std::io::Result<()> {
    let full_path;
    let path = match path.is_relative() {
        false => path,
        _ => {
            full_path = path.canonicalize()?;
            &full_path
        }
    };
    // ...
    Ok(())
}
5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.