An array of premium seats is passed where an array of seats is expected, then a standard seat is stored - why is that unsafe?
answer
- passing shares the buffer, does not copy
- who else still reads that array
- element type fixed when created
- reads keep the promise, writes break it
- damage at the store, symptom at the read
basics
~20 sThe array's real element type is still premium-only, so the store plants a value the original holder's type promises can never be there, and the next read of that slot returns something the reader believes is impossible.
solid answer
~40 sPassing the array does not copy it: both sides now alias one buffer. Through the wider element type the routine is allowed to store any seat, but the buffer was created to hold premium seats and its original holder still reads it as premium-only. The moment a standard seat lands in a slot, that holder's type is lying about its own data, and the damage shows up later at a read that had nothing to do with the store. Reading through the wider type was always fine - every premium seat really is a seat. It is the write that breaks, which is why a container you can both read and write cannot be widened for free.
code
pseudocode · 8 linesfunction fillEmpty(seats: Array<Seat>)
seats[0] = makeStandardSeat() // fine against this signature
block = newArray of PremiumSeat, size 100
fillEmpty(block) // allowed where arrays widen
p = block[0] // typed as PremiumSeat
p.loungeAccess() // but the slot holds a standard seatgo deeper
Remember that passing a container shares it rather than copying it, and that the receiving code may store into it. Reading through a wider element type is safe; storing through one is not.
Explain the mechanics: the buffer's element type is fixed at creation, the widened reference permits stores the buffer cannot honour, and the symptom appears at a later read by a third party.
Show how you would find this in a running system - the exception or corruption lands far from the store, so you trace who else holds a reference to the buffer and which of them widened it.
Frame it as an API rule for the codebase: mutable containers cross module boundaries at their own element type, and anything handed to readers goes out as a read-only view.
## The setting A stadium keeps its seating chart as an array. The general form is an array of `Seat`; one block of the stadium is held as an array of `PremiumSeat`, where every premium seat is a seat but not every seat is premium. Somewhere else there is a routine that takes an array of `Seat` and writes a standard seat into empty slots. Handing it the premium block feels obviously safe: premium seats are seats, so an array of them looks like an array of seats. It is safe for exactly half of what an array does. ## An array carries two promises, pointing opposite ways - **What may come out.** Every element is at least a `Seat`. Widening preserves this: read any slot of the premium block through the wider type and you get something that genuinely is a seat. - **What may go in.** Through the wider type, the type system says *any* `Seat` may be stored. Through the buffer's real identity, only a `PremiumSeat` belongs there. Widening keeps the first promise and destroys the second. That asymmetry - reads survive, writes do not - is the entire subject. ## The failure, step by step 1. The block is created as an array of `PremiumSeat` with room for a hundred entries. The element type is fixed at that moment and travels with the buffer. 2. The block is passed to a routine whose parameter is an array of `Seat`. No copy is made; the routine holds a second reference to the same buffer. 3. The routine stores a standard seat at index 0. Against its own signature this line is impeccable - it is storing a `Seat` into an array of `Seat`. 4. The original holder later reads index 0 through its own type, which says every slot holds a `PremiumSeat`, and asks the value for something only premium seats offer. 5. Step 4 is where the program misbehaves, and step 3 is where it was ruined. Nothing at step 4 is wrong. ## What each site believes | Site | What the static type says | What is actually true | |---|---|---| | The block's creator | every slot holds a premium seat | true until someone else stores | | The call that passes it | an array of premium seats is an array of seats | only for reading | | The store inside the routine | any seat may be written here | only a premium seat may | | The later read | this slot holds a premium seat | it holds a standard seat | ## Why the compiler is not in a position to catch it - The routine is **correct against its own signature**. Judged alone, it does nothing questionable, and it is typically compiled without ever seeing the call that will hand it the narrower array. - The two references **alias** one buffer. Proving that no writer anywhere can reach an array that some reader elsewhere treats as narrower is a whole-program question, not a local one. - The array can travel arbitrarily far - stored in a field, put inside another structure, handed on again - so the distance between the widening and the damaging store is unbounded. A language that permits this widening has therefore given up compile-time safety for that class of store, and must buy the safety back somewhere else or not at all. ## The general shape of the rule The seating chart is just the vivid instance. The rule is about **mutation**, not about arrays: - A container you may only **read** from can be used where a container of a wider element is expected - every element still satisfies the wider type. - A container you may **write** to cannot, because the write is typed against the wider element while the storage is still narrow. - A container that does **both** therefore gets the strict answer: it is usable only at its own element type. ## What a careful engineer does instead If the routine only reads, hand it something that only reads - a view exposing the reading side of the chart, which may safely widen. If it must write, it has to be typed against the element the buffer actually holds, or be made generic over that element so the caller's type argument travels with it. The one thing that is not an option is hoping nobody stores: the type system, not discipline, is what other people's code is checked against.
- If the routine only read from the array and never stored into it, would the call still be unsound?No. Every element of the narrower array already satisfies the wider element type, so a read through the wider view can only hand back a value that genuinely fits. Unsoundness enters solely through the store. That is why handing out a read-only view of the same buffer is a safe way to grant access.
- Does copying the array before passing it fix the problem?It removes the aliasing, so the original holder is no longer affected by stores into the copy - but the copy itself still has a narrow element type and can still be handed a value that does not fit. A copy fixes who gets hurt, not whether the store is type-correct.
- Why is this widening more dangerous than an ordinary unchecked cast?An unchecked cast is written at one visible site by someone who knows they are overriding the compiler. This widening is implicit and looks like ordinary subtyping, so it happens in code nobody flagged, and the failure surfaces in a different routine at a different time.
Lending someone your box of first-class tickets after relabelling it "tickets": they are entitled to drop an economy ticket in, and you find out when you hand one to a first-class gate.
saying these in an interview costs you the question
- Says it is fine because every premium seat is a seat
- Assumes the array was copied when it was passed
- Claims the compiler rejects the call, so nothing can happen
- Blames the later read rather than the store
- Thinks a cast at the read site would have prevented it