4 ms·
Great post! I appreciate the socratic style. I agree with other posters that stuff like this is important to be comfortable with when writing Rust, and more mat
by burntsushi 9y ago
Great post! I appreciate the socratic style. I agree with other posters that stuff like this is important to be comfortable with when writing Rust, and more material like this blog post is fantastic. I think if I were to write a part 2 of this blog post, it would be about learning how to read Rust code such that you know what is a reference and what isn't, and more pointedly, when something is behind two references. These things are important for effectively using pattern matching among other things.
With that said, I'd like to add some advice by spring-boarding off a part of the post.
> Converting from a Vec<T> to a &[T] is really easy – you just run vec.as_ref(). The reason you can do this conversion is that you’re just “forgetting” that that variable is allocated on the heap and saying “who cares, this is just a reference”. String and Box<T> also has an .as_ref() method that convert to the reference version of those types in the same way.
While on the surface this is absolutely correct, there is a subtle point missing here: as_ref on Vec/String/Box is implemented as part of the AsRef[1] trait, which is _intended_ for use in generic programming. Aside from intent, practically speaking, using as_ref in a non-generic context can often be somewhat unergonomic, since depending on how you use it, it might require a type annotation (because it's generic!).
Where AsRef is useful is in making the types of parameters to functions a bit more liberal. One particularly convenient place where it's used in the standard library is for defining functions that accept file paths. For example, the type signature of the function that opens a file is[2]:
fn open<P: AsRef<Path>>(path: P) -> Result<File>
Basically, this function says that it accepts a parameter `path` with a type `P` that can be infallibly converted into a `Path`. Why is that convenient? Because lots of useful types implement `AsRef<Path>`. They include OsStr, Cow<'a, OsStr>, OsString, str, String, PathBuf, and of course, Path itself. This is what let's you write `File::open("foo/bar")`. Without the generic `AsRef<Path>` constraint, the signature would look like this:
fn open(path: &Path) -> Result<File>
Which would mean that you'd need to write something like `File::open(Path::new("foo/bar"))` instead.
So what's the alternative to using `as_ref` if I'm here poo-pooing it? In my experience, the typical thing to do here is to rely on something called deref. That is, if `s` is a `String` then `{STAR}s` is a `str` and `&{STAR}s` is a `&str`. In many cases, the explicit dereference (so that's `&s` instead of `&{STAR}s`) can be elided and the compiler will "auto-deref" for you. For example, given a function like the following
fn repeat(string: &str, count: u64) -> String
and a string `s` with type `String`, then
repeat(&s, 5)
will "just work." If you prefer the explicit, then I think the recommendation is to use type specific conversion methods. For `Vec<T>`, `as_slice` will give you a `&[T]`. For `String`, `as_str` will give you a `&str`.
OK, that's enough for now! This rabbit hole goes deeper, but I'll stop here. :)
> One question I have (that I think I will just resolve by getting more Rust experience!) is – when I write a Rust struct, how often will I be using lifetimes vs making the struct own all its own data?
If I were forced to give a pithy answer to this question, then I think I would say (predominantly from the perspective of a library writer): "It's a healthy mix, but if I don't care about performance for $reasons, I can usually ignore lifetimes in the types I define."
[1] - https://doc.rust-lang.org/std/convert/trait.AsRef.html https://doc.rust-lang.org/std/convert/trait.AsRef.html
[2] - https://doc.rust-lang.org/std/fs/struct.File.html#method.open https://doc.rust-lang.org/std/fs/struct.File.html#method.ope...
- rathm 9y agoGood stuff. Thank you so much.
- ejanus 9y ago> open<P: AsRef<Path>>(path: P) Can't one achieve same with enum??
- kelnos 9y agoYes, but it's awkward to work with from the caller's side. You'd have to do something like this (hard to pick non-clashy names, too): enum Path { FromString(String), FromStr(&str), FromOsStr(OsStr), // ... } fn open(path: Path) ... { } And then as the caller you'd have to do things like: let file = open(Path::FromStr("/foo/bar")); It's not particularly nice to read, and you also have the overhead of creating and throwing away the enum instance.
- wtetzner 9y agoAn using AsRef means you can define your own types that can be used as paths, whereas an enum would be fixed once it was defined.
- Manishearth 9y agoYou then need to do extra wrapping, and other crates can't add new things that are "as reffable as a path", and there's a runtime penalty