Extending a type takes its entire public surface, including members its author adds in a later release, whereas containing it means you list what you re-expose. What are the mechanical consequences of taking the whole surface, and how do different languages guard that seam when the parent grows a new member?
answer
- extending adopts the parent's future releases too
- Kotlin: mandatory `override` turns capture into a build error
- C#: `new` required, otherwise warning CS0108
- Go: promotion by depth, ambiguity errors at the call site
- containment surface = whitelist you maintain
basics
~20 sSubtyping copies every current and future parent member into your surface, so a later addition can collide with a member you already had. Kotlin requires the override keyword and reports an accidental override; C# demands new and warns otherwise; Go turns depth-equal promotions into a call-site error; Rust has no promotion at all.
solid answer
~50 sTaking the surface means someone else's release notes edit your API. - **Java**: if a parent adds a method matching a signature you already declared, yours silently becomes its implementation and starts answering parent-typed calls it was never written for. - **Kotlin** closes exactly that hole: overriding requires the `override` keyword, so the new parent member produces an `accidental override` compile error instead of a silent capture; the price is `open` and `override` ceremony everywhere. - **C#**: members are non-virtual by default and shadowing wants `new`; without it you get warning CS0108 — the collision is reported, not resolved. - **Go**: promotion is by name and depth, so a new method on an embedded type can shadow or, at equal depth with another embedded type, make a call site ambiguous — reported only where the name is selected. - **Rust**: no surface inheritance; colliding trait methods are disambiguated explicitly. Containment's surface is a whitelist you maintain by hand — that maintenance is the price of owning your API.
code
kotlin · 9 lines// library v2 adds: open fun flush() {}
open class Buffer {
open fun flush() { /* new in v2 */ }
}
class MyBuffer : Buffer() {
fun flush() { } // error: accidental override -
// 'flush' hides a member of supertype and needs 'override'
}go deeper
Know that extending a type gives you all of its public members, while containing it means you expose only what you write out.
Explain that the parent's future additions land on your type too, and name at least one language that turns the resulting collision into a compile error.
Compare the guards concretely — Kotlin's accidental-override error, C# CS0108, Go's call-site ambiguity, Rust's explicit qualification — and predict which upgrades break loudly versus silently.
Treat it as API ownership: extending a type you do not control adopts its release cadence, so require a build-time signal for surface drift or refuse the inheritance and pay the whitelist cost deliberately.
## What 'taking the whole surface' means mechanically Declaring a subtype relationship publishes, on your type, every accessible member the parent has — and every member it will have. You do not choose the list, you inherit it, including members added years later by someone who has never seen your code. Containment inverts this: your surface is whatever you declare, and re-exposing a parent capability is an explicit act. Three mechanical consequences follow, independent of language: 1. **Your API grows without a commit of yours.** Consumers can call new parent members through your type immediately, so behaviour you never designed becomes part of what you support. 2. **Names can collide.** A new parent member may match one of yours. What happens then is where languages diverge sharply. 3. **Invariants can be bypassed.** A new parent mutator reaching your state directly is a hole in whatever the rest of your surface enforced. ## Java: the silent capture Java requires no keyword to override. If a parent gains `void flush()` and your subclass already declared `void flush()` for its own purposes, yours becomes the parent's implementation without any diagnostic, and every parent-typed call now routes into it. `@Override` is optional and only *asks* the compiler to verify the direction you expect — it does not stop the reverse case where a new parent member captures an existing subclass method. ## Kotlin: the accidental-override error Kotlin makes `override` mandatory and classes closed by default. When a parent adds a member matching yours, the compiler reports an accidental override rather than silently binding: the collision becomes a build failure at exactly the moment the upstream change arrives. The cost is ceremony — `open` on every class and member intended for extension, `override` on every implementation — which is the same design bet stated positively: extension must be declared, on both sides. ## C#: report, do not resolve C# methods are non-virtual unless marked, and deliberately shadowing a parent member requires `new`. Omit it and the compiler emits warning CS0108 telling you the member hides an inherited one. The language's answer is therefore diagnostic: you are told a collision exists and must state your intent, rather than being given a resolution you did not choose. ## Go: promotion by name and depth Go has no inheritance, but embedding promotes members, and the selection rules are depth-based: the shallowest name wins, and two names at the same depth are *ambiguous*. Crucially the ambiguity is not an error at the declaration; it is an error at the call site that selects the name. So adding a method to a widely embedded type can shadow an existing promoted member, or break call sites in code that embeds two types now sharing a name — and the failure appears in the consumer's file, not in the type's. ## Rust: no promotion at all Rust has no surface inheritance. A type's inherent methods are its own, and trait methods are only in scope where the trait is imported. When two traits provide the same method name, the call is disambiguated explicitly with fully-qualified syntax. An upstream trait gaining a new default method therefore cannot capture your call; it can still cause ambiguity you must qualify, and it can affect inference, but the resolution is always something you write down. ## Python: neither guard nor error Python resolves by MRO with no keyword and no diagnostic, so a new base attribute is simply shadowed by a subclass attribute of the same name or vice versa depending on the linearization. The compensations are conventional: name-mangled private attributes for state you must not have captured, and tests. ## The design conclusion this supports Rank the languages by how much they make you say out loud: Rust and Kotlin demand a declaration and fail the build when reality diverges; C# warns; Go defers the failure to the call site; Java and Python resolve silently. If your type extends a type you do not own, you have adopted its release cadence, and only the first group gives you a build-time signal when the adoption goes wrong. Containment removes the question entirely at the price of a whitelist you maintain by hand — the pass-through members that generators like Kotlin's `by` or Scala 3's `export` exist to write for you. That maintenance cost is the fee for owning your own API surface, and it is usually cheaper than the alternative of discovering the surface changed when a consumer calls something you never designed.
- Kotlin's mandatory `override` costs ceremony on every extensible member. What exactly does the ceremony buy that an optional annotation does not?It makes the check bidirectional. An optional annotation only verifies the case you anticipated — that the member you meant to override really overrides something. Mandatory `override` also catches the unanticipated direction: a parent gaining a member that matches one of yours becomes a compile error rather than a silent capture, because your member did not declare an intent to override anything. That converts an upstream release into a build failure you see immediately.
- Go reports embedding ambiguity at the call site rather than at the type declaration. What are the practical consequences of that choice?The type keeps compiling after an upstream method is added, and only code that selects the ambiguous name breaks — which may be in another module entirely. That is friendly for large dependency graphs, because unused overlaps cost nothing, but it moves the failure away from the change that caused it. The mitigation is to give embedded fields explicit names and call through them when the surfaces are likely to overlap.
- If your surface is a whitelist you must maintain, why not use a generator such as Kotlin's `by` or Scala 3's `export` and get the whole thing for free?You can, but note what each generator commits you to. Kotlin's `by` regenerates the whole interface, so a new interface member appears on you automatically and you are back to inheriting a surface you did not choose — the only difference is that it is an interface's surface. Scala 3's `export` lets you name or exclude members, so it keeps the whitelist property while removing the typing. Pick the generator whose default matches the API guarantee you intend to make.
saying these in an interview costs you the question
- Believing `@Override`-style annotations protect against a parent later adding a member that matches yours.
- Assuming embedding in Go is checked for name conflicts at the point of declaration.
- Treating the inherited surface as fixed at the moment you wrote the subclass.
- Claiming containment has no maintenance cost, when its cost is exactly the whitelist and its upkeep.
- Arguing this is a style preference rather than a question of who owns your published API.