The semantics of a slice are similar to a structure like this:
fn Slice(comptime T:type) type {
return struct {
ptr: [*]T,
len: usize,
};
}
It’s essentially a pointer carrying length information. First, it possesses all the inherent complexities of a pointer. A pointer may or may not own a block of heap memory. The same applies to slices; they may correspond to a block of heap memory or simply be a reference to other heap or stack memory. Therefore, you must understand where a slice comes from before making a judgment.
Second, there’s the complexity introduced by the length information it carries. If your modifications to the slice don’t involve len, then the slice is stateless; you can freely modify its contents without changing len.
However, once you attempt to modify len, it means you’re using a stateful slice. Your modifications only affect the current slice, and other copies of the slice are unaware of your length modification.
If your slice is allocated on the heap, attempting to modify len means your slice itself cannot fully express heap memory ownership information. You must first store the maximum capacity of the slice in an additional form; in this case, ptr[0..capacity] is the true owner of this slice, while slices whose len can be modified only exist as references.
Therefore, we discuss stateful slices used as references: regardless of whether their ownership is on the stack or the heap, you must be mindful of their capacity. If the slice comes from a stack buffer with a certain capacity, you cannot modify its contents beyond the buffer’s size. If the slice comes from heap memory, and you want to modify its contents beyond its capacity, you must reallocate a larger block of heap memory.
In practice, I recommend the convention of treating slices as stateless, immutable, fat pointers. If you want to modify them dynamically(especially the need to modify len), always use std.ArrayList, and never use a naked slice.