Item 44: Avoid member extensions

When we define an extension function to some class, it is not added to this class as a member. An extension function is just a different kind of function that we call on the first argument that is there, called a receiver. Under the hood, extension functions are compiled to normal functions, and the receiver is placed as the first parameter. For instance, the following function:

  1. fun String.isPhoneNumber(): Boolean =
  2. length == 7 && all { it.isDigit() }

Under the hood is compiled to a function similar to this one:

  1. fun isPhoneNumber(`$this`: String): Boolean =
  2. `$this`.length == 7 && `$this`.all { it.isDigit() }

One of the consequences of how they are implemented is that we can have member extensions or even define extensions in interfaces:

  1. interface PhoneBook {
  2. fun String.isPhoneNumber(): Boolean
  3. }
  4. class Fizz: PhoneBook {
  5. override fun String.isPhoneNumber(): Boolean =
  6. length == 7 && all { it.isDigit() }
  7. }

Even though it is possible, there are good reasons to avoid defining member extensions (except for DSLs). Especially, do not define extension as members just to restrict visibility.

  1. // Bad practice, do not do this
  2. class PhoneBookIncorrect {
  3. // ...
  4. fun String.isPhoneNumber() =
  5. length == 7 && all { it.isDigit() }
  6. }

One big reason is that it does not really restrict visibility. It only makes it more complicated to use the extension function since the user would need to provide both the extension and dispatch receivers:

  1. PhoneBookIncorrect().apply { "1234567890".test() }

You should restrict the extension visibility using a visibility modifier and not by making it a member.

  1. // This is how we limit extension functions visibility
  2. class PhoneBookCorrect {
  3. // ...
  4. }
  5. private fun String.isPhoneNumber() =
  6. length == 7 && all { it.isDigit() }

There are a few good reasons why we prefer to avoid member extensions:

  • Reference is not supported:
  1. val ref = String::isPhoneNumber
  2. val str = "1234567890"
  3. val boundedRef = str::isPhoneNumber
  4. val refX = PhoneBookIncorrect::isPhoneNumber // ERROR
  5. val book = PhoneBookIncorrect()
  6. val boundedRefX = book::isPhoneNumber // ERROR
  • Implicit access to both receivers might be confusing:
  1. class A {
  2. val a = 10
  3. }
  4. class B {
  5. val a = 20
  6. val b = 30
  7. fun A.test() = a + b // Is it 40 or 50?
  8. }
  • When we expect an extension to modify or reference a receiver, it is not clear if we modify the extension or dispatch receiver (the class in which the extension is defined):
  1. class A {
  2. //...
  3. }
  4. class B {
  5. //...
  6. fun A.update() = ... // Does it update A or B?
  7. }
  • For less experienced developers it might be counterintuitive or scary to see member extensions.

To summarize, if there is a good reason to use a member extension, it is fine. Just be aware of the downsides and generally try to avoid it. To restrict visibility, use visibility modifiers. Just placing an extension in a class does not limit its use from outside.