Transcript from the "Integers" Lesson
>> So one other thing that Rust has is it has multiple sizes of numbers. So we saw f64 before, there's also f32. So f64 is a 64-bit float, f32 is a 32-bit float. So why have two? When would you wanna use one versus the other? Well, basically, there are just trade-offs to when you might wanna use one versus the other.
[00:00:19] So here, I have both of these set to be 10 divided by 3. And in both cases, we're gonna get 3.333 repeating, there's gonna be a lot of 3s in there. Now, the difference here is that f64, because it's bigger, it's 64-bits rather than 32-bits, it can hold more 3s.
[00:00:36] So it has eight bytes of storage, 64-bits being the same thing as eight bytes, whereas f32 only has 32-bits, or four bytes of storage. And because of that, f64 is sort of more precise, it can hold more of those repeating 3s. And so, that makes f64 better for sort of like calculations where precision is really important.
[00:00:55] So why not always use f64? Well, although more memory used allows for more precision, it also means there's more memory used, which can slow down your program. So if you're just doing a local calculation like this, like in the middle of your function you're doing like I wanna do a division operation, do I use f64 or f32?
[00:01:14] Probably not gonna make a difference, just go ahead and use the f64. Treat yourself, it's gonna be fine. But if you can imagine, let's say I'm making a game engine and I have millions and millions of coordinates. The decision between using f64 or f32 for all of those coordinates could add up to megabytes, maybe hundreds of megabytes, maybe even a gigabyte of RAM.
[00:01:34] That's probably extreme, but it can add up to a really big difference in how much memory you're using, which in turn can translate into the program running slower. So basically, that's the trade-off is that f64 gives you more precision, but also takes up more memory, and especially when you have a lot of these things, that can really make a big difference.
[00:01:51] In fact, in most really performance intensive applications that use coordinate systems like this, for example, game engines, it's way more common to find 32-bit floats than 64-bit float floats for exactly this reason. Integers also have different sizes. So let's start by talking about integers in general in Rust.
[00:02:10] So here, I've said let ninety equals, and then the number 90. Notice I didn't have a decimal point here. So the difference between an integer and a float fundamentally is that integers don't have a decimal component to them, they're just a sort of a whole number or a 0.
[00:02:23] They can also be negative. So here's negative 5, that's an example of a negative integer. Can also say 1_000. Note here that I put an underscore here. This is just a little syntactic convenience that Rust has. You're allowed to put underscores wherever you want in number literals. They're usually used as sort of a placeholder for digits, like a comma.
[00:02:40] Or if you're in the US, we use commas, I would put a comma there. But I know, depending on where else you are, you might put a space there or something like that. In Rust, it's always an underscore, so it's a little bit more international friendly. And basically, those are just treated as whitespace by the compiler, basically they're just ignored so you can put as many of those as you want in whatever places you want.
[00:02:59] They're totally optional. I've named this one exactly_three because if I don't put a decimal point here, this is going to do integer division. So 10 divided by 3 is going to say I'm gonna take the integer 10 and divide it by the integer 3, and I'm gonna get an integer out the other side.
[00:03:15] Which means that whatever the decimal component of that calculation is, is gonna get completely discarded. So that's not rounding up, that's just saying whatever it was, we're discarding it and we're gonna end up with exactly 3. But if we ended up with 3.9, that would still get rounded down to 3.
[00:03:30] So definitely be careful with that. You don't wanna accidentally do integer division when you think you're doing floating point division. Also, with integer division, if you divide by 0, it'll panic. And like we talked about earlier, when a panic happens, that's it, game over. [LAUGH] That's sort of the end of the program.
[00:04:09] But for integer division, it's different. Integer division will panic, whereas those other ones will just give you some special value that indicates a problem happened. Okay, there are different sizes of integers just like there are with floats. But actually, there's a lot more integer sizes than there are float sizes.
[00:04:26] So floats it's just f64 and f32, so 64-bit and 32-bit. Whereas integers, you can get as small as i8, which is 8-bits, so that's just one byte, or i16, i32, i64, or even i128, that's as high as they go. So i8 is 8-bits, aka 1 byte. 16 is 2 bytes, 4 bytes, 8 bytes and all the way up to 16 bytes.
[00:04:50] So these have different ranges of integers that they support. So an i8 can represent numbers as low as negative 127, all the way up to positive 128. i6 goes from negative 32,000 to positive 32,000, and so on and so forth. You can find on the Rust website documentation for how big all these different sizes are.
[00:05:10] There's also unsigned integers. So the difference between those integers that we just saw, which are known as signed integers, and unsigned integers is whether or not they have a potential minus sign. So unsigned means that you don't have the minus sign, meaning the lowest value you can have in these is 0.
[00:05:25] So these basically just have a different range. So u8 goes from 0 to 255. You may recall that i8 only went up to 127. Well, u8, even though it's the same size, it's the same eight bits, because it doesn't store the information about whether or not it's negative, it uses that one extra bit as a way to allow it to store more positive numbers.
[00:05:46] So it goes all the way up to 255 for the same storage space. u16 goes up to about 65,000. u32 goes up to about 4 billion. u64 it goes up to 18 quintillion. And if that's not enough for you, u128 goes all the way up to 170 undecillion.
[00:05:59] I had to learn the word undecillion just to be able to express how big 128 is. Sometimes people ask me does Rust have arbitrary ints, like arbitrary size ints? The answer is yes. Not built into the language, but if you want to you can get a third -party package that gets an arbitrarily large int.
[00:06:17] But if you need that, I'm really, really curious to know what wouldn't fit in 171 undecillion, because I personally have never done any math that that would not be able to accommodate. Okay, cool, so these are the unsigned integers. There's two reasons that you might choose an unsigned integer.
[00:06:35] One is maybe you're sort of size constrained on how many bytes you wanna use for your storage, but you really wanna be able to store some extra data. Or more commonly, I just find them to be pretty nice because a lot of cases I have values where I don't want them to be negative.
[00:06:50] I want to make sure that they're never negative, and it's just kind of nice to be able to have a number where it's like, yeah, I know this is never gonna be negative because it just doesn't even store that possibility. There's one other unsigned integer in here known as char, and char is basically just the same thing as u32 with the additional constraint that it's been validated to be valid Unicode.
[00:07:12] So it's otherwise exactly the same thing. So it's a u32, it's some number between 0 and about 4 billion. But it's also been validated to make sure that that number is a valid Unicode scalar value. And so, char is used basically to make up strings, like each element of a string is a char, and that's just something to be aware of.
[00:07:32] Char will come up a little bit in the course of the workshop, but it's basically u32. Yeah, a question
>> If we use i8, is the compiler smart enough to pack the memory to use a 64-bit space with many i8s?
>> Is the compiler smart enough to use, so if you have four i8s, will the compiler just use one i64 to represent that?
[00:07:55] So actually, the short answer is that I don't think it would do that because it might actually be less efficient, believe it or not. This gets into registers on your CPU, and I don't wanna go down a rabbit hole on that. But basically, I would say the Rust compiler is very smart in a lot of cases, and also builds on LLVM, which is a technology that a lot of other compilers use, like C++ and Swift, which does a lot of that really low level optimization stuff.
[00:08:25] And in general, I wouldn't assume that that's actually gonna be more efficient. It might be surprisingly less efficient, even though it sounds like it might be more efficient. So there's trade-offs to all that stuff. But short answer is I don't think that particular thing is something that I would expect the Rust compiler to do, and I think that's probably the right choice.
[00:08:43] Okay, so another thing you can do is you can convert between different numeric types using the as keyword. So here's an example of this. I've got this function called multiply, it takes x, which is an i64 and y which is u8, and by default those are incompatible. I can't just do multiplication between an i64 and u8.
[00:09:00] First, I need to convert to one or the other. Now, whenever I'm in a situation like this, I'm gonna use y as i64, I would have a choice here. I could have also said x as u8, so that they would both be u8s. But in this case, I wanna change y to be an i64 for two reasons.
[00:09:16] One is cuz the function as a whole is returning an i64. So I might as well just get it in an i64 format cuz otherwise I need to use a second as to convert them from u8 to i64. But the second reason is just that, like we've been talking about, there's more precision here.
[00:09:29] An i64 can fit all of the possible values that a u8 can have, but the reverse is not true. So if I were to use as to convert this x down to a u8, well, what happens if x is like more than 255? Then, we're just gonna lose that information, it's just gonna jumble it up.
[00:09:44] So, I don't wanna do that. I would generally speaking prefer always prefer to us as to make something that's strictly bigger, that can hold strictly more data. That may not always be possible, but it's definitely sort of the preference. Worth noting that you can not only do this to convert between integer types, you can also convert between integers and floats.
[00:10:07] So, here we have x as f64, x being an i32, so I'm converting a 32-bit integer to a 64-bit float. And here, we have y which is a 16-bit unsigned integer converting also to an f64. And this is actually a pretty common thing, is I'll have some integers and I wanna do some float division because, as we talked about previously, when you're doing integer division, it truncates the decimal, just throws it away.
[00:10:28] So it's a reasonably common use of as to say okay, I've got some integers, but I actually wanna end up with a float because I care about that decimal portion. And again, typically, the most common way you'll do it is you'll use f64 instead of f32 because if I'm just doing a little local calculation, I kind of wanna get the most performance I can out of it.
>> You mentioned that it's better for the multiply to cast as i64 because you don't lose precision. But so you're saying that you can do x as u8 and the Rust compiler won't be mad at that?
>> Correct, yeah, it won't be mad at it, but you might not be happy with what happens at runtime.
>> I think this is the same question. So the Rust compiler will let you do x as u8 as multiple-
>> Functions, and happily truncate according to some specification.
>> Yeah, it'll actually wrap around. Basically don't use as if you're worried about that happening. You'll have a bad time.