Properties
A property is a special syntax. It does not store values like a field. Instead, it provides a getter and an optional setter to indirectly retrieve and set values.
By using properties, data operations can be encapsulated into access functions. The use of properties is the same as that of common fields, which means that you can perform data operations without being aware of the underlying implementation. In this way, mechanisms such as access control, data monitoring, tracing and debugging, and data binding can be implemented easily.
Properties have the same syntax as fields and can be used as expressions or assigned values.
The following is a simple example where b
is a typical property that encapsulates external access to a
.
class Foo {
private var a = 0
mut prop b: Int64 {
get() {
print("get")
a
}
set(value) {
print("set")
a = value
}
}
}
main() {
var x = Foo()
let y = x.b + 1 // get
x.b = y // set
}
Property Syntax
The syntax rules of properties are as follows:
propertyDefinition
: propertyModifier* 'prop' identifier ':' type propertyBody?
;
propertyBody
: '{' propertyMemberDeclaration+ '}'
;
propertyMemberDeclaration
: 'get' '(' ')' block end*
| 'set' '(' identifier ')' block end*
;
propertyModifier
: 'public'
| 'private'
| 'protected'
| 'internal'
| 'static'
| 'open'
| 'override'
| 'redef'
| 'mut'
;
Properties without the mut
modifier require a getter implementation. Properties declared by the mut
modifier require separate getter and setter implementations.
In particular, you cannot define properties modified by mut
or implement interfaces that have mut
properties within extensions or definition bodies of the numeric, Bool
, Unit
, Nothing
, Rune
, String
, Range
, Function
, Enum
, and Tuple
types.
In the following example, a
is a property not declared by mut
, and b
is a property declared by mut
.
class Foo {
prop a: Int64 {
get() {
0
}
}
mut prop b: Int64 {
get() {
0
}
set(v) {}
}
}
Properties not declared by mut
do not have setters. Like fields declared by let
, they cannot be assigned values.
class A {
prop i: Int64 {
get() {
0
}
}
}
main() {
var x = A()
x.i = 1 // error
}
Specifically, when a struct
instance is declared by let
, you cannot assign values to properties in the struct
, just like fields declared by let
.
struct A {
var i1 = 0
mut prop i2: Int64 {
get() {
i1
}
set(value) {
i1 = value
}
}
}
main() {
let x = A()
x.i1 = 2 // error
x.i2 = 2 // error
}
Properties differ from fields in that they cannot have initial values assigned and must have their types declared.
Property Definition
Properties can be defined in interfaces, classes, structs, enums, and extends.
class A {
prop i: Int64 {
get() {
0
}
}
}
struct B {
prop i: Int64 {
get() {
0
}
}
}
enum C {
prop i: Int64 {
get() {
0
}
}
}
extend A {
prop s: String {
get() {
""
}
}
}
You can declare an abstract property in an interface and abstract class, and its definition body can be omitted. When an abstract property is implemented in an implementation type, its name, type, and mut
modifier must remain unchanged.
interface I {
prop a: Int64
}
class A <: I {
public prop a: Int64 {
get() {
0
}
}
}
Just as abstract functions in an interface can have default implementations, abstract properties in an interface can also have default implementations.
When an abstract property has a default implementation, the implementation type is not required to provide its own implementation (as long as it complies with the usage rules of the default implementation).
interface I {
prop a: Int64 { // ok
get() {
0
}
}
}
class A <: I {} // ok
Properties are classified into instance member properties and static member properties. Instance member properties can be accessed only by instances. You can use this
expression in the getter or setter implementation of the property to access other instance members and static members. You cannot access instance members in the implementation of static member properties.
class A {
var x = 0
mut prop X: Int64 {
get() {
x + y
}
set(v) {
x = v + y
}
}
static var y = 0
static mut prop Y: Int64 {
get() {
y
}
set(v) {
y = v
}
}
}
Properties do not support overloading or overriding, and they cannot have the same name as other members at the same level.
open class A {
var i = 0
prop i: Int64 { // error
get() {
0
}
}
}
class B <: A {
prop i: Int64 { // error
get() {
0
}
}
}
Property Implementation
The getter and setter of a property correspond to two different functions.
- The getter function has the type
()->T
, where T is the type of the property. When the property is used as an expression, the getter function is executed. - The setter function has the type
(T)->Unit
, where T is the type of the property, and the parameter name must be explicitly specified. The setter function is executed when the property is assigned a value.
The implementation rules of properties are the same as those of functions. Properties can contain declarations and expressions, return
can be omitted, and the return value must comply with the return type.
class Foo {
mut prop a: Int64 {
get() { // () -> Int64
"123" // error
}
set(v) { // (Int64) -> Unit
123
}
}
}
The behavior of accessing a property is consistent both inside and outside the property. Therefore, recursive access to a property may cause an infinite loop, which is the same as that of a function.
class Foo {
prop i: Int64 {
get() {
i // dead loop
}
}
}
Note that the setter of struct is a mut function. Therefore, you can modify the values of other fields in the setter, and this
is restricted by the mut function.
Modifiers for Properties
Similar to functions, properties can be modified using modifiers. However, only the entire property can be modified. The getter or setter cannot be modified independently.
class Foo {
public mut prop a: Int64 { // ok
get() {
0
}
set(v) {}
}
mut prop b: Int64 {
public get() { // error
0
}
public set(v) {} // error
}
}
Access control modifiers private
, protected
, and public
can be used for properties.
class Foo {
private prop a: Int64 { // ok
get() { 0 }
}
protected prop b: Int64 { // ok
get() { 0 }
}
public static prop c: Int64 { // ok
get() { 0 }
}
}
Like an instance function, an instance property can be modified using open
and override
.
For properties modified by open
, a subtype can use override
to override the implementation of the supertype (override
is optional).
open class A {
public open mut prop i: Int64 {
get() { 0 }
set(v) {}
}
}
class B <: A {
override mut prop i: Int64 {
get() { 1 }
set(v) {}
}
}
Static properties, like static functions, can be modified using redef
(redef
is optional), allowing a subtype to re-implement the static properties from the supertype.
open class A {
static mut prop i: Int64 {
get() { 0 }
set(v) {}
}
}
class B <: A {
redef static mut prop i: Int64 {
get() { 1 }
set(v) {}
}
}
When a subtype uses override
or redef
on properties that are declared by let
, it must re-implement the getter.
When a subtype uses override
or redef
on properties that are declared by the mut
modifier in the supertype, it is allowed to re-implement only the getter or the setter, or both.
open class A {
public open mut prop i1: Int64 {
get() { 0 }
set(v) {}
}
static mut prop i2: Int64 {
get() { 0 }
set(v) {}
}
}
// case 1
class B <: A {
public override mut prop i1: Int64 {
get() { 1 } // ok
}
redef static mut prop i2: Int64 {
get() { 1 } // ok
}
}
// case 2
class B <: A {
public override mut prop i1: Int64 {
set(v) {} // ok
}
redef static mut prop i2: Int64 {
set(v) {} // ok
}
}
// case 3
class B <: A {
override mut prop i1: Int64 {} // error
redef static mut prop i2: Int64 {} // error
}
When a subtype uses override
or redef
on properties, it must retain the same mut
modifier and the same type as that in the supertype.
class P {}
class S {}
open class A {
open prop i1: P {
get() { P() }
}
static prop i2: P {
get() { P() }
}
}
// case 1
class B <: A {
override mut prop i1: P { // error
set(v) {}
}
redef static mut prop i2: P { // error
set(v) {}
}
}
// case 2
class B <: A {
override prop i1: S { // error
get() { S() }
}
redef static prop i2: S { // error
get() { S() }
}
}
When a subtype uses override
on a supertype's property, it can use super
to call the instance property of the supertype.
open class A {
open prop v: Int64 {
get() { 1 }
}
}
class B <: A {
override prop v: Int64 {
get() { super.v + 1 }
}
}