skip to content

A walker lists a loaded type's members two ways and gets two different sets — which two axes separate the lists?

level: middleimportance: must knowfreq 64%

answer

  1. two questions, one call
  2. where declared, and how visible
  3. the declared list stops here
  4. the public view climbs the chain
  5. walk levels for inherited hidden fields

basics

~20 s

Two axes: where a member is declared (on this type or inherited from a supertype) and how visible it is (any visibility or public only). Common lookups bundle the two, so each call answers a fixed combination rather than letting you choose.

solid answer

~40 s

The axes are **declaration site** — declared by this type itself versus inherited from a supertype — and **visibility**, meaning every member regardless of who may see it versus public members only. These are independent questions, but the usual pair of lookups bundles them: one returns everything this type declares at any visibility and stops there, the other returns public members including inherited ones. So the quadrant a data renderer actually wants, every field including non-public ones declared further up, has no single call in the common shape of these APIs. You build it by walking the supertype chain and taking each level's declared members, de-duplicating names that appear at more than one level. A renderer that skips the walk silently loses columns for fields the parent type declares.

code

pseudocode · 9 lines
pseudocode
function allDataFields(type)
    result = []
    level = type
    while level is not none
        for each field in declaredFieldsOf(level)   // this level, any visibility
            if result.hasName(field.name) then continue   // a nearer level already won
            result.append(field)
        level = supertypeOf(level)
    return result

go deeper

for a junior

Remember that a member listing is not simply every member a reader of the source would see, and that there is more than one listing with different contents. Knowing the two exist is enough at this level.

for a middle

Name both axes and place each lookup in the right quadrant, then explain why the quadrant a data walker wants needs a supertype traversal rather than a call.

for a senior

Diagnose the silent version of this: a mapper that has quietly dropped every inherited field for months. Show the chain walk, the de-duplication rule, and how you would test that a parent's field appears exactly once.

for a principal

Decide whether components in your systems walk types themselves at all, since each one re-implements this traversal and each gets the edge cases slightly differently; a single shared walker is usually the call worth making.

## Two independent questions asked by one call When a program asks a loaded type for its members, two separate questions are being answered at once: 1. **Where was the member declared?** On this type itself, or somewhere up the supertype chain? 2. **Who may see it?** Every member the type has, or only the ones published to outside callers? These are orthogonal. All four combinations are meaningful, and a discovery-driven component usually wants a specific one. The lookups a runtime exposes, however, typically come as a fixed pair, each pinning both axes at once. | | declared on this type only | including inherited members | |---|---|---| | **any visibility** | the usual declared-members lookup | no single call in the common shape — walk the chain | | **public only** | filter the declared list by visibility | the usual public-members lookup | The diagonal is what trips people. The two calls sit in opposite corners, so the naive assumption — that one is a superset of the other — is wrong in both directions. The declared lookup sees hidden members the public lookup does not; the public lookup sees inherited members the declared lookup does not. Neither is complete. ## Why the bundling exists The public-members view answers the question a caller asks: *what can I use on a value of this type?* Inherited members are part of that answer, and non-public ones are not. The declared view answers the question a tool asks: *what does this type itself define?* — which is a property of one declaration, so climbing would make it a different question. Each call is coherent on its own; the trouble is only that neither is the tool's actual question when the tool wants data fields. ## Building the quadrant you want To collect every field including non-public ones declared further up: 1. Start at the object's run-time type. 2. Take that level's **declared** members — this level only, every visibility. 3. Move to the supertype and repeat, until the chain ends. 4. De-duplicate as you go, because the same name can be declared at more than one level. Step 4 is not bookkeeping. A subtype declaring a member with the same name as one above it does not remove the supertype's member; the description still carries both, and a chain walk will report both. You must decide which wins — normally the most derived — and you need a rule for what counts as the same member, since two methods can share a name but differ in parameters and both legitimately exist. ## What this costs the renderer that skips it - Columns for fields declared on a shared parent type simply do not appear, and the omission is silent: the report renders, just narrower than expected. - Switching to the public-members lookup to fix that usually makes it worse — data fields are commonly non-public and reached through accessors, so the field list comes back nearly empty while the method list fills with everything inherited from the chain's root. - Walking the chain without de-duplication produces two columns with the same header when a subtype redeclares a parent's field. - Filtering by visibility is not the same as filtering out compiler-generated members; those are marked by a different flag and survive a visibility filter. ## Where runtimes differ They differ in how much choice they give. Some expose a single query where both axes are parameters, so the quadrant is selectable and no walk is needed. Others expose only the fixed pair described above. Some include compiler-generated members in every listing; others hide them by default. Some also expose the type's nested types through the same member listing, which a field walker must exclude. The mechanism is the same everywhere — a type's description records what that type itself declares, and inheritance is a relation you traverse — but the convenience layer over it is not, so a portable walker should assume the fixed pair and do the traversal itself. The interview value of this question is that it separates candidates who have used a member lookup from candidates who know what it answered. The first group is surprised by the missing columns; the second predicts them from the two axes before running anything.

  • Walking the chain, you meet the same member name at two levels — what do you do?
    Decide which one wins and keep only that, normally the most derived, because that is what a value of the run-time type presents. You also need a rule for what counts as the same member: matching by name alone merges two methods that differ only in parameters, while matching by full signature keeps both, which is usually what you want for methods and never what you want for fields.
  • Why is the public-members view usually the wrong basis for a data renderer?
    Because the data it wants is normally not public. Fields are commonly hidden behind accessors, so the public field list comes back nearly empty, while the public method list fills with every inherited method up to the chain's root — including ones that have nothing to do with the record's data. The renderer ends up with no columns and a long list of irrelevant methods.

saying these in an interview costs you the question

  • Thinks the declared-members list already includes inherited members.
  • Thinks the public-members view also exposes non-public members.
  • Assumes one lookup covers every field including inherited hidden ones.
  • Walks the chain and reports a redeclared name twice as two columns.
  • Believes a visibility filter also removes compiler-generated members.