How costly is an `is` check on all platforms? Is t...
# getting-started
c
How costly is an
is
check on all platforms? Is there some hidden cost, or is it safe to use in performance-sensitive applications?
e
doing your own function dispatch via
is
checks is typically slower than using the JVM's built-in virtual dispatch
c
I can’t speak to other platforms, but on JVM it’s considered an expensive call because it’s using reflection, and in particular traversing the object’s class hierarchy. A few years back, i contributed to a Java template engine, and using the
is
(
instanceof
in Java) operator was one of the biggest contributors to performance issues. It’s probably worse in old Java apps which had deep class hierarchies, but not quite as terribly expensive in the typical Kotlin app
e
whether that matters (or if you even have an alternative) depends on your use case
instanceof Class
is probably faster than
instanceof Interface
because the former only needs to scan the linear superclass hierarchy whereas superinterfaces form a tree
c
For performance-sensitive situations, you’re probably better using some kind of “registration” mechanism. The necessary types are registered and a check is compared to the types in the registry. This is how it’s done with Kotlinx Serialization, for example, with its contextual serializers Not only will a registry will be faster, but it also future-proofs you against many of the issues that come with reflection (other Kotlin targets, proguard, AOT compilations, etc)
e
that only works if you don't allow subclassing outside of the registry, and is still slower than virtual dispatch
💯 1
c
For a more precise description of the use case: I'm providing a few interfaces and
object
implementations, expecting the user to combine them via interface delegation. However, one of the downside of interface delegation is that an implementation's methods cannot be overridden by the final object (since it's delegation, not overriding):
Copy code
interface A {
    fun a()
]

interface B {
    fun b()
}

object FooDefault : A, B {
    override fun a() = println("FooDefault.a")
    override fun b() = println("FooDefault.b")
}

object Custom : A, B by FooDefault {
    override fun a() = println("Custom.a")
}

Custom.b()
Output:
Copy code
println("FooDefault.b")
println("FooDefault.a")
Whereas I would like to see
Copy code
println("FooDefault.b")
println("Custom.a")
Instead, I thought of letting the caller know which implementation it was called on, and having each implementation call other methods as
(implementation as? A) ?: this
c
I’m not sure what you mean by “cannot be overriden”. This playground seems like it’s working as I would expect https://pl.kotl.in/x-wUTvLIR That said, I think you’d probably have a better time keeping a 1-to-1 relationship between the default objects and their interfaces. Don’t try to combine 2 interfaces into a single “default object”, because you’ll end up with an exponential number of default objects for the various combinations of the interfaces. Much better for you, and for the end-users, to just let them decide which interfaces should use the provided defaults, or omit those and use their own. And with a common naming scheme or pattern for setting these up, it’s easy to discover the defaults as well (see this thread discussing the same kind of idea)
c
Sorry, I meant that
FooDefault.b
calls
a()
.
I'm going to try and come up with a better example
e
then why not
Copy code
interface FooDefault : A, B
class Custom : FooDefault
c
Ah, I see. That is definitely very unintuitive behavior, and for that reason alone I’d recommend not going down that route. Even if you set it up with runtime checks on the final objects class to make it technically work as you’d expect, it’s still going against the normal rules of the language, and could easily be done incorrectly. Here’s a tip I learned the hard way when I started programming: if you think you’re doing something clever, it’s probably a sign that it’s bad code. What you want is simple, elegant code that works, not clever code. Someone else reading that code, who didn’t have the same thought process as you did when you first wrote it, will often be more confused than they are helped by it.
c
@Casey Brooks I completely agree with you, however in this case it's the only credible solution I came up with. Of course, I'm just emulating what regular interface implementation would be like, the issue is that there are actually dozens of sub-interfaces, not just
A
and
B
, so it's not feasible to implement them in a single object. However, if I use delegation to implement them separately, they can't call each other…
c
For this case, since A and B are clearly related in some way, you might just want to move them into the same interface, so it’s clear that the two are able to talk to each other. Alternatively, without coupling those two interfaces together, you could use a Mediator Pattern instead, to allow both interfaces access to a common space, while making it clear and explicit how the relationship between the interfaces works. Give me a min to throw together an example
c
For this case, since A and B are clearly related in some way, you might just want to move them into the same interface, so it’s clear that the two are able to talk to each other.
It's true that this would make everything simpler, however in the real case there are dozens of sub-interfaces which all have 1--20 methods, so moving everything in a super-interface is not great 😕 As you can see, the main issue in this example is that
DefaultB
needs an instance of
A
. The problem is, this dependency is specific to
DefaultB
, not to all implementations of
B
(otherwise, I would have had
B : A
to make it explicit). So
DefaultB
must allow the end-user (who defines the
Custom
object) to control which instance of
A
is used, otherwise the end-user can never override
A.a
.
c
Perhaps the complexity here just isn’t best served with implementation/delegation? Maybe just using normal classes and a DSL would be better than having the end-user implement and override interfaces. The user can supply lambdas to the DSL rather than overriding functions if they need to supply their own logic. Then with normal classes, it becomes easier to use traditional container types (Map, List, Set, etc.) to manage the relationships among the types
c
I'll have to think about it, but you're bringing up a great point
c
Here’s an example using classes instead of interfaces. I think it’s capturing the kind of logic you’re looking for 😅 https://pl.kotl.in/Yd95MSTal