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).
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 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.
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.