CHAPTER 11
Extension class provides a less intrusive way of sharing the code. Instead of inheriting from a class or implementing an interface, one may as well declare that the class/interface is being extended by one or more extension classes. The extension class doesn't provide storage, and only has access to the externally visible members of the extendee. Using extension methods has a more strict syntax requirement, so you will sometimes be surprised by it not working as you would expect. This chapter will dive into these details.
The word extend is used in Julian's documentation to describe the relationship between an interface subclassing another. Extension class is of a different concept. Therefore when we apply an extension to another type we refer to it as installing an extension to the target, which is also called an extendee.
An extension class must be declared static and, to be practical in the sense of being an extension, have at least one method that meets the following criteria:
interface ICalculator : CalculatorExtension {
int add(int a, int b);
}
static class CalculatorExtension {
static int multiply(ICalculator this, int a, int b) {
int times = Math.abs(b);
int sign = Math.sign(b);
int total = 0;
while (times > 0) {
total = this.add(a, total);
times--;
}
if (sign < 0) {
total = -total;
}
return total;
}
}
In the example above, we installed an extension to the interface ICalculator
, which only supports the adding functionality. The extension class builds a multiplication on top of that, leveraging only what is visible to an external consumer of ICalculator.
As you have learned, this
is not an assignable value in instance-scoped methods. In extension methods, however, they are mutable.
static class CalculatorExtension2 {
static Calculator multiply(Calculator this, int a, int b) {
this = new Calculator(a * b); // Allowed
return this;
}
}
As of 0.1.34, Julian only provides one way of adding an extension, which somewhat beats the purpose of the extension for unobtrusiveness - you must install an extension using the standard inheritance syntax, namely the colon(:)-led, comma(,)-separated id list trailing the type name in the definition.
One can install multiple extensions to the same type, and it's not really required that the extension class have an extension method that targets the extendee. Suppose HairExtension
is a valid extension class (static), it can be added to ICalculator
's list just fine. However, no methods will be actually added to the type as the result of installation. Still, via the Reflection API, HairExtension will be listed as an installed extension class.
interface ICalculator : CalculatorExtension, HairExtension { ... }
It's about the time to recap the inheritance declaration. For static class, it can have up to one static type as the parent. For non-static class, it can have up to one non-static type as the parent, zero or more interfaces, and zero or more static classes for the extension. For interface, it can have zero or more interfaces, and zero or more static classes for the extension.
static class MyStaticClass : ParentClass;
interface MyInterface : Interface1, Interface2, ..., Extension1, Extension2, ...;
class MyClass : ParentClass, Interface1, Interface2, ..., Extension1, Extension2, ...;
There are two ways of calling an extension method. First and foremost, bear in mind that an extension method is a static method, so you can always call it using the standard invocation syntax:
ICalculator calc = new Calculator();
int result = CalculatorExtension.multiply(calc, 5, 10);
Absolutely legitimate and equally unimpressive. The true power of extension method, however, lies in the syntax support for calling it as if it were instance-scoped:
ICalculator calc = new Calculator();
int result = calc.multiply(5, 10);
Note that this time, do not pass along the instance as the first argument. It will be automatically filled in by the interpreter. If you insist passing it yourself, you will get an exception complaining about the unmatched argument types.
Nonetheless, the ability to be called by dot (.) syntax is merely a layer of syntax sugar. In particular, you cannot call an extension method as an instance member from another instance method without using this
reference. Assume we have the following definitions:
class Person : PersonExtension {
void talk(string msg) {
...
repeat(msg, 3); // Fail!
...
}
void utter(string msg) {
...
}
}
static class PersonExtension {
static string repeat(Person this, string msg, int times) {
...
}
}
From Person.talk()
the attempt to call repeat()
would fail because there isn't a real instance member called repeat()
defined in Person. To make it work, one must explicitly invoke through dot-addressing syntax.
class Person : PersonExtension {
void talk(string msg) {
...
this.repeat(msg, 3); // Success!
...
}
...
}
Resolution for an extension method happens independently from that for the inherent members. In fact, resorting to extension method will only occur after the regular resolution bears no result. If any member of the same name is inherently defined in the extendee, the interpreter will not try to resolve the member name as extension at all.
interface ICalculator : CalculatorExtension {
int add(int a, int b);
}
class MyCalculator : ICalculator {
int multiply(int a, int b);
}
static class CalculatorExtension {
static int multiply(ICalculator this, int a, int b) {
... ...
}
}
In this example, MyCalculator implements a member of the exact same name as CalculatorExtension offers, so this member will completely hide the extension method. This behavior doesn't change even if the member is invisible, has different parameters, or not invocable.
class Calculator2 : ICalculator {
protected int multiply(int a, int b);
}
class Calculator3 : ICalculator {
int multiply(int a, int b, int c);
}
class Calculator4 : ICalculator {
const int multiply = 10;
}
In each of these classes, they all somehow added a definition for a member named 'multiply'. Therefore calling multiple(5, 10)
against any of these instances will not resolve to the extension method. Instead, they will likely end up with different kinds of exceptions. In some sense, extension method approximates the mechanism of Java's default methods for interface. Naturally, this also applies if the member is inherited from a parent class/interface.
Despite of this restriction, extension methods can be overloaded among themselves. For example:
interface ICalculator : CalculatorExtension {
int add(int a, int b);
}
static class CalculatorExtension {
static int multiply(ICalculator this, int a, int b) { ... } // version 1
static int multiply(ICalculator this, int a, int b, int c) { ... } // version 2
}
ICalculator calc = new Calculator();
calc.multiple(1, 2); // call version 1
calc.multiple(3, 4, 5); // call version 2
Another noteworthy point regarding the use of extension method relates to first-class function. If one tries to assign the method member to a variable, the variable will become representing the static member of the extension class, not associated in any way with the instance from which the function is obtained.
static class CalculatorExtension {
static int multiply(ICalculator this, int a, int b) { ... }
}
ICalculator calc = new Calculator();
var fun1 = calc.multiply;
var fun2 = CalculatorExtension.multiply;
bool isTrue = fun1 == fun2; // true
fun11(c, 10, 5); // OK
Extension class is an experimental feature and is bound to evolve more in the future. Among the features being considered on is the ability to apply the extension in a dynamic way through Reflection API, or a pre-configured way through Julian engine API (exposed through JSR223 interface as well), and relaxed syntactic requirement, such as referring to the extension by name only. So definitely expect more to come in the future!