Question about nested classes syntax

I was wondering about a decision made in the language (and I am certainly not advocating a change - just curious)

class Outside {
    class InsideClass {
    }
    companion object {
        val insideVariable = 0
    }
}

To create a new instance we call

 val inside = Outside.InsideClass()

which seems to have the same scope inside Outside as

val i = Outside.insideVariable.

It seems to me that it might have been more consistent to have the nested class as

class Outside {
    companion object {
        class InsideClass {
        }
        val insideVariable = 0
    }
}

Of course the current form could be used for an inner class and the second for a nested class… but that may be a little obscure.

I only ask because I twice lately I needed to declare a nested class and did so in the companion. Not complaining - just asking why that decision was made. (Oh, and everyone here loves Kotlin - keep up the good work).

Phill

I wasn’t at all involved in the process of designing Kotlin, so I can only guess.

For me there are two entirely separate cases here: referencing compile-time symbols/declarations and referencing the runtime data. Nobody says Outside. shouldn’t mean accessing items inside the Outside class. If Outside has a function foo, we can reference it with Outside::foo. Similar with nested classes/objects.

But because storing any data requires an instance, we can’t store it directly in Outside. We need some instance and this instance is a companion object.

So this is the reason why we need to put “static” members in a companion. It doesn’t mean Outside. is somehow more related to the companion than to the class itself.

I’m not sure if I can explain my point clearly :wink:

I am perfectly willing to believe that I have my levels of abstraction muddled up - but I can’t quite see it at the moment.

In my mind Outside.xxx is referencing something declared at ‘static’ or ‘companion’ scope. Outside().yyy is referencing something at the instance scope.

And in my mind Outside.Inside is a class defined at ‘static’ scope (just using ‘Outside’ as a ‘namespace’) and therefore belongs in the companion.

But I see what you mean by companions being a ‘mechanism’ to allow data storage, the Inner class definition does not need storage so does not need to live in the companion.

1 Like

I think I can explain the difference, so let me give it a shot.

In the first case, your inner class is a namespacing difference. Let’s demonstrate in Java, cause I think Java makes this more obvious.

public class Outside {
    private final int insideVariable = 0;

    private Outside() {
    }

    public static final Outside companion = Outside();

    public static class InsideClass {
    }

    public int getInsideVariable() {
        return insideVariable;
    }
}

We have a singleton instance of Outside that we can access via the companion variable. However, we don’t need to instantiate Outside in order to use InsideClass; InsideClass being an inner class is effectively just a namespacing difference.

Now, if we do the second example in Java, it’s like this:

public class Outside {
    private final int insideVariable = 0;

    private Outside() {
    }

    public static final Outside companion = Outside() {
        public static class InsideClass {
        }
    };

    public int getInsideVariable() {
        return insideVariable;
    }
}

I think that’s the equivalent anyway… it’s hard to wrap my head around exactly what’s happening in Kotlin, and how that would translate to Java.

Hopefully though, the second example shows that in this case, the InnerClass exists inside an instance, and we can only access it because we instantiated an anonymous class that extends Outside.

Functionally speaking, it’s pretty much the same thing, and I don’t think either way really affects how you use the InnerClass, but hopefully this at least explains the difference “under-the-hood” between the two ways of doing it.

Another reason is the rule of simplicity: Kotlin tries very hard to have the simpler code be the most likely to be correct.

Nested classes are “safer” than inner classes: they behave just like a normal class, the only difference is namespacing. It is also much more common to use nested classes than inner classes—and personally, I have no yet seen a good example of an inner class with a public constructor.

Therefore, the default should be a nested class. The additional behavior of inner classes must be behind a new modifier (inner) such that a reader knows immediately that something additional is happening, and knows immediately where to find it in the documentation.

Here’s an example of a nested hierarchy, try to compare how much verbose it would be if everything needed to be inside a companion object:

sealed interface Progress {
    data object Done : Progress
    sealed interface Loading : Progress {
        data object Unquantified : Loading 
        interface Quantified : Loading {
            val percent: Int
        }
    }
}

Also, your last example is not as consistent as you think it is: if “something being inside another” means ‘inner’ and not ‘nested’, what does an object in a class mean? By definition, an inner object is not possible (it can’t have a single instance if each outer class has its own instance). So you’ve added one more difference between class and object: that class is inner by default, and object isn’t.

1 Like