Comparison of Static Members

[Companion objects](https://www.baeldung.com/kotlin/companion-object) is an interesting feature in Kotlin, meant to replace the concept of static members found in languages like Java, C#, and Python. To understand its benefits, let’s examine how static members work in these other languages.

In Java, static members (fields and methods) are accessible from both the class itself and instances of the class (though accessing them through instances isn’t recommended). They are inherited, but static methods are not virtual. Resolution happens at compile-time based on the compile-time type, which is important when calling them through an instance. If a subclass defines a static method with the same signature as a parent class, invoking it through a parent variable referencing a child instance will execute the parent’s method due to compile-time resolution and the lack of polymorphism for static members. More information can be found here.

C# addresses this Java behavior by allowing access to static members only through the class, not instances. Inheritance remains (a child class can access a parent’s static member), and you can redefine a static method in a subclass using the new modifier.

Modern JavaScript, with its addition of static members (though classes are essentially syntactic sugar on top of its prototypical nature), handles static members similarly to C#. Access is restricted to the class, inheritance is preserved, and redefinition in subclasses is allowed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Person {
    static planet = "Earth"
    
    constructor(name) {
        this.name = name;
    }

    static shout() {
        return `${this.planet} inhabitant AAAAAAAAAAAA`;
    }

}

class ExtendedPerson extends Person {

}

console.log(Person.shout())

try {
    console.log(new Person("Francois").shout());
}
catch (ex) {
    console.log(ex);
}

// inheritance of static fields/methods works OK
console.log(ExtendedPerson.shout());

//it works because of this:
console.log(Object.getPrototypeOf(ExtendedPerson) === Person);
//true

This implementation likely involves setting properties directly on the class object (e.g., Person.shout = function(){};). Inheritance works because a child “class” prototype ([[Prototype]]) points to the parent.

Interestingly, static methods can and should access other static methods within the same class using this. This makes sense because this is dynamic, representing the “receiver,” which is the class itself in a static context. Using this instead of the class name enables a form of polymorphism, as demonstrated below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Person {
    static shout() {
        return "I'm shouting";
    }

    static kick() {
        return "I'm kicking";
    }

    static makeTrouble() {
        return `${this.shout()}, ${Person.kick()}`;
    }

}

class StrongPerson extends Person {
    static shout() {
        return "I'm shouting Loud";
    }
    static kick() {
        return "I'm kicking Hard";
    }    
}

console.log(Person.makeTrouble());
console.log("--------------");
console.log(StrongPerson.makeTrouble());

// I'm shouting, I'm kicking
// --------------
// I'm shouting Loud, I'm kicking 

Note how using this calls Child.shout(), while kick() remains tied to the parent’s implementation.

Python handles static/class members uniquely. Attributes declared in a standard class belong to the class by default. Static data attributes require no special keywords, while static/class methods use the @classmethod decorator (if interacting with other class methods) or @staticmethod decorator otherwise. Method invocation in Python uses an attribute lookup algorithm. explained here Functions are data descriptors with a __get__ method. When fetched, this method executes, creating a bound method object (bound to the instance or class) or a staticmethod object (unbound) depending on the decorator. Consequently, class/static methods can be invoked through the class or an instance, inheritance is preserved, and polymorphism, similar to JavaScript, functions correctly.

Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Person:
    planet = "Earth"
    
    def __init__(self, name: str):
        self.name = name

    def say_hi(self):
        return f"Bonjour, je m'appelle {self.name}"
    
    @staticmethod
    def shout():
        return "I'm shouting"

    @staticmethod   
    def kick():
        return "I'm kicking"

    @classmethod
    def makeTrouble(cls):
        return f"{cls.shout()}, {cls.kick()}"


class StrongPerson(Person):
    @staticmethod
    def shout():
        return "I'm shouting Loud"

    @staticmethod   
    def kick():
        return "I'm kicking hard"


print(Person.makeTrouble())
p1 = Person("Iyan")
print(p1.makeTrouble())

print("--------------")

# inheritance works fine, with polymorphism, both invoked through the class or through an instance
print(StrongPerson.makeTrouble())
p2 = StrongPerson("Iyan")
print(p2.makeTrouble())

# I'm shouting, I'm kicking
# I'm shouting, I'm kicking
# --------------
# I'm shouting Loud, I'm kicking hard
# I'm shouting Loud, I'm kicking hard


print(Person.planet) # Earth
print(p1.planet) # Earth

Person.planet = "New Earth"
print(Person.planet) # New Earth
print(p1.planet) # New Earth

# this assignment will set the attibute in the instance, not in the class
p1.planet = "Earth 22"
print(Person.planet) # New Earth
print(p1.planet) # Earth 22

While you can read a static attribute (e.g., planet) via the class or an instance, modifying it through an instance adds it to the instance instead of updating the class.

Lastly, when using dataclasses where instance members are declared at the class level, use the ClassVar type annotation (e.g., cvar: ClassVar[float] = 0.5) to declare static/class attributes.

Licensed under CC BY-NC-SA 4.0
Last updated on Jul 29, 2023 14:59 +0100