Classes
The class
type is a typical concept in object-oriented programming. Cangjie supports object-oriented programming using the class
type. The differences between a class
and a struct
are as follows: a class
is a reference type, and a struct
is a value type. They behave differently when assigning values or transferring parameters. A class
can inherit from another class
, but a struct
cannot inherit from another struct
.
This section describes how to define the class
type, create an object, and inherit a class
.
Defining Classes
The definition of the class
type starts with the keyword class
, followed by the class
name, and then a class
body defined in a pair of braces. The class
body can define a series of member variables, member properties, static initializers, constructors, member functions, and operator functions. For details about member properties, see Properties. For details about operator functions, see Operator Overloading.
class Rectangle {
let width: Int64
let height: Int64
public init(width: Int64, height: Int64) {
this.width = width
this.height = height
}
public func area() {
width * height
}
}
In the preceding example, the class
type named Rectangle
is defined. The class type has two Int64
member variables width
and height
, a constructor with two Int64
parameters, and a member function area
(returning the product of width
and height
).
Note:
A
class
can be defined only at the top layer of the source file.
Class Member Variables
Member variables of a class
are classified into instance member variables and static member variables. Static member variables are modified by static
and must have initial values. Static member variables can be accessed only by type names. The following is an example:
class Rectangle {
let width = 10
static let height = 20
}
let l = Rectangle.height // l = 20
When defining an instance member variable, you do not need to set the initial value (but you need to mark the type). You can also set the initial value, but it can be accessed only through an object (an instance of a class). The following is an example:
class Rectangle {
let width = 10
let height: Int64
init(h: Int64){
height = h
}
}
let rec = Rectangle(20)
let l = rec.height // l = 20
Class Static Initializers
A class
supports the definition of static initializers in which static member variables are initialized through assignment expressions.
A static initializer starts with the keyword combination static init
, followed by an empty parameter list and a function body, and cannot be modified by an access modifier. All uninitialized static member variables must be initialized in the function body. Otherwise, a compilation error is reported.
class Rectangle {
static let degree: Int64
static init() {
degree = 180
}
}
A class
can define only one static initializer. Otherwise, a redefinition error is reported.
class Rectangle {
static let degree: Int64
static init() {
degree = 180
}
static init() { // Error, redefinition with the previous static init function
degree = 180
}
}
Class Constructors
Like a struct
, a class
supports the definition of normal constructors and main constructors.
A common constructor starts with the keyword init
, followed by a parameter list and a function body. In the function body, all uninitialized instance member variables must be initialized. Otherwise, a compilation error is reported.
class Rectangle {
let width: Int64
let height: Int64
public init(width: Int64, height: Int64) { // Error, 'height' is not initialized in the constructor
this.width = width
}
}
Multiple common constructors can be defined in a class, but they must be overloaded. Otherwise, a redefinition error is reported. For details about function overloading, see Function Overloading.
class Rectangle {
let width: Int64
let height: Int64
public init(width: Int64) {
this.width = width
this.height = width
}
public init(width: Int64, height: Int64) { // Ok: overloading with the first init function
this.width = width
this.height = height
}
public init(height: Int64) { // Error, redefinition with the first init function
this.width = height
this.height = height
}
}
In addition to defining several common constructors named init
, you can define (at most) one main constructor within a class
. The name of the main constructor is the same as that of the class
type. The parameter list of the main constructor can contain two types of formal parameters: common formal parameters and formal parameters of member variables (let
or var
must be added before the parameter name). Formal parameters of member variables can define member variables and be used as constructor parameters.
The definition of a class
can be simplified by using the main constructor. For example, the preceding Rectangle
containing one init
constructor can be simplified as follows:
class Rectangle {
public Rectangle(let width: Int64, let height: Int64) {}
}
Common formal parameters can also be defined in the parameter list of the main constructor. For example:
class Rectangle {
public Rectangle(name: String, let width: Int64, let height: Int64) {}
}
The constructor called when an instance of a class is created executes the expressions in the class in the following sequence:
- Initialize the variables that are defined outside the primary constructor and have default values.
- If the constructor does not explicitly call the superclass constructor or other constructors of the class, the parameterless constructor
super()
of the superclass is called. If the superclass does not have a parameterless constructor, an error is reported. - Execute the code in the constructor body.
func foo(x: Int64): Int64 {
println("I'm foo, got ${x}")
x
}
open class A {
init() {
println("I'm A")
}
}
class B <: A {
var x = foo(0)
init() {
x = foo(1)
println("init B finished")
}
}
main() {
B()
0
}
In the preceding example, when the constructor of B
is called, the variable x
with a default value is initialized, and foo(0)
is called. Then, the parameterless constructor of the superclass is called, triggering the call to the constructor of A
. Next, the code inside the constructor body is executed, and foo(1)
is called, printing the string. Therefore, the output of the preceding example is as follows:
I'm foo, got 0
I'm A
I'm foo, got 1
init B finished
If no custom constructor (including the main constructor) exists in the class
definition and all instance member variables have initial values, parameterless constructor is automatically generated. (If this parameterless constructor is called, an object is created in which the values of all instance member variables are equal to the initial values.) Otherwise, the parameterless constructor is not automatically generated. For example, the compiler automatically generates a parameterless constructor for the following class
definition:
class Rectangle {
let width = 10
let height = 20
/* Auto-generated parameterless constructor:
public init() {
}
*/
}
// Invoke the auto-generated parameterless constructor
let r = Rectangle() // r.width = 10, r.height = 20
Class Finalizers
A class
can define a finalizer which is called when an instance of a class is garbage-collected. The function name of finalizers is fixed to ~init
. Finalizers are used to release system resources.
class C {
var p: CString
init(s: String) {
p = unsafe { LibC.mallocCString(s) }
println(s)
}
~init() {
unsafe { LibC.free(p) }
}
}
Developers need to pay attention to the following restrictions when using finalizers:
- Finalizers do not have parameters, return types, generic type parameters, or modifiers, and cannot be explicitly called.
- Classes with finalizers cannot be modified by
open
. Only classes that are notopen
can have finalizers. - A class can define at most one finalizer.
- A finalizer cannot be defined in extensions.
- The timing when a finalizer is triggered is uncertain.
- A finalizer may be executed on any thread.
- Multiple finalizers are executed in a random sequence.
- It is an undefined behavior that a finalizer throws an uncaptured exception.
- It is an undefined behavior to create a thread in a finalizer or use the thread synchronization function.
- It is an undefined behavior if the object can still be accessed after the finalizer is executed.
- If an object throws an exception during initialization, the finalizer of the object, which has not been fully initialized, will not be executed.
Class Member Functions
Member functions of a class
are classified into instance member functions and static member functions (modified by static
). Instance member functions can be accessed only through objects, and static member functions can be accessed only through class
type names. Static member functions cannot access instance member variables or call instance member functions. However, static member variables and static member functions can be accessed in instance member functions.
In the following example, area
is an instance member function, and typeName
is a static member function.
class Rectangle {
let width: Int64 = 10
let height: Int64 = 20
public func area() {
this.width * this.height
}
public static func typeName(): String {
"Rectangle"
}
}
Instance member functions can be classified into abstract and non-abstract member functions based on whether there is a function body. Abstract member functions have no function bodies and can be defined only in abstract classes or interfaces. For details, see Interfaces. In the following example, the abstract function foo
is defined in the abstract class AbRectangle
(modified by the keyword abstract
).
abstract class AbRectangle {
public func foo(): Unit
}
Note that abstract instance member functions have the semantics of open
by default. The open
modifier is optional and the public
or protected
modifier is mandatory.
A non-abstract function must have a function body. In the function body, instance member variables can be accessed through this
. For example:
class Rectangle {
let width: Int64 = 10
let height: Int64 = 20
public func area() {
this.width * this.height
}
}
Access Modifiers of Class Members
For class
members (including member variables, member properties, constructors, and member functions), there are four types of access modifiers: private
, internal
, protected
, and public
. Default indicates internal
.
- The
private
modifier indicates that members are visible within theclass
definition. - The
internal
modifier indicates that only the current package and subpackages (including the subpackages of the subpackages) are visible. For details, see Package. - The
protected
modifier indicates that the current module and the subclasses of the current class are visible. For details, see Package. - The
public
modifier indicates that members are visible both inside and outside the module.
package a
public open class Rectangle {
public var width: Int64
protected var height: Int64
private var area: Int64
public init(width: Int64, height: Int64) {
this.width = width
this.height = height
this.area = this.width * this.height
}
init(width: Int64, height: Int64, multiple: Int64) {
this.width = width
this.height = height
this.area = width * height * multiple
}
}
func samePkgFunc() {
var r = Rectangle(10, 20) // Ok: constructor 'Rectangle' can be accessed here
r.width = 8 // Ok: public 'width' can be accessed here
r.height = 24 // Ok: protected 'height' can be accessed here
r.area = 30 // Error, private 'area' cannot be accessed here
}
package b
import a.*
public class Cuboid <: Rectangle {
private var length: Int64
public init(width: Int64, height: Int64, length: Int64) {
super(width, height)
this.length = length
}
public func volume() {
this.width * this.height * this.length // Ok: protected 'height' can be accessed here
}
}
main() {
var r = Rectangle(10, 20, 2) // Error, Rectangle has no `public` constructor with three parameters
var c = Cuboid(20, 20, 20)
c.width = 8 // Ok: public 'width' can be accessed here
c.height = 24 // Error, protected 'height' cannot be accessed here
c.area = 30 // Error, private 'area' cannot be accessed here
}
This Type
Inside a class, the This
type placeholder is supported and refers to the type of the current class. The This
type placeholder can only be used as the return type of an instance member function. When a subclass object is used to call a function whose return type is This
type defined in the superclass, the calling type of the function is identified as a subclass instead of the superclass where the function is defined.
If an instance member function does not declare a return type and only an expression of This
type exists, the return type of the current function is inferred as This
. The following is an example:
open class C1 {
func f(): This { // its type is `() -> C1`
return this
}
func f2() { // its type is `() -> C1`
return this
}
public open func f3(): C1 {
return this
}
}
class C2 <: C1 {
// member function f is inherited from C1, and its type is `() -> C2` now
public override func f3(): This { // Ok
return this
}
}
var obj1: C2 = C2()
var obj2: C1 = C2()
var x = obj1.f() // During compilation, the type of x is C2
var y = obj2.f() // During compilation, the type of y is C1
Creating Objects
After the class
type is defined, you can call its constructor to create an object (by calling the constructor through the class
type name). In the following example, Rectangle(10, 20)
is used to create an object of the Rectangle
type and assign a value to the variable r
.
let r = Rectangle(10, 20)
After an object is created, instance member variables and instance member functions (modified by public
) can be accessed through the object. In the following example, r.width
and r.height
can be used to access the values of width
and height
in r
, and r.area()
can be used to call the member function area
.
let r = Rectangle(10, 20) // r.width = 10, r.height = 20
let width = r.width // width = 10
let height = r.height // height = 20
let a = r.area() // a = 200
If you want to change the value of a member variable through an object (this method is not recommended, and you are advised to change the value through a member function), you need to define the member variable of the class
type as a mutable member variable (by using var
). The following is an example:
class Rectangle {
public var width: Int64
public var height: Int64
...
}
main() {
let r = Rectangle(10, 20) // r.width = 10, r.height = 20
r.width = 8 // r.width = 8
r.height = 24 // r.height = 24
let a = r.area() // a = 192
}
Different from the object of a struct
, the object of a class
is not copied when its values are assigned or its parameters are transferred. Multiple variables point to the same object. If the value of a member in an object is changed through a variable, the values of the corresponding member variables in other variables are also changed. The following uses value assignment as an example. After r1
is assigned to r2
, if the values of width
and height
of r1
are changed, the values of width
and height
of r2
are also changed.
main() {
var r1 = Rectangle(10, 20) // r1.width = 10, r1.height = 20
var r2 = r1 // r2.width = 10, r2.height = 20
r1.width = 8 // r1.width = 8
r1.height = 24 // r1.height = 24
let a1 = r1.area() // a1 = 192
let a2 = r2.area() // a2 = 192
}
Inheriting Classes
Like most programming languages that support the class
type, Cangjie supports class
inheritance. If class B inherits class A, class A is the superclass and class B is the subclass. The subclass inherits all members of the superclass except private
members and constructors.
Abstract classes can always be inherited. Therefore, the open
modifier is optional when an abstract class is defined. You can also use sealed
to modify an abstract class, indicating that the abstract class can be inherited only in the current package. A non-abstract class can be inherited only if it is modified by open
during definition. When an instance member with the open
modifier is inherited by a class, the open
modifier is also inherited. When a class without the open
modifier contains a member with the open
modifier, the compiler generates an alarm.
You can use <:
to specify the superclass that a subclass inherits, but the superclass must be inheritable. In the following example, class
A is modified by open
and can be inherited by class B. However, class B cannot be inherited. Therefore, an error is reported when class C inherits class B.
open class A {
let a: Int64 = 10
}
class B <: A { // Ok: 'B' Inheritance 'A'
let b: Int64 = 20
}
class C <: B { // Error, 'B' is not inheritable
let c: Int64 = 30
}
A class
supports only single inheritance. Therefore, it is invalid for a class to inherit the code of two classes. (&
is the syntax for a class to implement multiple interfaces. For details, see Interfaces.
open class A {
let a: Int64 = 10
}
open class B {
let b: Int64 = 20
}
class C <: A & B { // Error, 'C' can only inherit one class
let c: Int64 = 30
}
Because classes support single inheritance, each class can have at most one direct superclass. For a class
whose superclass is specified during definition, its direct superclass is the specified class during definition. For a class
whose superclass is not specified during definition, its direct superclass is of the Object
type. Object
is the superclass of all classes (note that Object
has no direct superclass and contains no members).
Because a subclass is inherited from its superclass, the object of the subclass can be used as the object of its superclass. However, the object of the superclass cannot be used as the object of its subclass. In the following example, B is a subclass of A. Therefore, an object of type B can be assigned to a variable of type A, but an object of type A cannot be assigned to a variable of type B.
open class A {
let a: Int64 = 10
}
class B <: A {
let b: Int64 = 20
}
let a: A = B() // Ok: subclass objects can be assigned to superclass variables
open class A {
let a: Int64 = 10
}
class B <: A {
let b: Int64 = 20
}
let b: B = A() // Error, superclass objects cannot be assigned to subclass variables
The type defined by a class
cannot inherit the type itself.
class A <: A {} // Error, 'A' inherits itself.
The sealed
modifier can modify only abstract classes, indicating that the modified class definition can be inherited by other classes only in the package where the definition is located. The sealed
modifier contains the semantics of public
or open
. Therefore, if the public
or open
modifier is provided when a sealed abstract class is defined, the compiler generates an alarm. A sealed
subclass may be a non-sealed
class. It can still be modified by open
or sealed
, or has no inheritance modifier. If a subclass of the sealed
class is modified by open
, the subclass can be inherited outside the package. A sealed
subclass does not need to be modified by public
.
package A
public sealed abstract class C1 {} // Warning, redundant modifier, 'sealed' implies 'public'
sealed open abstract class C2 {} // Warning, redundant modifier, 'sealed' implies 'open'
sealed abstract class C3 {} // OK, 'public' is optional when 'sealed' is used
class S1 <: C1 {} // OK
public open class S2 <: C1 {} // OK
public sealed abstract class S3 <: C1 {} // OK
open class S4 <: C1 {} // OK
package B
import A.*
class SS1 <: S2 {} // OK
class SS2 <: S3 {} // Error, S3 is sealed class, cannot be inherited here.
sealed class SS3 {} // Error, 'sealed' cannot be used on non-abstract class.
Calling Superclass Constructors
The init
constructor of a subclass can call constructors of the superclass using super(args)
or call other constructors of the class using this(args)
. However, only one of them can be called. If called, the constructor must be at the first expression in the constructor body with no preceding expressions or declarations.
open class A {
A(let a: Int64) {}
}
class B <: A {
let b: Int64
init(b: Int64) {
super(30)
this.b = b
}
init() {
this(20)
}
}
The main constructor of a subclass can use super(args)
to call the constructor of the superclass, but cannot use this(args)
to call other constructors of the class.
If the constructor of a subclass does not explicitly call the constructor of the superclass or other constructors, the compiler inserts the called parameterless constructor of the direct superclass at the beginning of the constructor body. If the superclass has no parameterless constructor, a compilation error is reported.
open class A {
let a: Int64
init() {
a = 100
}
}
open class B <: A {
let b: Int64
init(b: Int64) {
// OK, `super()` added by compiler
this.b = b
}
}
open class C <: B {
let c: Int64
init(c: Int64) { // Error, there is no non-parameter constructor in super class
this.c = c
}
}
Overriding and Redefining
A subclass can override a non-abstract instance member function with the same name in the superclass, that is, a subclass can define a new implementation for an instance member function in the superclass. During overriding, member functions in the superclass must be modified by open
, and functions with the same name in the subclass must be modified by override
. The override
modifier is optional. In the following example, the function f
in subclass B overrides the function f
in superclass A.
open class A {
public open func f(): Unit {
println("I am superclass")
}
}
class B <: A {
public override func f(): Unit {
println("I am subclass")
}
}
main() {
let a: A = A()
let b: A = B()
a.f()
b.f()
}
For an overridden function, the version to be called is determined by the runtime type of the variable (determined by the object that is actually assigned to the variable). This is known as dynamic dispatch. In the preceding example, the runtime type of a
is A. Therefore, a.f()
calls the function f
in superclass A. The runtime type of b
is B (the compile-time type is A). Therefore, b.f()
calls the function f
in subclass B. Therefore, the program displays the following information:
I am superclass
I am subclass
For a static function, a subclass can redefine a non-abstract static function with the same name in the superclass, that is, a subclass can define a new implementation for a static function in the superclass. During redefinition, static functions with the same name in a subclass must be modified by redef
. The redef
modifier is optional. In the following example, the function foo
in subclass D redefines the function foo
in superclass C.
open class C {
public static func foo(): Unit {
println("I am class C")
}
}
class D <: C {
public redef static func foo(): Unit {
println("I am class D")
}
}
main() {
C.foo()
D.foo()
}
For a redefined function, the version to be called is determined by the type of class
. For example, in the preceding example, C.foo()
calls the foo
function in superclass C, and D.foo()
calls the foo
function in subclass D.
I am class C
I am class D
If an abstract function or a function modified by open
has named formal parameters, the implemented function or the function modified by override
must have the same named formal parameters.
open class A {
public open func f(a!: Int32): Int32 {
a + 1
}
}
class B <: A {
public override func f(a!: Int32): Int32 { // Ok
a + 2
}
}
class C <: A {
public override func f(b!: Int32): Int32 { // Error
b + 3
}
}
main() {
B().f(a: 0)
C().f(b: 0)
}
It should be noted that when the implemented or redefined function is a generic function, the type variant constraint of the function in the subtype needs to be looser or the same as that of the corresponding function in the supertype.
open class A {}
open class B <: A {}
open class C <: B {}
open class Base {
static func f<T>(a: T): Unit where T <: B {}
static func g<T>(): Unit where T <: B {}
}
class D <: Base {
redef static func f<T>(a: T): Unit where T <: C {} // Error, stricter constraint
redef static func g<T>(): Unit where T <: C {} // Error, stricter constraint
}
class E <: Base {
redef static func f<T>(a: T): Unit where T <: A {} // OK: looser constraint
redef static func g<T>(): Unit where T <: A {} // OK: looser constraint
}
class F <: Base {
redef static func f<T>(a: T): Unit where T <: B {} // OK: same constraint
redef static func g<T>(): Unit where T <: B {} // OK: same constraint
}