1. 33
  1.  

  2. 18

    One I’ll add, for very early on in the learning process — use owned in structs and return values, use references for function parameters.

    That should guide you into using more advanced types like Rc or Mutex instead of tearing your hair out on the borrow checker. There are times to break that rule, but treat it like optimization (don’t start there).

    1. 11

      Very much this. If you have a lifetime in a struct, unless the usage of the struct is very restricted it will come back to bite you later.

      Afaict one problem that causes this is that the standard lib is full of these kinds of structs, especially in iterators and data structures, which are some of the more commonly used things, so new Rust programmers see lots and lots of API’s that do the things they should usually avoid. And for the stdlib it’s the right decision! But most programs don’t need to be designed that way. No idea how to solve that conundrum.

      1. 1

        I think actually the design that the standard library uses here aligns with what I have found to be ergonomic in library design in practice: cases where you need complex and/or stateful borrows (such as iterators) are usually deserving of their own type.

      2. 1

        I have thought about this a lot: why not just use Rc and Mutex instead of fighting the compiler. Everyone jumps and says it’s not the most optimal thing to do in Rust but coming from Node or Java or Python Rc in Rust would beat the heck out of every of those PLs. Do you have any info on how to approach with this mental model?

        1. 1

          Look at numbers every programmer should know. If “optimizing your code” is going to involve reducing database calls, then the mutex is about 1,000x to 10,000x from the window of operations you care about.

          1. 1

            coming from Node or Java or Python Rc in Rust would beat the heck out of every of those PLs

            Even if Rust overall is faster, pervasive use of reference counting could be less efficient than pervasive use of tracing GC.

        2. 16

          Embrace unsafe but sound code

          Eh. Certainly don’t be afraid of unsafe code. But also accept that most all the time, you ain’t gonna need it. The example shown of an unchecked array access after an explicit bounds check. As recently demonstrated, the compiler is smart enough to do this for you. This code is literally a waste of time.

          Honestly though, the rest of it is pretty fantastic advice.

          1. 4

            Rust’s reference model is fairly simple. Borrowers can have as many shared reference to something as they need, but there can only be a single exclusive reference at a time. Otherwise, you could have many callers trying to modify a value at the same time. If many borrowers could also hold exclusive references, you risk undefined behavior, which safe Rust makes impossible.

            Calling &mut “exclusive” references would have saved me some time while learning Rust.

            This seems like such a helpful point for new Rust developers. Borrowing and ownership is always the thing that trips people up first. I’m curious what folks who are less familiar with Rust make of this way of thinking about it.

            1. 2

              it has also recently been released as part of Linux kernel - a feat no language other than C has been able to accomplish.

              There are thousands of lines of assembly in Linux, too :)

              1. 1

                Passing impl Foo as an argument is smell in my opinion, and actually performs worse (due to dynamic dispatch at runtime). Passing a T: Foo instead allows the compiler to monomorphise the function. It’s also cleaner to do this when you have multiple constraints on the T and not just a single Foo.

                1. 13

                  You are thinking about dyn Foo, not impl Foo

                2. 1

                  we can satisfy the borrow checker by wrapping the parent reference in something called a Weak pointer. This type tells Rust that a node going away, or its children going away, shouldn’t mean that its parent should also be dropped.

                  I read this a few times and am feeling nitpicky, but I feel Weak is more the opposite. It says that a node or its children sticking around isn’t sufficient reason to not drop the parent. Or, more to the point, the T in an Rc<T> can be dropped if only Weak<T> pointers remain.

                  From the docs

                  Since a Weak reference does not count towards ownership, it will not prevent the value stored in the allocation from being dropped, and Weak itself makes no guarantees about the value still being present.

                  Interestingly, Weak<T> pointers will allow the T to drop, cleaning up cycles if T contains Rcs, but will keep ahold of the allocation itself. This surprised me at first, but makes sense once you realize that that counters are themselves stored in the allocation. If dropping all the Rcs cleared the allocation too then the Weaks would deference dangling pointers when trying to access those counters.

                  1. 2

                    The Weak keeping allocations alive might be more obvious to people coming from a c++ background - there is the same issue with shared_ptr and weak_ptr.