Types
The Cangjie programming language is a statically-typed language. Most type checks that ensure program security occur during compilation. In addition, the Cangjie programming language is a strongly-typed language. Each expression has a type, and the type of an expression determines its value space and supported operations. Statically-typed and strongly-typed mechanisms can help programmers locate a large number of program errors caused by types during compilation.
Types in Cangjie may be classified into the immutable types and mutable types. Immutable types include the numeric (which can be an integer or a floating-point number, see [Numeric]), Rune, Bool, Unit, Nothing, String, Tuple, Range, Function, and enum. Mutable types include Array, VArray, struct, class, and interface.
The difference between the immutable type and the mutable type is that the data value of the immutable type does not change after initialization, and the data value of the mutable type can be changed after initialization.
The following describes each type, values of types, and operations supported by types.
Immutable Types
The following sequentially describes immutable types in the Cangjie programming language.
Numeric
Numeric types include integer and floating-point number types, which are respectively used to represent integers and floating-point numbers. Integer types include signed integer and unsigned integer types. Signed integer types include Int8, Int16, Int32, Int64, and IntNative, indicating signed integers whose encoding length are 8 bits, 16 bits, 32 bits, 64 bits, and platform-dependent sizes, respectively. Unsigned integer types include UInt8, UInt16, UInt32, UInt64, and UIntNative, indicating unsigned integers whose encoding length are 8 bits, 16 bits, 32 bits, 64 bits, and platform-related sizes, respectively. Floating-point number types include Float16, Float32, and Float64, indicating floating-point numbers whose encoding lengths are 16 bits, 32 bits, and 64 bits, respectively.
The lengths of IntNative and UIntNative are the same as the bit width of the current system.
The following table lists all numeric types and their ranges.
| Type | Range |
|---|---|
| Int8 | $-2^7 \sim 2^7-1$ (-128 to 127) |
| Int16 | $-2^{15} \sim 2^{15}-1$ (-32768 to 32767) |
| Int32 | $-2^{31} \sim 2^{31}-1$ (-2147483648 to 2147483647) |
| Int64 | $-2^{63} \sim 2^{63}-1$ (-9223372036854775808 to 9223372036854775807) |
| IntNative | platform dependent |
| UInt8 | $0 \sim 2^8-1$ (0 to 255) |
| UInt16 | $0 \sim 2^{16}-1$ (0 to 65535) |
| UInt32 | $0 \sim 2^{32}-1$ (0 to 4294967295) |
| UInt64 | $0 \sim 2^{64}-1$ (0 to 18446744073709551615) |
| UIntNative | platform dependent |
| Float16 | See IEEE 754 binary16 format |
| Float32 | See IEEE 754 binary32 format |
| Float64 | See IEEE 754 binary64 format |
For ease of use, the Cangjie programming language specifically provides some type aliases (for details about the type aliases, refer to the type alias section below):
Byteis used as the type alias ofUInt8. TheBytetype is equivalent to theUInt8type.IntandUIntare used as the aliases of theInt64andUInt64types, respectively. TheInttype is equivalent to theInt64type, and theUInttype is equivalent to theUInt64type.
let a: UInt8 = 128
let b: Byte = a // ok
let c: Int64 = 9223372036854775807
let d: Int = c // ok
let e: UInt64 = 18446744073709551615
let f: UInt = e // ok
The following describes numeric literals and operations supported by the numeric type.
Numeric Literals
Values such as 5, 24, 2.9, and 3.14 are numeric literals. Literals can only be called by their values, and cannot be changed.
The following describes integer literals and floating-point literals:
Integer literals can be expressed in four forms: binary, octal, decimal, and hexadecimal. The binary value starts with 0b (or 0B), the octal value starts with 0o (or 0O), the hexadecimal value starts with 0x (or 0X), and the decimal value has no prefix. For example, the decimal value 24 may be defined as any one of the following four forms, where the underlined _ is used as a separator to facilitate identification of a quantity of digits of the value.
0b0001_1000 // Binary.
0o30 // Octal.
24 // Decimal.
0x18 // Hexadecimal.
For the syntax definition of integer literals, see [Integer Literals].
Floating-point types comply with the IEEE 754 format. Float16 uses the half-precision format (binary16) of IEEE 754, Float32 uses the single-precision format (binary32) of IEEE 754, and Float64 uses the double-precision format (binary64) of IEEE 754. Generally, a floating-point number contains the integer part, decimal part (including the decimal point), and exponent part. It can be notated in decimal or hexadecimal format. In the decimal format, a floating-point number must contain an integer or a decimal. If there is no decimal, the floating-point literal must contain the exponent part (with e or E as the prefix and 10 as the base number). In the hexadecimal format, a floating-point number must contain an integer or a decimal (with 0x as the prefix) and the exponent part (with p or P as the prefix and 2 as the base number). Pay attention to the following special values in floating-point parameters: positive infinity (POSITIVE_INFINITY), negative infinity (NEGATIVE_INFINITY), Not a Number (NaN), positive 0 (+0.0), and negative 0 (-0.0). Infinity indicates that an operation result exceeds a representation range. For example, when two large floating-point numbers are multiplied, or a non-zero floating-point number is divided by a floating-point zero, the result is infinity, and a sign of infinity is determined by a sign of an operand, complying with the following rule: A positive and a negative make a negative, and two negatives make a positive. NaN indicates that the value is neither a real number nor an infinity. For example, the result of multiplying positive infinity by 0 is NaN. The following is an example of a floating-point literal:
3.14 // decimal Float64 3.14.
2.4e-1 // decimal Float64 0.24.
2e3 // decimal Float64 2000.0.
.8 // decimal Float64 0.8.
.123e2 // decimal Float64 12.3.
0x1.1p0 // hexadecimal Float64 1.0625 (decimal value).
0x1p2 // hexadecimal Float64 4 (decimal value).
0x.2p4 // hexadecimal Float64 2 (decimal value).
For the syntax definition of floating-point literals, see [Floating-Point Literals].
A minimum positive value of a floating-point decimal type in the Cangjie programming language is a subnormal floating-point number.
For the Float32 type, the minimum positive floating-point number is $2^{-149}$, which is about 1.4e-45. The maximum positive floating-point number is $(2 - 2^{-23}) \times 2^{127}$, which is about 3.40282e38.
For the Float64 type, the minimum positive floating-point number is $2^{-1074}$, which is about 4.9e-324. The maximum positive floating-point number is $(2 - 2^{-52}) \times 2^{1023}$, which is about 1.79769e308.
If the face value of a floating-point decimal other than $0$ is rounded to $0$ or infinity because it is too small or too large, the compiler generates an alarm.
Operators Supported by Numeric Types
Numeric types support the following operators: arithmetic operators, bitwise operators, relational operators, increment and decrement operators, unary minus operators, and (compound) assignment operators. The integer type supports all the preceding operators, and the floating-point type supports all operators except bitwise operators. Therefore, the following briefly describes these operators of the integer type. For details, see [Expressions].
Arithmetic operators include addition (+), subtraction (-), multiplication (*), division (/), modulo (%), and power (**). By default, if arithmetic operators are not overloaded (see [Operator Overloading] for details about how to overload operators), the two operands of arithmetic operators must be of the same type. To operate two operands of different types, forcibly convert the type first. For details about the priority and binding of arithmetic operators, see [Arithmetic Expressions]. The following example describes an arithmetic operation on an integer type:
2 + 3 // add result: 5
3 - 1 // sub result: 2
3 * 9 // mul result: 27
24 / 8 // div result: 3
7 % 3 // mod result: 1
2 ** 3 // power result: 8
5 + 15 - 2 * 10 / 4 // result: 15
5 + 10 - 3 * 4 ** 2 / 3 % 5 // result: 14
Bitwise operators include bitwise NOT (!), left shift (<<), right shift (>>), bitwise AND (&), bitwise XOR (^), and bitwise OR (|). For details about bitwise operators, see [Bitwise Operation Expressions]. The following example describes a bit operation on an integer type:
!10 // bitwise logical NOT operator: -11
10 << 1 // left shift operator: 10*2=20
10 >> 1 // right shift operator: 10/2=5
10 & 15 // bitwise logical AND operator: 10
10 ^ 15 // bitwise XOR operator: 5
10 | 15 // bitwise logical OR operator: 15
0b1010 & 0b1110 ^ 0b1111 | 0b0101 // result: 0b0101
Relational operators include less than (<), greater than (>), less than or equal to (<=), greater than or equal to (>=), equal to (==), and not equal to (!=). The result of a relational expression is a value of the Bool type (true or false). For details about relational operators, see [Relational Expressions]. The following example describes a relational operation on an integer type:
2 == 3 // result: false
2 != 3 // result: true
8 < 10 // result: true
9 <= 9 // result: true
24 > 28 // result: false
24 >= 23 // result: true
Increment and decrement operators include increment (++) and decrement (--), which can be considered as special assignment operators (see below) to increase or decrease the value of a variable by 1. Increment and decrement operators can be used only as a suffix operator and can be used only for mutable variables of the integer type, because the values of immutable variables and integer literals cannot be modified. For details about the increment and decrement operators, see [Increment and Decrement Expressions]. The following example describes an increment or decrement operation on an integer type:
main() {
var i: Int32 = 5
var j: Int32 = 6
i++ // i=6
j-- // j=5
return 0
}
A unary minus operator is represented by -, and the result is the negative of its operand. For details about -, see the description of [Arithmetic Expressions]. The following example describes a unary minus operation on an integer type:
var i: Int32 = 5
var j: Int64 = 100
var k: Int32 = -i // k = -5
var l: Int64 = -j // l = -100
(Compound) assignment operators include assignment (=) and compound assignment (op=). op can be any binary operator among arithmetic operators, logical operators, and bitwise operators. The (compound) assignment operation can assign a value of an expression of the numeric type to a variable of the numeric type. For details about (compound) assignment operators, see [Assignment Expressions]. The following example describes a (compound) assignment on an integer type:
main() {
var x: Int64 = 5
var y: Int64 = 10
x = y // assignment: x = 10
x += y // compound assignment: x = 20
x -= y // x = 10
x *= y // x = 100
x /= y // x = 10
x %= y // x = 0
x = 5 // x = 5
x **= 2 // x = 25
x <<= 1 // x = 50
x >>= 2 // x = 12
x &= y // x = 8
x ^= y // x = 2
x |= y // x = 10
return 0
}
Operators Supported by Floating-Point Numbers
Floating-point types support the following operations (except bit operations): arithmetic operations, relational operations, increment and decrement operations, unary positive (negative) operations, conditional operations, and (composite) assignment operations. The calculation of floating-point numbers may use operations with different precisions from their own types. Note the following special cases in floating-point operations: In a relational expression, if the value of an operand is NaN, the result of the expression is false, except that the results of NaN != x and x != NaN are true (x can be any floating-point number including NaN).
let x = NaN
var y = 3.14
var z = x < y // z = false
var v = x != x // v = true
var w = (x < y) == !(x >= y) // w = false
In arithmetic expressions, values containing NaN or infinite expressions comply with the IEEE 754 standard.
Rune
The Cangjie programming language uses the Rune type to represent Unicode code point. These values can be classified into printable characters, non-printable characters (characters that cannot be displayed, such as control characters), and special characters (such as single quotation marks and backslashes).
-
Rune literals
For the syntax definition of
Runeliterals, see [Rune Literals].
A Rune literal consists of a pair of single quotation marks and characters, for example:
'S'
'5'
' ' // blank
Non-printable and special character literals are defined using escape characters (\):
| Escape Character | Character |
|---|---|
\0 | Empty character |
\\ | backslash \ |
\b | Backspace |
\f | Writer |
\n | New line |
\r | Enter |
\t | Horizontal tab |
\v | Vertical tab |
\' | Single quotation mark ' |
\" | Double quotation marks " |
\u{X} | Any Unicode code point, where X is a 1-8 digit hexadecimal number |
-
Operations supported by the
RunetypeThe
Runetype supports only relational operators (compared based on theUnicode code point).main() { 'A'=='A' // result: true 'A'!='A' // result: false 'A'<'a' // result: true 'A'<='A' // result: true 'A'>'a' // result: false 'A'>='A' // result: true return 0 }
Boolean
The Cangjie programming language uses Bool to indicate the Boolean type.
-
BoolliteralThe
Booltype has only two literals:trueandfalse.let bool1: Bool = true let bool2: Bool = false -
Operations supported by the
BooltypeThe Cangjie programming language does not support type conversion between the numeric type and
Bool. Non-Boolvalues cannot be used asBoolvalues. Otherwise, a compilation error is reported. TheBooltype supports the following operations: assignment, partial compound assignment (&&=and||=), partial relation (==and!=), and all logical operations (!,&&, and||). The following is an example:main() { let bool1: Bool = true var bool2: Bool = false bool2 = true // assignment bool2 &&= bool1 // bool2=true bool2 ||= bool1 // bool2=true true == false // return false true != false // return true !false // logical NOT, return true true && false // logical AND, return false false || false // logical OR, return false return 0 }
Unit
The Unit type has only one value: ().
Nothing
Nothing is a special type that does not contain any value, and Nothing is a subtype of all types. Nothing indicates dead code. For example, if a variable is of the Nothing type, it will never be used, and no memory needs to be created for it. If a parameter of a function call is of the Nothing type, the subsequent parameters will not be evaluated, and the function call itself will not be executed. If an expression in a block is of the Nothing type, all expressions and declarations following the expression will not be executed. The Cangjie compiler generates a compilation alarm for the detected dead code.
For details about which expressions have the Nothing type, see [Control Transfer Expressions].
String
The Cangjie programming language uses String to indicate the string type, which is used to express text data and consists of a string of Unicode characters.
String Literals
String literals are classified into three types: single-line string literals, multi-line string literals, and multi-line original string literals.
For details about the syntax definition of string literals, see [String Type Literals].
Single-line string literals are defined using a pair of double quotation marks (for example, "singleLineStringLiteral"). The content in the double quotation marks can be any number of characters (except non-escaped double quotation marks and a separate \). A single-line string literal can be written in only one line.
let s1 = "" // empty string
let s2: String = "Hello Cangjie Lang" // define string s2
var s3 = "\"Hello Cangjie Lang\"" // define string s3 containing character "
var s4: String = "Hello Cangjie Lang\n" // define string s4 containing character \n
// illegal string with character \
let s5 = "hello\"
// illegal string with character "
let s6 = "Hello "Cangjie Lang"
A multi-line string literal starts and ends with three double quotation marks ("""). A new line is required after the double quotation marks at the beginning. Otherwise, an error is reported. The literal starts from the first line after the double quotation marks at the beginning and ends with the first three non-escaped double quotation marks. The content between the literals can be any number of valid characters (except the three non-escaped double quotation marks """ and the separate \). Before the current file ends, if the three non-escaped double quotation marks are not encountered, a compilation error is reported. Unlike single-line string literals, multi-line string literals can span multiple lines.
// empty multi-line string
let s1 = """
"""
// Error: there must be a line terminator after the beginning double quotations.
let errorStr = """abc
"""
/*
The result of s2 is:
This
is a multi-line string
*/
let s2 = """
This
is a multi-line string"""
/*
The result of s3 is:
This
is a multi-line string
*/
let s3 = """
This
is a multi-line string"""
/*
The result of s4 is:
This
is a
multi-line string
*/
let s4 = """
This
is a
multi-line string
"""
/*
The result of s5 is:
This is a
multi-line string
*/
let s5 = """
This is a\n
multi-line string
"""
Multi-line original string literal starts with one or more comment tags (#) and a double quotation mark ("), and ends with a double quotation mark (") and the number of comment tags (#) at the beginning. Note that the matching ends when the string literal value encounters the first matched double quotation mark (") and the number of comment tags (#) at the beginning. The content between the start and end double quotation marks (") can be any number of valid characters (except a double quotation mark and the same number of comment tags (#) at the beginning). Before the current file ends, if no matching quotation mark and the same number of comment tags are encountered, a compilation error is reported. Unlike (common) multi-line string literals, the content in the original multi-line string literal remains unchanged (escape characters are not escaped).
// empty multi-line raw string
let empty1 = #""#
// empty multi-line raw string
let empty2 = ##""##
/*
The result of s2 is:
This
is a multi-line string
*/
let s2 = ##"
This
is a multi-line raw string"##
/*
The result of s3 is:
This is a\n
multi-line string
*/
let s3 = #"This is a\n
multi-line string"#
/*
The result of s4 is:
This is a "#
*/
let s4 = ##" This is a "#
"##
// Error: the beginning and ending numbers of `#` are not matched.
let errorStr1 = ##" error string "#
// Error: the beginning and ending numbers of `#` are not matched.
let errorStr2 = ##" error string "###
// Error: the string literal is terminated when meeting the first matched `"##`.
let errorStr3 = ##" error string "## error "##
The String type can be determined to equality using == (inequality using !=). Two character strings are the same only when their corresponding code point sequences are the same.
Interpolated String
An interpolated string is a string literal that contains one or more interpolation expressions (not applicable to multi-line raw string literals). When an interpolation string is parsed into a result string, the position of each interpolation expression is replaced with the result of the corresponding expression.
Each interpolation expression must be enclosed in {} parentheses and prefixed with $. The expression and declaration sequence in braces can be the same as that in [Block], but cannot be empty. Interpolation expressions in multi-line strings support line breaks.
let obj = "apples"
let count = 10
let interps1 = "There are ${count * count} ${obj}."
// The result of "interps1" is: There are 100 apples.
let error = "Empty sequence is not allowed ${}" // Error
If the $ symbol in the character string is not followed by the { symbol, the character string is processed as a common $ symbol.
If the escape character \ is added before $, no matter whether $ is followed by {, $ is not processed as an interpolation expression.
let d1 = "The $ sign."
// The result of "d1" is: The $ sign.
let d2 = "The \${v}."
// The result of "d2" is: The ${v}.
Interpolation expressions also support user-defined types as long as the types meet the ToString interface restrictions. The preceding types meet the ToString interface requirement.
Tuple
In the Cangjie programming language, a Tuple type is an immutable type formed by combining a plurality of different types, and is represented by (Type1, Type2, ..., TypeN), where N indicates a dimension of the tuple. The tuple types are described as follows:
-
Type1toTypeNcan be of any type (N must be greater than or equal to 2, that is,Tuplemust be at least binary). -
For each dimension of a tuple, for example, the Kth dimension, an instance of any
TypeKsubtype may be stored. -
When
Type1toTypeNin(Type1, Type2, ..., TypeN)use==for value equality (!=for value inequality), theTupletype supports==for value equality (!=is used for value inequality). Otherwise, theTupletype does not support==and!=. In this case, if==and!=are used, an error is reported during compilation. TwoTupleinstances of the same type are equal only when all elements in the same position (index) are equal (indicating that their lengths are equal).
The syntax of the Tuple type is defined as follows:
tupleType
: '(' type (',' type)+ ')'
;
Tuple Literals
A tuple literal is expressed in the format of (expr1, expr2, ..., exprN). Multiple expressions are separated by commas (,), and each expression can have a different type. The syntax of the Tuple literal is defined as follows:
tupleLiteral
: '(' expression (',' expression)+ ')'
;
Example of tuple literals:
(3.14, "PIE")
(2.4, 3.5)
(2, 3, 4)
((2, 3), 4)
Deconstructing a Tuple
A tuple can also be used to deconstruct another tuple value. Elements at different positions in a tuple are bound to different variables. The following example shows how to deconstruct the return value of a multi-return-value function:
func multiValues(a: Int32, b: Int32): (Int32, Int32) {
return (a + b, a - b) // The type of the return value of the function multiValues is (Int32, Int32).
}
main() {
var (x, y) = multiValues(8,24) // Define an anonymous tuple who has two elements, i.e., x and y.
print("${x}") // output: 32
print("${y}") // output: -16
return 0
}
Access Through a Tuple Index
A tuple allows you to access an element in a specific position using tupleName[index]. (index starts from 0 and can only be a literal of the Int64 type.)
main() {
var z = multiValues(8, 24) // the type of z is inferred to be (Int32, Int32)
print("${z[0]}") // output: 32
print("${z[1]}") // output: -16
return 0
}
Defining a Variable of the Tuple Type
When defining a variable of the tuple type, you can omit the type annotation. The compiler infers the specific type based on the program context.
let tuplePIE = (3.14, "PIE") // The type of tuplePIE is inferred to be (Float64, String).
var pointOne = (2.4, 3.5) // The type of pointOne is inferred to be (Float64, Float64).
var pointTwo = (2, 3, 4) // The type of pointTwo is inferred to be (Int64, Int64, Int64).
var pointThree = ((2, 3), 4) // The type of pointThree is inferred to be ((Int64, Int64), Int64).
Range
The Cangjie programming language uses Range<T> to indicate the Range type, and requires that T can be instantiated only to implement the Comparable interface and Countable interface types. The Range type is used to represent a sequence with a fixed step and is an immutable type.
Each Range<T> instance contains the following values: start, end, and step. start and end indicate a start value and an end value of the sequence, respectively. step indicates a difference between two adjacent elements in the sequence.
The Range type can be determined to equality using == (inequality using !=). Two Range instances of the same type are the same only when they are both left-closed and right-open or left-closed and right-closed, the values of start, end, and step are the same.
Creating a Range Instance
There are two Range operators: .. and ..=, which are used to create left-closed and right-open Range instances and left-closed and right-closed Range instances, respectively. They are used in start..end:step and start..=end:step, in which start, end, and step are expressions. The two expressions are described as follows:
-
The types of
startandendmust be the same, and the type ofstepmust beInt64. -
In the expression
start..end:step, whenstep > 0andstart >= endorstep < 0andstart <= end,start..end:stepreturns an emptyRangeinstance (an empty sequence that does not contain any element). If the result ofstart..end:stepis a non-emptyRangeinstance, the number of elements in theRangeinstance is equal to the round-up value of(end-start)/step(that is, greater than or equal to the minimum integer of(end-start)/step). -
In the expression
start..=end:step, whenstep > 0andstart > endorstep < 0andstart < end,start..=end:stepreturns an emptyRangeinstance. If the result ofstart..=end:stepis a non-emptyRangeinstance, the number of elements in theRangeinstance is equal to the round-down value of((end-start)/step)+1(that is, less than or equal to the maximum integer of((end-start)/step)+1).
let range1 = 0..10:1 // Define a half-open range [0,10) (with step = 1) which contains 0, 1, 2, 3, 4, 5, 6, 7, 8 and 9.
let range2 = 0..=10:2 // Define a closed range [0,10] (with step = 2) which contains 0, 2, 4, 6, 8 and 10.
let range3 = 10..0:-2 // Define a half-open range [10,0) (with step = -2) which contains 10, 8, 6, 4 and 2.
let range4 = 10..=0:-1 // Define a closed range [10,0] (with step = -1) which contains 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 and 0.
let range5 = 10..0:1 // Define an empty range.
let range6 = 0..0:1 // Define an empty range.
let range7 = 0..=0:1 // Define a closed range [0,0] (with step = 1) which only contains 0.
let range8 = 0..=10:-1 // Define an empty range.
- In the
start..end:stepandstart..=end:stepexpressions, the value ofstepcannot be0.
let range9 = 0..10: 0 // error: the value of the step should not be zero.
let range10 = 0..=10: 0 // error: the value of the step should not be zero.
- When the
Range<Int64>instance is used in the index operator[],startorendcan be omitted. In other scenarios,startandendare required, and onlystepis optional (step=1is used by default).
let range11 = 1..10 // Define a half-open range [1, 10) with step = 1.
let range12 = 1..=10 // Define a closed range [1, 10] with step = 1.
let range13 = ..10 // error: the start value is required.
let range14 = 1.. // error: the end value is required.
let range15 = .. // error: the start and end value are required.
Using a Range Type
Expressions of the Range type are mainly used in for-in expressions (see [Loop Expressions]) and are used as indexes to truncate segments. Note that:
- When an instance of the
Range<Int64>type is used in the index operator[], bothstartandendare optional. Their values are determined by the context. If the index operator[]is overloaded on a custom type and the parameter type isRange<Int64>, the value ofstartis0whenstartis omitted, and the value ofendis equal to the maximum value ofInt64whenendis omitted.
Defining a Variable of the Range Type
When defining a variable of the Range<T> type, you can explicitly add a type annotation or omit the type annotation (in this case, the compiler infers the type annotation based on the program context).
let range16: Range<Int64> = 0..10 // Define a half-open range [0,10) with step = 1
let range17: Range<Int64> = -10..10:2 // Define a half-open range [-10,10) with step = 2
let range18 = 0..=10 // Define a closed range [0,10] with step = 1
let range19 = -10..=10:2 // Define a closed range [-10,10] with step = 2
Function
A function type represents a type of a specific function, and is immutable. The syntax of the function type is defined as follows:
arrowType
: parameterTypes '->' type
;
parameterTypes
: '(' (type (',' type)*)? ')'
;
parameterTypes (parameter type list) and type (return type) are connected using ->. A pair of () in the parameter type list is required. () can contain zero or more types (multiple types are separated by ,).
() -> Int32 // A function type has an empty parameter type list, and its return value type is 'Int32'.
() -> Unit // A function type has an empty parameter type list, and its return value type is 'Unit'.
(Float32) -> Int32 // A function type has one parameter whose type is 'Float32', and its return value type is 'Int32'
(Int32, Int32, Float32) -> Int32 // A function type has three parameters, and its return value type is 'Int32'
(Int32, Int32, Float32) -> (Int32, Int32) // A function type has three parameters, and its return value type is '(Int32, Int32)'
(Int32, Int32, Float32) -> Unit // A function type has three parameters, and its return value type is 'Unit'.
In the Cangjie programming language, a function is a first-class citizen, which indicates that the function can be used as a parameter of another function, can be used as a return value of another function, or can be directly assigned to a variable.
The function type does not support == or != for equality or inequality, respectively. For more information about functions, see [Functions].
Enum
The enum type is an immutable type used to organize a set of related values (which are called constructor). A value of the enum type can be obtained only from a constructor defined in the enum type at any time.
Defining the Enum Type
The syntax for defining the enum type is as follows:
enumDefinition
: enumModifier? 'enum' identifier typeParameters? ('<:' superInterfaces)? genericConstraints? '{' enumBody '}'
;
enumBody
: '|'? caseBody ('|' caseBody)* (functionDefinition | operatorFunctionDefinition | propertyDefinition | macroExpression)*
;
caseBody
: identifier ('(' type (',' type)* ')')?
;
enumModifier is the modifier (that is, public) of the enum type, enum is the keyword, identifier is the name of the enum type, and typeParameters and genericConstraints are the type variable list and type variable constraints, respectively. For details, see [Generics]. Multiple values of caseBody (constructors) can be defined in enumBody. Each constructor can have no parameter or a group of parameters of different types. Multiple constructors are separated by |. Before the first constructor, | can exist, which is optional. You can define other members of the enum after caseBody, including member functions, operator overloading functions, and member attributes.
The following is an example of the enum type:
/*
Define an enum type 'TimeUnit1' who has four constructors: 'Year', 'Month', 'Day' and 'Hour'.
*/
enum TimeUnit1 {
Year | Month | Day | Hour
}
/*
'TimeUnit2' has four constructors: 'Year(Int32)', 'Month(Int32, Float32)', 'Day(Int32, Float32, Float32)' and 'Hour(Int32, Float32, Float32, Float32)'.
*/
enum TimeUnit2 {
| Year(Float32)
| Month(Float32, Float32)
| Day(Float32, Float32, Float32)
| Hour(Float32, Float32, Float32, Float32)
}
/*
Define a generic enum type 'TimeUnit3<T1, T2>' who has four constructors: 'Year(T1)', 'Month(T1, T2)', 'Day(T1, T2, T2)' and 'Hour(T1, T2, T2, T2)'.
*/
enum TimeUnit3<T1, T2> {
| Year(T1)
| Month(T1, T2)
| Day(T1, T2, T2)
| Hour(T1, T2, T2, T2)
}
Pay attention to the following points about the enum type:
-
The
enumtype can be defined only intop-level. -
The
constructordefinition without parentheses or parameters is not supported. The type of theconstructorwithout parameters is the definedenumtype and is not considered as the function type. Aconstructorwith parameters has a function type, but cannot be used as a first-class citizen. For example:
enum E {
| mkE // OK. The type of mkE is E but not () -> E.
| mkE() // Syntax error.
}
enum E1 {
| A
}
let a = A // ok, a: E1
enum E2 {
| B(Bool)
}
let b = B // error
enum E3 {
| C
| C(Bool)
}
let c = C // ok, c: E3
-
As a user-defined type, the
enumtype does not support==for equality or!=for inequality by default. You can overload the==or!=operator (see [Operator Overloading]) to enable the customenumtype to support==or!=. -
The same
enumsupportsconstructoroverloading, but only the number of parameters is involved in overloading. That is, multipleconstructorswith the same name can be defined in the sameenum, but the number of parameters must be different. (Aconstructorwithout parameters can be overloaded with otherconstructorsalthough it is not a function type.) For example:
enum TimeUnit4 {
| Year
| Year(Int32) // ok
| Year(Float32) // error: redeclaration of 'Year'
| Month(Int32, Float32) // ok
| Month(Int32, Int32) // error: redeclaration of 'Month'
| Month(Int32) // ok
| Day(Int32, Float32, Float32) // ok
| Day(Float32, Float32, Float32) // error: redeclaration of 'Day'
| Day(Float32, Float32) // ok
| Hour(Int32, Float32, Float32, Float32) // ok
| Hour(Int32, Int32, Int32, Int32) // error: redeclaration of 'Hour'
| Hour(Int32, Int32, Int32) // ok
}
- The
enumtype supports recursion and mutual recursion. For example:
// recursive enum
enum TimeUnit5 {
| Year(Int32)
| Month(Int32, Float32)
| Day(Int32, Float32, Float32)
| Hour(Int32, Float32, Float32, Float32)
| Twounit(TimeUnit5, TimeUnit5)
}
// mutually recursive enums
enum E1 {
A | B(E2)
}
enum E2 {
C(E1) | D(E1)
}
-
The
enumtype cannot be inherited. -
The
enumtype can implement interfaces. For details about the interfaces, see [Classes and Interfaces].
Accessing the Enum Value
The first method is to add the enum name to the constructor name. For example:
let time1 = TimeUnit1.Year
let time2 = TimeUnit2.Month(1.0, 2.0)
let time3 = TimeUnit3<Int64, Float64>.Day(1, 2.0, 3.0)
The second method is to omit the enum name. For example:
let time4 = Year // syntax sugar of 'TimeUnit1.Year'
let time5 = Month(1.0, 2.0) // syntax sugar of 'TimeUnit2.Month(1.0, 2.0)'
let time6 = Day<Int64, Float64>(1, 2.0, 3.0) // syntax sugar of 'TimeUnit3<Int64, Float64>.Day(1, 2.0, 3.0)'
Note: The Cangjie language supports type derivation of generic parameters. Therefore, the generic parameter <T> can be omitted when the preceding method 1 and method 2 are used for access.
The second method must meet the following rules:
If there is no conflict with other variable names, function names, type names, or package names, you can omit the type prefix. Otherwise, the variable name, function name, type name, or package name is preferred.
package p
}
func g() {
let a = A // ok, find p.A
let b = E.A // ok
let c = p.A // ok
F(1) // ok, find p.F
E.F(1) // ok
p.F(1) // ok
let x: C // ok, find p.C
let y: p.C // ok
}
When the enum constructor is used without the type prefix, the generic parameters of the enum type can be declared after the constructor name.
enum E<T> {
| A(T)
| B(T)
}
func f() {
let a = A(1) // ok, a: E<Int64>
let b = A<Int32>(2) // ok, b: E<Int32>
}
Deconstructing Enums
The match expression and enum pattern can be used to deconstruct the enum type. For details, see [Patterns].
The following is an example of deconstructing the TimeUnit1 and TimeUnit2 defined in the preceding section:
let time12 = TimeUnit1.Year
let howManyHours = match (time12) {
case Year => 365 * 24 // matched
case Month => 30 * 24
case Day => 24
case Hour => 1
}
let time13 = TimeUnit2.Month(1.0, 1.5)
let howManyHours = match (time13) {
case Year(y) => y * 365.0 * 24.0
case Month(y, m) => y * 365.0 * 24.0 + m * 30.0 * 24.0 // matched
case Day(y, m, d) => y * 365.0 * 24.0 + m * 30.0 * 24.0 + d * 24.0
case Hour(y, m, d, h) => y * 365.0 * 24.0 + m * 30.0 * 24.0 + d * 24.0 + h
}
Other Members of Enum
You can define member functions, operator functions, and member attributes in the enum type.
- For details about how to define a common member function, see [Function Definition].
- For details about the syntax for defining an operator function, see [Operator Overloading].
- For details about the syntax for defining member attributes, see [Attribute Definition].
The following are some simple examples of enum definitions:
enum Foo {
A | B | C
func f1() {
f2(this)
}
static func f2(v: Foo) {
match (v) {
case A => 0
case B => 1
case C => 2
}
}
prop item: Int64 {
get() {
f1()
}
}
}
main() {
let a = Foo.A
a.f1()
Foo.f2(a)
a.item
}
A constructor in the enum type, a static member function, and an instance member function cannot be overloaded. Therefore, the names of constructors in the enum type, static member functions, instance member functions, and member attributes must be unique.
Option
Option type is a generic enum type.
enum Option<T> { Some(T) | None }
Some and None are two constructors. Some(T) indicates a state with a value, and None indicates a state without a value. The type variable T is instantiated into different types, and different Option types, for example, Option<Int32> and Option<String>, are obtained.
Another method of writing the Option type is to add ? before the type name. That is, for any Type, ?Type is equivalent to Option<Type>. For example, ?Int32 is equivalent to Option<Int32>, ?String is equivalent to Option<String>, and so on.
Pay attention to the following points about the Option type:
- Although
TandOption<T>are of different types, you can directly specify a value of theTtype when a value of theOption<T>type is required at a certain position. The compiler encapsulates the value of theTtype intoSome constructorof theOption<T>type. For example, the following definition is valid:
let v1: ?Int64 = 100
let v2: ??Int64 = 100
- For two unequal types
T1andT2, even if there is a subtype relationship between them, there is no subtype relationship betweenOption<T1>andOption<T2>.
The Option type complies with the usage rules of the generic enum type. For example, you can define a series of variables of different Option types.
let opInt32_1 = Some(100) // The type of 'opInt32_1' is 'Option<Int32>'
let opInt32_2 = Option<Int32>.None // The type of 'opInt32_2' is 'Option<Int32>'
let opChar = Some('m') // The type of 'opChar' is 'Option<Rune>'
let opBool = Option<Bool>.None // The type of 'opBool' is 'Option<Bool>'
let opEnum = Some(TimeUnit1.Year) // The type of 'opEnum' is 'Option<TimeUnit1>'
Deconstructing Option Values
Several methods are provided to facilitate the deconstruction of option values: pattern matching, getOrThrow function, coalescing operator (??), and question mark operator (?).
- The Option type is an enum type. Therefore, the enum pattern in the pattern matching expression can be used to deconstruct the
Optionvalue. For details, see [Enum Pattern].
let number1 = match (opInt32_1) {
case Some(num) => num // matched
case None => 0
}
let number2 = match (opInt32_2) {
case Some(num) => num
case None => 0 // matched
}
let enumValue = match (opEnum) {
case Some(tu) => match (tu) {
case Year => "Year" // matched
case Month => "Month"
case Day => "Day"
case Hour => "Hour"
}
case None => "None"
}
- For the expression
eof theOption<T>type, thegetOrThrow()orgetOrThrow(exception: ()->Exception)function is called to deconstructe. If the value ofeis equal to that ofOption<T>.Some(v), the value ofe.getOrThrow()ore.getOrThrow(lambdaArg)is equal to that ofv. If the value ofeis equal to that ofOption<T>.None,e.getOrThrow()throws aNoneValueExceptionexception during running (e.getOrThrow(lambdaArg)throws an exception specified inlambdaArgduring running).
let number1 = opInt32_1.getOrThrow() // number1 = 100
let number2 = opInt32_2.getOrThrow() // throw NoneValueException
let number3 = opInt32_2.getOrThrow{ MyException("Get None value") } // throw MyException
- For the
e1expression of theOption<T>type and thee2expression of theTtype, the type of thee1 ?? e2expression isT. When the value ofe1is equal to that ofOption<T>.Some(v), the value ofe1 ?? e2is equal to that ofv. When the value ofe1is equal to that ofOption<T>.None, the value ofe1 ?? e2is equal to that ofe2. About??For details about operators, see [coalescing Expressions].
let number1 = opInt32_1 ?? 0 // number1 = 100
let number2 = opInt32_2 ?? 0 // number2 = 0
- The question mark operator (
?) is a unary suffix operator.?It must be used together with the suffix operators.,(),{}, or[]so thatOption<T>supports these suffix operators. For an expressioneof theOptiontype,e?.bindicates thatOption<T>.Some(b)is obtained wheneisSome; otherwise,Option<T>.Noneis obtained (T is of the b type). The same rule applies to other operators. About?For details about operators, see [Question Mark Operators].
class C {
var item = 100
}
let c = C()
let c1 = Some(c)
let c2 = Option<C>.None
let r1 = c1?.item // r1 = Option<Int64>.Some(100)
let r2 = c2?.item // r2 = Option<Int64>.None
Mutable Types
The following sequentially describes mutable types in the Cangjie programming language.
Array
The Cangjie programming language uses the generic type Array<T> to represent the Array type. The Array type is used to store a series of elements of the same type (or having a common parent class). The description of Array<T> is as follows:
-
The elements in
Array<T>are ordered and can be accessed by index (starting from0). -
The length of the
Arraytype is fixed. That is, once anArrayinstance is created, the length cannot be changed. -
Arrayis a reference type and is defined as a variable of the reference type. The variable name stores the reference pointing to the data value. Therefore, when a value is assigned or a function specifies parameters, theArraytype copies the reference. -
When the type variable
Tis instantiated into different types, differentArraytypes are obtained. For example,Array<Int32>andArray<Float64>indicateArrayof theInt32type andArrayof theFloat64type, respectively. -
When
TypeinArray<T>supports==for equality and!=for inequality,Array<T>supports==and!=. Otherwise,Array<T>does not support==and!=. If==and!=are used, an error is reported during compilation. Two instance values forArray<T>of the same type are equal only when all elements in the same position (index) are equal (indicating that their lengths are equal). -
A multidimensional
Arrayis defined as anArrayof anArrayand is represented byArray<Array<...>>. For example,Array<Array<Int32>>indicates a two-dimensionalArrayof theInt32type. -
When
ElementTypehas subtypes,Array<ElementType>can store instances of anyElementTypesubtype. For example,Array<Object>can store instances of anyObjectsubtype.
Creating an Array Instance
You can create an Array instance in either of the following ways.
Constructors
// Array<T>()
let emptyArr1 = Array<Int64>() // create an empty array whose type is Array<Int64>
let emptyArr2 = Array<String>() // create an empty array whose type is Array<String>
// Array<T>(size: Int64, initElement: (Int64)->T)
let array3 = Array<Int64>(3) { i => i * 2 } // 'array3' has 3 elements: 0, 2, 4
let array4 = Array<String>(2) { i => "$i" } // 'array4' has 2 elements: "0", "1"
Array Literals
Array literals are in the format of [element1, element2, ..., elementN]. Multiple elements are separated by commas (,). Each element can be a common expression expressionElement. The syntax of an array literal is defined as follows:
arrayLiteral : '[' (elements)? ']' ;
elements : element (',' element)* ;
element : expression ;
Each element of the Array literal is an expression.
let emptyArray: Array<Int64> = [] // empty Array<Int64>
let array0 = [1, 2, 3, 3, 2, 1] // array0 = [1, 2, 3, 3, 2, 1]
let array1 = [1 + 3, 2 + 3, 3 + 3] // array1 = [4, 5, 6]
- When the context does not have specific type requirements, if the minimum common parent type of all
elementsisT, the type of theArrayliteral isArray<T>. - If the context has specific type requirements, all
elementtypes must be subtypes of theelementtype required by the context.
Accessing Elements in an Array
For the arr instance of the Array type, elements in arr can be accessed in the following ways:
Element accessing at a specific position: Access an element at a specific position using arr[index1]...[indexN] (index1, ..., indexN are index expressions of the Int64 type). For example:
let array5 = [0, 1]
let element0 = array5[0] // element0 = 0
array5[1] = 10 // change the value of the second element of 'array5' through index
let array6 = [[0.1, 0.2], [0.3, 0.4]]
let element01 = array6[0][1] // element01 = 0.2
array6[1][1] = 4.0 // change the value of the last element of 'array6' through index
Iterative accessing: Iteratively access elements in arr using the for-in expression (see [for-in Expressions]). For example:
func access() {
let array8 = [1, 8, 0, 1, 0]
for (num in array8) {
print("${num}") // output: 18010
}
}
Size of the Accessed Array
The number of elements in arr can be returned in arr.size mode.
let array9 = [0, 1, 2, 3, 4, 5]
let array10 = [[0, 1, 2], [3, 4, 5]]
let size1 = array9.size // size1 = 6
let size2 = array10.size // size2 = 2
Slice of an Array
When a value of the Range<Int64> type is used as the Array index, a segment (called slicing) of the Array is truncated. Note that:
- The value of
stepmust be1. Otherwise, an exception will be thrown. - The type returned by
slicingis the same as that ofArrayand is the reference of the originalArray. Modifications on elements in a slice affect the original array. - When
start..endis used as the index ofArray, ifstartis omitted, the value ofstartis set to0. Ifendis omitted, the value ofendis set to the length ofArray. - When
start..=endis used as the index ofArray, ifstartis omitted, the value ofstartis set to0.endin thestart..=endformat cannot be omitted. - If the index is an empty
range, an emptyArrayis returned.
let array7 = [0, 1, 2, 3, 4, 5]
func slicingTest() {
array7[0..5] // [0, 1, 2, 3, 4]
array7[0..5:1] // [0, 1, 2, 3, 4]
array7[0..5:2] // runtime exception
array7[5..0:-1] // runtime exception
array7[5..0:-2] // runtime exception
array7[0..=5] // [0, 1, 2, 3, 4, 5]
array7[0..=5:1] // [0, 1, 2, 3, 4, 5]
array7[0..=5:2] // runtime exception
array7[5..=0:-1] // runtime exception
array7[5..=0:-2] // runtime exception
array7[..4] // [0, 1, 2, 3]
array7[2..] // [2, 3, 4, 5]
array7[..] // [0, 1, 2, 3, 4, 5]
array7[..4:2] // error: the 'range step' is not allowed here.
array7[2..:-2] // error: the 'range step' is not allowed here.
array7[..:-1] // error: the 'range step' is not allowed here.
array7[..=4] // [0, 1, 2, 3, 4]
array7[..=4:2] // error: the 'range step' is not allowed here.
array7[0..5:-1] // runtime exception
array7[5..=0] // []
array7[..0] // []
array7[..=-1] // []
let temp: Array<Int64> = array7[..]
temp[0] = 6 // temp == array7 == [6, 1, 2, 3, 4, 5]
}
When slicing is used to assign values, the following usages are supported:
- If the expression on the right of
=is of the array element type, the value of the expression is used as the element that covers the slice range. - If the type of the expression on the right of
=is the same as that of the array, the array will be copied to overwrite the current slice range. In this case, the size of the expression on the right must be the same as that of the slice range. Otherwise, an exception will be thrown during running.
let arr = [1, 2, 3, 4, 5]
arr[..] = 0
// arr = [0, 0, 0, 0, 0]
arr[0..2] = 1
// arr = [1, 1, 0, 0, 0]
arr[0..2] = [2, 2]
// arr = [2, 2, 0, 0, 0]
arr[0..2] = [3, 3, 3] // runtime exception
arr[0..2] = [4] // runtime exception
let arr2 = [1, 2, 3, 4, 5]
arr[0..2] = arr2[0..2] // ok
// arr = [1, 2, 0, 0, 0]
arr[0..2] = arr2 // runtime exception
arr[..] = arr2
VArray
The Cangjie programming language uses the generic type VArray<T, $N> to represent the VArray type. The VArray type is used to store a series of elements of the same type (or having a common parent class). The description of VArray<T, $N> is as follows:
-
The elements in
VArray<T, $N>are ordered and can be accessed by index (starting from0). If the provided index is greater than or equal to its length and the index can be deduced during compilation, a compilation error is reported. Otherwise, an exception is thrown during running. -
The length of the
VArray<T, $N>type is a part of the type.Nindicates the length of VArray. It must be an integer literal and is marked by the fixed syntax$plus a number. If the provided integer literal is a negative number, an error is reported during compilation. -
VArrayis a variable of the value type. The variable name stores the data value. Therefore, when a value is assigned or a function specifies parameters, the value is copied for theVArraytype. -
When the type variable
Tis instantiated into different types or the length of the$Nannotation is different, differentVArraytypes are obtained. For example,VArray<Int32, $5>andVArray<Float64, $5>are different types ofVArray.VArray<Int32, $2>andVArray<Int32, $3>are different types ofVArray. -
When the generic variable
TofVArrayis of theVArraytype, it indicates a multi-dimensionalVArraytype.
Creating a VArray Instance
VArray can also use the Array literal to create an instance. This method can be used only when the literal is of the VArray type in the context. Otherwise, the literal is of the Array type.
The length of VArray must be the literal type of Int64 and must be the same as the length of the provided literal array. Otherwise, a compilation error is reported.
let arr1: VArray<Int64, $5> = [1,2,3,4,5]
let arr2: VArray<Int16, $0> = []
let arr3: VArray<Int16, $0> = [1] // error
let arr4: VArray<Int16, $-1> = [] // error
In addition, VArray can be used to create instances in the form of constructors.
// VArray<T, $N>(initElement: (Int64)->T)
let arr5: VArray<Int64, $5> = VArray<Int64, $5> { i => i } // [0, 1, 2, 3, 4]
// VArray<T, $N>(item!: T)
let arr6: VArray<Int64, $5> = VArray<Int64, $5>(item: 0) // [0, 0, 0, 0, 0]
VArray does not allow the default type parameter <T, $N>.
Accessing Elements in a VArray
For the arr instance of the VArray type, elements in arr can be accessed in the following ways:
Access an element at a specific position using arr[index1]...[indexN] (index1,...,indexN are index expressions of the Int64 type). For example:
Note that the index operation of VArray returns the copy of the specified element. It indicates that if the element is of the value type, the index value will get a new instance that cannot be modified. For multi-dimensional VArray types, the inner VArray cannot be modified by arr[n][m] = e because the inner VArray obtained by arr[n] is a new VArray instance that has been copied.
var arr7: VArray<Int64, $2> = [0, 1]
let element0 = arr7[0] // element0 = 0
arr7[1] = 10 // change the value of the second element of 'arr7' through index
// Get and Set of multi-dimensional VArrays.
var arr8: VArray<VArray<Int64, $2>, $2> = [[1, 2], [3, 4]]
let arr9: VArray<Int64, $2> = [0, 1]
let element1 = arr8[1][0] // element1 = 3
arr8[1][1] = 5 // error: function call returns immutable value
arr8[1] = arr9 // arr8 = [[1, 2], [0, 1]]
Obtaining the VArray Lengths
The number of elements in arr can be returned in arr.size mode.
let arr9: VArray<Int64, $6> = [0, 1, 2, 3, 4, 5]
let size = arr9.size // size = 6
VArray in the Function Signature
When VArray is used as a parameter or return value of a function, the length of VArray must be specified.
func mergeVArray(a: VArray<Int64,$2>, b: VArray<Int64, $3>): VArray<Int64, $5> {
var ret = VArray<Int64, $5>(item: 0)
for(i in 0..2) {
ret[i] = a[i]
}
for (i in 0..3) {
ret[a.size + i] = b[i]
}
return ret
}
Struct
The struct type is a mutable type. A series of member variables and member functions can be defined in the struct type. The syntax for defining the struct type is as follows:
structDefinition
: structModifier? 'struct' identifier typeParameters? ('<:' superInterfaces)? genericConstraints? structBody
;
structBody
: '{'
structMemberDeclaration*
structPrimaryInit?
structMemberDeclaration*
'}'
;
structMemberDeclaration
: structInit
| staticInit
| variableDeclaration
| functionDefinition
| operatorFunctionDefinition
| macroExpression
| propertyDefinition
;
structModifier is the modifier of the struct type, struct is the keyword, identifier is the name of the struct type, and typeParameters and genericConstraints are the type variable list and type variable constraints, respectively. For details, see [Generics].In structBody, you can define member variables (variableDeclaration), member attributes (propertyDefinition), main constructors (structPrimaryInit), constructors (structInit), and member functions (including common member functions and operator functions).
Pay attention to the following points about the struct type:
-
structis a variable of the value type. The variable name stores the data value. Therefore, when a value is assigned or a function specifies parameters, the value is copied for thestructtype. -
The
structtype can be defined only intop-level. -
As a user-defined type, the
structtype does not support==for equality or!=for inequality by default. You can overload the==or!=operator (see [Operator Overloading]) to enable the customstructtype to support==or!=. -
The
structtype cannot be inherited. -
The
structtype can implement the [Interface]. -
If the type of one or more non-static member variables of a struct type references the struct itself, the struct type is called a recursive struct type. For multiple struct types, if the types of their non-static member variables form a circular reference, the struct types form a recursion. The struct type defined by recursion (or mutual recursion) is invalid unless at least one
T_iis encapsulated in the class, interface, enum, or function type on eachT_1, T_2, ..., T_Nof the recursion chain. That is, the class, interface, enum, or function type can be used to implement recursion (or mutual recursion). The struct definition is legalized.
The following is an example of the struct type:
struct Rectangle1 {
let width1: Int32
let length1: Int32
let perimeter1: () -> Int32
init (width1: Int32, length1: Int32) {
this.width1 = width1
this.length1 = length1
this.perimeter1 = { => 2 * (width1 + length1) }
}
init (side: Int32) {
this(side, side)
}
func area1(): Int32 { width1 * length1 }
}
// Define a generic struct type.
struct Rectangle2<T> {
let width2: T
let length2: T
init (side: T) {
this.width2 = side
this.length2 = side
}
init (width2!: T, length2!: T) {
this.width2 = width2
this.length2 = length2
}
}
The following is an example of the struct type definition for recursion and mutual recursion:
struct R1 { // error: 'R1' cannot have a member that recursively contains it
let other: R1
}
struct R2 { // ok
let other: Array<R2>
}
struct R3 { // error: 'R3' cannot have a member that recursively contains it
let other: R4
}
struct R4 { // error: 'R4' cannot have a member that recursively contains it
let other: R3
}
struct R5 { // ok
let other: E1
}
enum E1 { // ok
A(R5)
}
struct Member Variables
The syntax for defining a member variable is as follows:
variableDeclaration
: variableModifier* NL* (LET | VAR) NL* patternsMaybeIrrefutable ((NL* COLON NL* type)? (NL* ASSIGN NL* expression) | (NL* COLON NL* type))
;
When defining member variables, pay attention to the following points:
- A member variable defined outside the primary constructor can have an initial value or not. If there is an initial value, the initial value expression can reference only the initialized member variables defined before it and the static member functions in
struct.
Constructors
In the Cangjie programming language, there are two types of constructors: primary constructor and init constructor (constructor for short).
Primary Constructor
The syntax of the primary constructor is defined as follows:
structPrimaryInit
: (structNonStaticMemberModifier)? structName '('
structPrimaryInitParamLists? ')'
'{'
expressionOrDeclarations?
'}'
;
structName
: identifier
;
structPrimaryInitParamLists
: unnamedParameterList (',' namedParameterList)?
(',' structNamedInitParamList)?
| unnamedParameterList (',' structUnnamedInitParamList)?
(',' structNamedInitParamList)?
| structUnnamedInitParamList (',' structNamedInitParamList)?
| namedParameterList (',' structNamedInitParamList)?
| structNamedInitParamList
;
structUnnamedInitParamList
: structUnnamedInitParam (',' structUnnamedInitParam)*
;
structNamedInitParamList
: structNamedInitParam (',' structNamedInitParam)*
;
structUnnamedInitParam
: structNonStaticMemberModifier? ('let' | 'var') identifier ':' type
;
structNamedInitParam
: structNonStaticMemberModifier? ('let' | 'var') identifier '!' ':' type
('=' expression)?
;
The definition of the primary constructor consists of the following parts:
-
Modifier (Optional): It can be modified by all access modifiers. The default accessibility is
internal. For details, see Access Modifiers in [Packages and Modules]. -
Primary constructor name: same as the type name. Do not use the
funckeyword before the primary constructor name. -
Parameter list: Different from the init constructor, the primary constructor has two types of parameters: ordinary parameters and member variable parameters. The syntax and semantics of ordinary parameters are the same as those of parameters in the function definition.
Member variable parameters are introduced to reduce code redundancy. The definition of a member variable parameter includes the definition of a parameter and a member variable. In addition, it indicates the semantics of assigning a value to a member variable through a parameter. Omitted definitions and expressions are automatically generated by the compiler.
-
The syntax of member variable parameters is the same as that of member variable definition. In addition,
!can be used to indicate whether a member variable parameter is a named parameter. -
The modifiers of the member variable parameter are
public,private,protected, andinternal. For details, see Access Modifiers in [Packages and Modules]. -
Member variable parameters only support non-static member variables. That is, they cannot be modified by
static. -
A member variable parameter cannot have the same name as a member variable outside the primary constructor.
-
A member variable parameter can have no initial value. This is because the compiler generates a corresponding constructor for the primary constructor and assigns values to member variables in the constructor body.
-
A parameter of a member variable can have an initial value. The initial value expression can reference other parameters or member variables (excluding instance member variables defined outside the primary constructor) that have been defined before the member variable is defined, but cannot modify the values of these parameters and member variables. Note that the initial value of the member variable parameter is valid only in the primary constructor and is not included in the member variable definition.
-
Member variable parameters cannot be followed by ordinary parameters. In addition, the parameter sequence must comply with that defined in the function. Parameters of named variables cannot be followed by non-named parameters.
- Main constructor body: The main constructor cannot call other constructors in this struct. Declarations and expressions can be written in the main constructor body. The declarations and expressions must meet the requirements of the init constructor.
The following is an example of the primary constructor definition:
struct Test{
static let counter: Int64 = 3
let name: String = "afdoaidfad"
private Test(
name: String, // regular parameter
annotation!: String = "nnn", // regular parameter
var width!: Int64 = 1, // member variable parameter with initial value
private var length!: Int64, // member variable parameter
private var height!: Int64 = 3 // member variable parameter
) {}
}
The primary constructor is the syntactic sugar of the init constructor. The compiler automatically generates the definitions of the constructor and member variables corresponding to the primary constructor. The constructor is automatically generated as follows:
- Its modifier is the same as that of the primary constructor.
- The sequence of parameters from left to right is the same as that declared in the parameter list of the primary constructor.
- The form of the constructor body is as follows:
- Values are assigned to member variables. The syntax format is
this.x = x, wherexis the member variable name. - Then, other code of the main constructor body is used.
- Values are assigned to member variables. The syntax format is
struct B<X,Y> {
B(
x: Int64, // primary constructor, it's name is the same as the struct
y: X,
v!: Int64 = 1, // regular parameter
private var z!: Y // member variable parameter
) {}
/* The corresponding init constructor with primary constructor auto-generated
by compiler.
private var z: Y // auto generated member variable definition
init( x: Int64, y: X, v!: Int64 = 1, z!: Y) { // auto generated named parameter definition
this.z = z // auto generated assign expression of member variable
}
*/
}
A struct can define only one primary constructor. In addition to the primary constructor, other constructors can be defined as usual, but other constructors and the constructor corresponding to the primary constructor must be overloaded.
init Constructor
In addition to the main constructor, you can also customize a constructor. The constructor is defined by using the keyword init, parameter list, and function body. Multiple constructors can be defined in a struct, but they and the constructor corresponding to the main constructor must form an overload. For details about function overload, see [Function Overload Definition]. Note:
-
The parameters of a constructor can have default values. Do not use the instance member variable
this.variableNameand its syntactic sugarvariableNameas the default values of constructor parameters. -
Before all instance member variables are initialized, the constructor cannot use implicit parameter passing or functions or
lambdathat capturethis. However,this.variableNameor its syntactic sugarvariableNamecan be used to access the initialized member variablevariableName. -
The
lambdaand nested functions in constructors cannot capturethis, andthiscannot be used independently as an expression. -
In a constructor,
thiscan be used to call other constructors. If this function is called, it must be called at the first expression in the constructor body. No expression or declaration is allowed before this function is called. Do not call expressions to call constructors throughthisoutside the constructor. -
If the constructor does not explicitly call other constructors, ensure that all instance member variables declared by the
structare initialized beforereturn. Otherwise, an error is reported during compilation. -
The compiler performs dependency analysis on the calling relationships between all constructors. If a constructor is called cyclically, an error is reported during compilation.
-
The return type of the constructor is
Unit.
If a struct does not define a primary constructor or an init constructor, it attempts to generate a parameterless constructor (public-modified). If the instance member variable of the struct does not have an initial value, an error is reported during compilation.
Other Members of Struct
You can also define member functions, operator functions, member attributes, and static initializers in struct.
- For details about how to define a common member function, see [Function Definition].
- For details about the syntax for defining an operator function, see [Operator Overloading].
- For details about the syntax for defining member attributes, see [Attribute Definition].
- For details about the syntax for defining a static initializer, see [Static Initializers].
Modifiers in the Struct
A struct and its members in a class can be modified using access modifiers. For details, see Access Modifiers in [Packages and Modules].
Member functions and variables can be modified using static. These members are static members of the struct type, not members of the struct instance. Outside the struct definition, instance member variables and instance member functions can be accessed only through the struct instance, and static member variables and static member functions can be accessed only through the name of the struct type.
In addition, a function can be modified by mut, which is a special instance member function. For details about the mut function, see [mut Functions].
The struct constructor and the member variables defined in the main constructor can be modified only by using access modifiers.
Instantiation of Structs
After defining the struct type, you can create a struct instance. The struct instance can be defined in either of the following methods based on whether the instance contains type variables:
- Define an instance of the non-general
struct:StructName(arguments).StructNameindicates the name of thestructtype, andargumentsindicates the argument list.StructName(arguments)calls the corresponding constructor based on the calling rule of overloaded functions (see [Function Overloading Resolution]), and then generates an instance ofStructName. For example:
let newRectangle1_1 = Rectangle1(100, 200) // Invoke the first custom constructor.
let newRectangle1_2 = Rectangle1(300) // Invoke the second custom constructor.
- Define an instance of the generic
struct:StructName<Type1, Type2, ..., TypeK>(labelValue1, labelValue2, ..., labelValueN). The only difference from defining an instance of a non-generalstructis that the generic parameter needs to be instantiated. For example:
let newRectangle2_1 = Rectangle2<Int32>(100) // Invoke the custom constructor.
let newRectangle2_1 = Rectangle2<Int32>(width2: 10, length2: 20) // Invoke another custom constructor.
Note that if the variable structInstance of the struct type is defined using let, the value of the member variable varName cannot be changed using structInstance.varName = expr (even if varName is defined using var). If both structInstance and varName are defined using var, the value of varName can be changed using structInstance.varName = expr.
Class and Interface Types
class and interface are reference types and are defined as variables of the reference type. The variable name stores the reference pointing to the data value. Therefore, when a value is assigned or a function specifies parameters, class and interface copy the reference.
For details, see [Classes and Interfaces].
Type Conversion
As a strongly-typed language, the Cangjie programming language supports only explicit type conversion (also referred to as forcible type conversion). For the value type, valueType(expr) can be used to convert the type of the expression expr to valueType. For class and interface, [as Operators]) can be used to implement static type conversion. For the value type, valueTypeA can be converted to valueTypeB. That is, the conversion rule for converting valueTypeA to valueTypeB is defined. For the two value types that do not have the conversion rule, they cannot be converted. For class and interface, if a parent-child type relationship exists between two types in the inheritance relationship diagram, the two types can be converted (the conversion may fail). Otherwise, the two types cannot be converted.
The following describes the type conversion rules between value types and between class and interface types. For other types that are not mentioned, Cangjie does not support type conversion between them in the preceding two methods.
Conversion Between Value Types
For numeric types, only the following conversions are supported:
-
Conversion from the
Runetype to theUInt32type. -
Conversion from integer types (including
Int8,Int16,Int32,Int64,IntNative,UInt8,UInt16,UInt32,UInt64, andUIntNative) to theRunetype. -
Bidirectional conversion between all numeric types (including
Int8,Int16,Int32,Int64,IntNative,UInt8,UInt16,UInt32,UInt64,UIntNative,Float16,Float32, andFloat64).
The conversion from Rune to UInt32 uses the UInt32(e) mode. e is an expression of the Rune type. The result of UInt32(e) is an integer of the UInt32 type corresponding to the Unicode scalar value of e.
The conversion from the integer type to the Rune type uses the Rune(num) mode. The num type can be any integer type. Only when the value of num is in the range [0x0000,0xD7FF] or [0xE000,0x10FFFF] (that is, Unicode scalar value), the character represented by the corresponding Unicode scalar value is returned. Otherwise, an error is reported during compilation (the value of num can be determined during compilation) or an exception is thrown during running.
main() {
var c: Rune = 'a'
var num: UInt32 = 0
num = UInt32(c) // num = 97
num -= 32 // num = 65
c = Rune(num) // c = `A`
return 0
}
To ensure type security, the Cangjie programming language does not support implicit type conversion between numeric types (the type of a numeric literal is inferred from the context, which is not implicit type conversion). To convert a numeric type to another numeric type, you must use the explicit method NumericType(expr) to forcibly convert the expr type to the NumericType type (NumericType indicates any numeric type). If the conversion is successful, a new value of the NumericType type constructed from the expr type is returned. The syntax for numeric type conversion is defined as follows:
numericTypeConvExpr
: numericTypes '(' expression ')'
;
numericTypes
: 'Int8' | 'Int16' | 'Int32' | 'Int64' | 'IntNative' | 'UInt8' | 'UInt16' | 'UInt32' | 'UInt64' | 'UIntNative' | 'Float16' | 'Float32' | 'Float64'
;
If the size relationship between numeric types is defined based on the number of bits occupied by numeric types (a larger number of bits indicates a larger type, and a smaller number of bits indicates a smaller type), the Cangjie programming language supports the following type conversion:
- Bidirectional conversion between signed integer types: When a small value is converted to a large value, the value result remains unchanged. If the value result exceeds the representation range of the small value type, the overflow processing policy is determined based on the attribute macro in the context. By default, the exception throwing policy is used. For details about different overflow policies, see [Arithmetic Expressions]. The following uses the conversion between
Int8andInt16as an example. When an overflow occurs, the policy of throwing an exception is used.
main() {
var i8Number: Int8 = 127
var i16Number: Int16 = 0
i16Number = Int16(i8Number) // ok: i16Number = 127
i8Number = Int8(i16Number) // ok: i8Number = 127
i16Number = 128
i8Number = Int8(i16Number) // throw an ArithmeticException
return 0
}
- The rule of the bidirectional conversion between unsigned integer types is the same as the preceding rule. The following uses the conversion between
UInt16andUInt32as an example (the same rule applies to other cases):
main() {
var u16Number: UInt16 = 65535
var u32Number: UInt32 = 0
u32Number = UInt32(u16Number) // ok: u32Number = 65535
u16Number = UInt16(u32Number) // ok: u16Number = 65535
u32Number = 65536
u16Number = UInt16(u32Number) // throw an ArithmeticException
return 0
}
- Bidirectional conversion between floating-point types uses the
round-to-nearestmode. The following is an example of the conversion betweenFloat32andFloat64:
main() {
var f32Number: Float32 = 1.1
var f64Number: Float64 = 0.0
f64Number = Float64(f32Number) // f64Number = 1.100000023841858
f32Number = Float32(f64Number) // f32Number = 1.1
f64Number = 1.123456789
f32Number = Float32(f64Number) // f32Number = 1.1234568
f32Number = 4.4E38 // f32Number = POSITIVE_INFINITY
f64Number = Float64(f32Number) // f64Number = POSITIVE_INFINITY
f64Number = 4.4E38
f32Number = Float32(f64Number) // f32Number = POSITIVE_INFINITY
f64Number = Float64(f32Number*0.0)
f32Number = Float32(f64Number) // f32Number = NaN
return 0
}
- Bidirectional conversion between signed integer types and unsigned integer types: Because the representation range of any signed integer type cannot contain the representation range of an unsigned integer type of the same length (or vice versa), the conversion is successful if the value of the expression to be converted is within the range of the target integer type during type conversion. Otherwise, the overflow processing policy is determined based on the attribute macro in the context. By default, the exception throwing policy is used. For details about different overflow processing policies, see [Arithmetic Expression]. The following uses the conversion between
Int8andUInt8as an example. When an overflow occurs, the policy of throwing an exception is used.
main() {
var i8Number: Int8 = 127
var u8Number: UInt8 = 0
u8Number = UInt8(i8Number) // ok: u8Number = 127
u8Number = 100
i8Number = Int8(u8Number) // ok: i8Number= 100
i8Number= -100
u8Number = UInt8(i8Number) // throw an ArithmeticException
u8Number = 255
i8Number = Int8(u8Number) // throw an ArithmeticException
return 0
}
-
Conversion from an integer to a floating-point number: The result is a floating-point number as close as possible to the original integer.
POSITIVE_INFINITYorNEGATIVE_INFINITYis returned if the value is out of the representation range of the target type. -
Conversion from a floating-point number to an integer: The
round-toward-zeromode is used to convert a floating-point number to a signed integer. That is, the integer part is retained and the decimal part is discarded. When the integer part exceeds the representation range of the target integer type, the integer overflow policy in the context is used. If the throwing policy is used, an exception is thrown. Otherwise, the conversion rule is as follows:
NaNreturns0.- If the value is less than the lower limit of the integer value range (including negative infinity), the lower limit of the integer value range is returned.
- If the value is greater than the upper limit of the integer value range (including positive infinity), the upper limit of the integer value range is returned.
main() {
var i32Number: Int32 = 1024
var f16Number: Float16 = 0.0
var f32Number: Float32 = 0.0
f16Number = Float16(i32Number) // ok: f16Number = 1024.0
f32Number = Float32(i32Number) // ok: f32Number = 1024.0
i32Number = 2147483647
f16Number = Float16(i32Number) // f16Number = POSITIVE_INFINITY
f32Number = Float32(i32Number) // precision lost: f32Number = 2.14748365E9
f32Number = 1024.1024
i32Number = Int32(f32Number) // ok: i32Number = 1024
f32Number = 1024e10
i32Number = Int32(f32Number) // throw an Exception
f32Number = 3.4e40 // f32Number = POSITIVE_INFINITY
i32Number = Int32(f32Number) // throw an Exception
f32Number = 3.4e40 * 0.0 // f32Number = NaN
i32Number = Int32(f32Number) // throw an Exception
return 0
}
Conversion Between Class and Interface Types
For an instance obj of the class or interface type, if you need to convert its (static) type to another TargetType of the class or interface type, use obj as TargetType.
For details about how to use the as operator and the type conversion rules between the class and interface types, see [as Operators].
Type Aliases
If the name of a type is complex or not intuitive in a specific scenario, you can use a type alias to obtain a simple and intuitive alias for the type. The syntax for defining a type alias is as follows:
typeAlias
: typeModifier? `type` identifier typeParameters? `=` type
;
typeModifier is an optional accessibility modifier (namely, public), type is a keyword, identifier is any valid identifier, type is any type visible to top-level, and identifier and type are connected by using =. In addition, you can define a generic alias by adding a type parameter (typeParameters) after identifier. For details, see [Generics].Through the preceding statement, an alias named identifier is defined for type, and identifier and type are considered as the same type. For example:
type Point2D = (Float64, Float64)
type Point3D = (Float64, Float64, Float64)
let point1: Point2D = (0.5, 0.8)
let point2: Point3D = (0.5, 0.8, 1.1)
The preceding type definition does not define a new type. It is only used to define another name for an existing type. The alias and the original type are considered as the same type, and the alias does not affect the use of the original type.
Rules for Defining Type Aliases
- The definition of a type alias can be used only in
top-level.
func test() {
type Point2D = (Float64, Float64) // error: type alias can only be defined at top-level
type Point3D = (Float64, Float64, Float64) // error: type alias can only be defined at top-level
}
- When
typeis used to define a type alias, the original type must be visible in the position defined bytype.
class LongNameClassA {
}
type ClassB = LongNameClassB // error: use of undeclared type 'LongNameClassB'
- When a generic alias is defined, if the generic alias introduces a generic parameter that is not used in the original type, the compiler generates a warning.
type Class1<V> = GenericClassA<Int64, V> // ok. ClassA is a generic class
type Class2<Value, V> = GenericClassB<Int64, V> // warning: the type parameter 'Value' in 'Class2<Value, V>' is not used in 'GenericClassB<Int64, V>'
type Int<T> = Int32 // warning: the type parameter 'T' in 'Int<T>' is not used in `Int32`
- When defining a generic alias, you are not allowed to add generic constraints to the alias and the type parameters in the original type. When using a generic alias, you can add generic constraints as required. In addition, existing generic constraints in the original type are passed to the alias.
type Class1<V> where V <: MyTrait = GenericClassA<Int64, V> // error: generic constraints are not allowed here
type Class2<V> = GenericClassB<Int64, V> where V <: MyTrait // error: generic constraints are not allowed here
type Class3<V> = GenericClassC<Int64, V>
func foo<V> (p: Class3<V>) where V <: MyTrait { // add generic constraints when 'Class3<V>' is used
functionBody
}
class ClassWithLongName<T> where T<:MyTrait {
classBody
}
type Class<T> = ClassWithLongName<T> // Class<T> also has the constraint 'where T<:MyTrait'
- Circular references (direct or indirect) are not allowed in one or more
typedefinitions. A manner of determining the circular reference is to determine whether the circular reference exists by using a name instead of using a manner of expanding a type.
type Value = GenericClassAp<Int64, Value> // error: 'Value' references itself
type Type1 = (Int64)->Type1 // error: 'Type1' references itself
type Type2 = (Int64, Type2) // error: 'Type2' references itself
type Type3 = Type4 // error: 'Type3' indirectly references itself
type Type4 = Type3
- A type alias is considered as a type equivalent to the original type. For example, in the following example, a parameter of the
Inttype and a parameter of theInt32type may be directly added.Intis defined as an alias ofInt32. Note that aliases cannot be used to overload functions.
type Int = Int32
let numOne: Int32 = 10
let numTwo: Int = 20
let numThree = numOne + numTwo
func add(left: Int, right: Int32): Int { left + right }
func add(left: Int32, right: Int32): Int32 { left + right } // error: invalid redeclaration of 'add : (Int32, Int32)->Int32'
- The default visibility defined by
typeisdefault. To use the type alias defined in thispackagein otherpackages, the following conditions must be met: (1) The visibility modifier of the original type in thispackageispublic; (2) Thetypedefinition uses thepublicmodifier. In addition, note that the alias can have a different visible range from the original type, but the visible range of the alias cannot be greater than the visible range of the original type.
// a.cj
package A
public class ClassWithLongNameA {
}
class ClassWithLongNameB {
}
public type classA = ClassWithLongNameA // ok
type classAInter = ClassWithLongNameA // ok
/* error: classB can not be declared with modifier 'public', as 'ClassWithLongNameB' is internal */
public type classB = ClassWithLongNameB
// b.cj
package B
import A.*
let myClassA: A.classA = ClassWithLongNameA()
Usage of Type Aliases
A type alias can be used in any position that the original type can use on the right-hand side of the equal sign (=).
-
When it is used as a type, for example:
type A = B class B {} var a: A = B() // Use typealias A as type B -
When the type alias refers to a class or struct, it can be used as the constructor name.
type A = B class B {} func foo() { A() } // Use type alias A as constructor of B -
When the type alias refers to a class, interface, or struct, it can be used as the name to access static members or functions.
type A = B class B { static var b : Int32 = 0; static func foo() {} } func foo() { A.foo() // Use A to access static method in class B A.b } -
When the type alias refers to an enum, it can be used as the type name for declaring the constructor of the enum.
enum TimeUnit { Day | Month | Year } type Time = TimeUnit var a = Time.Day var b = Time.Month // Use type alias Time to access constructors in TimeUnit
Relationship Between Types
There are two types of relationships: equality and subtype.
Type Equality
For any two types T1 and T2, T1 and T2 are considered equal (denoted as $T1 \equiv T2$) if they meet any of the following conditions:
-
The type alias definition
type T1 = T2exists. -
In the
classdefinition andextendofclass,T1is the name ofclass, andT2isThis. -
The names of
T1andT2are the same (reflective). -
$T2 \equiv T1$ (symmetry).
-
The type $Tk$ exists, which meets $T1 \equiv Tk$ and $Tk \equiv T2$ (transitive).
Subtypes
For any two types T1 and T2, T1 and T2 are considered as subtypes (denoted as $T1 <: T2$) if they meet any of the following conditions:
-
$T1 \equiv T2$.
-
T1is of theNothingtype. -
Both
T1andT2are of theTupletype, and the type at each position ofT1is the subtype of the type at the position corresponding toT2. -
Both
T1andT2are of theFunctiontype. The parameter type ofT2is the subtype of theT1parameter type, and the return type ofT1is the subtype of theT2return type. -
T1is of anyclasstype, andT2is of theObjecttype. -
Both
T1andT2are of theinterfacetype, andT1inheritsT2. -
Both
T1andT2are of theclasstype, andT1inheritsT2. -
T2is of theinterfacetype, andT1implementsT2. -
The type $Tk$ exists, which meets $T1 <: Tk$ and $Tk <: T2$ (transitive).
Minimum Common Supertype
In a type system with subtypes, the minimum common parent type of two types may be required. For example, the type of the if expression is the minimum common supertype of the types of the two branches. The match expression is similar.
The minimum common supertype between two types is a subtype of all other common supertypes.
The minimum common supertype is defined as follows: For any two types T1 and T2, if the type LUB meets the following rules, LUB is the minimum common supertype of T1 and T2:
- For any type of
Tthat meets bothT1 <: TandT2 <: T,LUB <: Tis true.
Note that if a type in the common supertype is not larger than other types, it is only an extremely small one and not necessarily minimum one.
Maximum Common Subtype
Inversion exists in the subtype relationship (see [Type Variation] in [Generics] for its definition).For example, if the parameter type of the function type is inversion, the maximum common subtype of the two types is required.
The maximum common subtype between two types is a supertype of all other common subtypes.
The maximum common subtype is defined as follows: For any two types T1 and T2, if the type GLB meets the following rules, GLB is the maximum common subtype of T1 and T2:
- For any type of
Tthat meets bothT <: T1andT <: T2,T <: GLBis true.
Note that if a type in the common subtype is not smaller than other types, it is only an extremely large one and not necessarily maximum one.
Type Security
In the case of no data contention, the compiler ensures memory security and type security.
The following is an example in which type security and memory security cannot be ensured:
class C {
var x = 1
var y = 2
var z = 3
}
enum E {
A(Int64) | B(C)
}
var e = A(1)
main() {
spawn {
while (true) {
e = B(C()) // writing to `e`
e = A(0)
}
}
while (true) {
match (e) { // reading from `e`
case A(n) =>
println(n+1)
case B(c) =>
c.x = 2
c.x = 3
c.x = 4
}
}
}
It cannot be ensured because there is data competition between the thread that assigns a value to the variable e and the thread that reads the variable. For more information about data contention, see [Concurrency].