名字、作用域、变量、修饰符

本章首先介绍名字、作用域、遮盖,然后介绍其中一种名字——变量,包括变量的定义和初始化,最后是修饰符。

名字

仓颉编程语言中,我们用名字(names)标识变量、函数、类型、package、module 等实体(entities)。

名字必须是一个合法的[标识符]。

仓颉编程语言的关键字、变量、函数、类型(包括:classinterfacestructenumtype alias)、泛型参数、package 名、module 名共用同一个命名空间,即,在同一个 scope 声明或定义的实体,不允许同名(除了构成重载的名字)。不同 scope 声明或定义的实体允许同名,但可能产生 shadow。

let f2 = 77

func f2() { // Error, function name is the same as the variable f2
    print("${f2}")
}     

// Variable, function, type names cannot be the same as keywords 
let Int64 = 77   // Error: 'Int64' is a keyword

func class() {  // Error: class is a keyword
    print("${f2}")
}    

main(): Int64 {  
    print("${f2}")   // Print 77

    let f2 = { => // Shadowed the global variable f2
       print("10")
    }
    f2()
    return 1
}

作用域

对一个实体,在其名字所在的作用域(scope)中可以直接使用名字访问,不需要通过前缀限定词访问。作用域是可嵌套的,一个作用域包含自身以及自身包含的嵌套的作用域。名字在其嵌套作用域中如果没有被遮盖或覆盖,也可以直接使用名字访问。

在仓颉语言中,由一对匹配的大括号及其中可选的表达式声明序列组成的结构被称之为(block)。块在仓颉语言中无处不在,例如函数定义的函数体、if 表达式的两个分支、while 表达式的循环体,都是块。块会引出新的作用域。

块的语法定义为:

block
    : '{' expressionOrDeclarations '}'
    ;

块拥有值。块的值由其中的表达式与声明序列确定。对块进行求值时,会按表达式和变量声明在块中的顺序进行。

若块的最后一项是表达式,当该表达式求值完毕后,该表达式的值即为该块的值:

{
    let a = 1
    let b = 2
    a + b
} // The value of the block is a + b

若块的最后一项是声明,当该声明处理完毕后,该块的值为 ():

{
    let a = 1
} // The value of the block is ()

若块中不含有任何表达式或声明,该块的值为 ()

{ } // The value of this empty block is ()

作用域级别

如果嵌套的多层作用域中存在相同的名字,内层作用域引入的名字会遮盖外层作用域的名字,我们称内层作用域级别比外层作用域级别更高。

在嵌套的作用域中,作用域级别比较定义如下:

  1. 通过import引入的名字,作用域级别最低;

  2. 包内 top-level 的名字,其作用域级别比 1 中的名字高;

  3. 类型内部、函数定义或表达式中引入的名字,其定义通常被包围在一对花括号{}(即块)中,其作用域级别相较于花括号{}外层的名字要高;

  4. 对于类和接口,子类中的名字比父类中的名字的作用域级别更高,子类中的名字可能会 shadow 或 override 父类中的名字。

import p1.a   // a has the lowest scope level

var x = 1   // x 's scope level is higher than p1.a

open class A {    // A's scope level the same as x at line 3
    var x = 3     // This x has higher scope level than the x at line 3
    var y = 'a'  
    func f(a: Int32, b: Float64): Unit {

    }
}

class B <: A {
    var x = 5   // This x has higher scope level than the x at line 6

    func f(x!: Int32 = 7) { // This x's scope level is higher than the x at line 14

    }
}

作用域原则

根据名字引入的位置分为三种:top-level,local-level,类型内部,这三种类型的 scope 原则各有不同,以下分别进行介绍。

Top-level

Top-level 引入的名字遵守如下作用域原则:

  • Top-level 函数、类型,其作用域为整个 package,名字对整个 package 可见,其中类型包括:classinterfaceenumstructtype引入的名字。
  • Top-level 变量,即由letvarconst 引入的名字,其作用域为从定义(包括赋初值)完成之后开始,不包括从本文件开头到变量声明之间的区间,名字对 package 的其它文件可见。但由于变量的初始化过程可能有副作用,必须先声明且初始化后再使用;
/* Global variables can be accessed only after defined. */
let x = y         //Error: y is used before being defined
let y = 4
let a = b         //Error: b is used before being defined 
let b = a         
let c = c         //Error: unresolved identifier 'c' (in the right of '=')  
//Function names are visible in the entire package
func test() { test2() }   // OK

func f() { test() } // OK

func test2(): Int64 {
    var x = 99
    return x
}

Local-level

在函数定义或表达式内部声明或定义的名字具有 local-level 作用域。定义在块内的变量比块外的有更高的作用域级别。

  • 局部变量,其作用域从声明之后开始到 scope 结束,必须先定义和初始化后使用; 局部变量的遮盖从引入变量名的声明或定义之后开始;
  • 局部函数, 其作用域从声明的位置之后到 scope 结束,支持递归定义,不支持互递归定义;
  • 函数的参数和泛型参数的作用域从参数名声明后开始到函数体结束,其作用域级别与函数体内定义的变量等同。
    • 函数定义 func f(x : Int32, y! : Int32 = x) { } 是合法的;
    • 函数定义 func f(x! : Int32 = x) { } 是不合法的。
  • 泛型类型声明或者扩展泛型类型时引入的泛型参数从参数名声明后开始到类型体或扩展体结束,其作用域级别与类型内定义的名字等同。
    • 泛型类型定义class C<T> {}T的作用域从T出现到整个class C的声明结束;
    • 泛型类型的扩展extend C<T> {}T的作用域从T出现到整个扩展定义结束。
  • lambda 表达式的参数名的作用域与函数的相同,为 lambda 表达式的函数体部分,其作用域级别可视为与 lambda 表达式的函数体内定义的变量等同。
  • main、构造函数的形参名字被视为由函数体块引入,在函数体块中再次引入与形参同名的名字会触发重定义错。
  • if-let 表达式条件中引入的名字被视为由 if 块引入,在 if 块中再次引入相同名字会触发重定义错。
  • match 表达式的 match case 中 pattern 引入的名字,其作用域级别比所在的 match 表达式更高,从引入处开始到该 match case 结束。每个 match case 均有独立的作用域。match case 绑定模式中引入的名字被视作由胖箭头 => 之后的作用域引入,在 => 之后再次引入相同名字会触发重定义错。
  • 对于所有三种循环,循环条件与循环块的作用域级别相同,即其中引入的名字互相不能遮盖。且额外规定循环条件无法引用循环体中定义的变量。则有如下推论:
    • 对于 for-in 表达式,其循环体可以引用循环条件中引入的变量名。
    • 对于 whiledo-while 表达式,它们的循环条件都无法引用其循环体中引入的变量名,即便 do-while 的循环条件在循环体后。
  • 对于 for-in 循环,额外规定其循环条件处引入的变量不能在 in 关键字之后的表达式中使用。
  • 对于 try 异常处理,try 后面紧跟的块以及每个 catch 块的的作用域互相独立。catch pattern 引入的名字被视作由 catch 后紧跟的块引入,在 catch 块中再次引入相同名字会触发重定义错。
  • try-with-resources 表达式,try 关键字和 {} 之间引入的名字,被视作由 try 块引入,在 try 块中再次引入相同名字会触发重定义错。
// a: The scope of a local variable begins after the declaration
let x = 4
func f(): Unit {
    print("${x}") // Print 4

    let x = 99    
    print("${x}") // Print 99
}

let y = 5
func g(): Unit {
    let y = y     // 'y' in the right of '=' is the global variable 'y'
    print("${y}") // Print 5 

    let z = z     // Error: unresolved identifier 'z' (in the right of '=')
}
// b: The scope of a local function begins after definition
func test1(): Unit {
    func test2(): Unit {
        print("test2")
        test3()  // Error,test3's scope is begin after definition
    }
    func test3(): Unit {
        test2()
    }

    test2()    
}
let score: Int64 = 90
let good = 70
var scoreResult: String = match (score) { // binding pattern
    case 60 => "pass" // constant pattern.
    case 100 => "Full" // constant pattern.
    case good => "good" // This good has higher scope level than the good at line 2
}

类型内部引入

  • class/interface/struct/enum的成员的scope为整个class/interface/struct/enum定义内部;
  • enum 定义中的constructor的作用域为整个enum定义内部,关于enumconstructor名字访问的具体规则,见[enum 类型]。

遮盖

通常来说,若两个相互重叠的拥有不同级别的作用域中引入了相同的名字,则会发生遮盖(shadowing):其中作用域级别高的名字会遮盖作用域级别低的名字。这导致作用域级别低的名字要么需要加上前缀限定词访问,要么无法访问。当作用域级别高的名字的作用域结束时,遮盖消除。具体说来,作用域级别不同时:

  • 若作用域级别高的名字 C 为一个类型,则直接发生遮盖。

    // == in package a ==
    public class C {} // ver 1
    
    // == in package b ==
    import a.*
    
    class C {} // ver 2
    
    let v = C() // will use ver 2
    
  • 若作用域级别高的名字 x 为一个变量,则直接发生遮盖。对于成员变量的遮盖规则,请参见 “类和接口” 章节。

    let x = 1
    
    func foo() {
        let x = 2
        println(x) // will print 2
    }
    
  • 若作用域级别高的名字 p 为package名字,则直接发生遮盖。

    // == in package a ==
    public class b {
        public static let c = 1
    }
    
    // == in package a.b ==
    public let c = 2
    
    // == in package test ==
    import a.*
    import a.b.*
    
    let v = a.b.c // will be 1
    
  • 若作用域级别高的名字 f 为一个成员函数,则按重载的规则判断 f 是否发生重载,如无重载,则可能发生覆盖或重定义;如无重载且不能覆盖/重定义,则报错。具体规则,见[覆盖]、[重定义]。

    open class A {
        public open func foo() {
            println(1)
        }
    }
    
    class B <: A {
        public override func foo() {  // override
            println(2)
        }
    
        public func foo() {  // error, conflicting definitions
            println(3)
        }
    }
    
  • 若作用域级别高的名字 f 为非成员函数,则按重载的规则判断 f 是否发生重载。如无重载,则视为遮盖。

    func foo() { 1 }
    
    func test() {
        func foo() { 2 } // shadows
        func foo(x: Int64) { 3 } // overloads
    
        println(foo()) // will print 2
    }
    

下面的例子展示了函数内变量的作用域级别和之间的遮盖关系,以及项与类型在同一命名空间。

func g(): Unit {
  	f(1, 2)  // OK. f is a top-level definition, which is visible in the whole file 
}

func f(x: Int32, y: Int32): Unit {  
    // Error. Term Maze shadows the type Maze, the Maze cannot be used as a type
    // let Maze : Maze 
    // var x = 1    // Error,x was introduced by parameter
    var i = 1       // OK

    for (i in 1..3) { // OK, a new i in the block
        let v = 1   
    }              // i and v disappear

    print("${i}")     // OK. i is defined at line 9
    // print("${v}")  // Error,v disappeared and cannot be found
}

enum Maze {
    C1 | C2
    // | C1 // Error. The C1 has been used
}

下面的例子展示了不同包之间定义的变量和类的作用域级别关系和之间遮盖关系。

// File a.cj
package p1

class A {}
var v: Int32 = 10

// File b.cj
package p2
import p1.A        // A belongs to the package p1
import p1.v        // v belongs to the package p1

// p2 has defined its own v, whose scope level is higher than the v from package p1
var v: Int32 = 20     

func displayV1(): Unit {
    // According to the scope level, p2.v shadows p1.v, therefore access p2.v here 
    print("${v}")          // output: 20
}

var a = A()         // Invoke A in p1

下面的例子展示了继承中的遮盖关系。

var x = 1   

open class A {
  	var x = 3   // this x shadows the top level x
}

class B <: A {
    var x = 5   // error: a member of the subtype must not shadow a member of the supertype.

    func f(x!: Int32 = 7): Unit { // this x shadows all the previous x

    }
}

变量

仓颉编程语言作为一种静态类型(statically typed)语言,要求每个变量的类型必须在编译时确定。

根据是否可进行修改,可将变量分为 3 类:不可变变量(一旦初始化,值不可改变)、可变变量(值可以改变)、const 变量(必须编译期初始化,不可修改)。

变量的定义

变量定义的语法定义为:

variableDeclaration
    : variableModifier* ('let' | 'var' | 'const') patternsMaybeIrrefutable (((':' type)? ('=' expression)) | (':' type))
    ;

patternsMaybeIrrefutable
    : wildcardPattern
    | varBindingPattern
    | tuplePattern
    | enumPattern
    ;

变量的定义均包括四个部分:修饰符、let/var/const 关键字、patternsMaybeIrrefutable 和变量类型。其中:

  1. 修饰符

    • top-level 变量的修饰符包括:public, protected, private, internal
    • 局部变量不能用修饰符修饰
    • class 类型的成员变量的修饰符包括:public, protected, private, internal, static
    • struct 类型的成员变量的修饰符包括:public, private, internal, static
  2. 关键字let/var/const

    • let用于定义不可变变量,let 变量一旦初始化就不能再改变。
    • var用于定义可变变量。
    • const用于定义 const 变量。
  3. patternsMaybeIrrefutable

    • let(或 var/const)之后只能是那些一定或可能为 irrefutable 的 pattern(见 [模式的分类])。在语义检查阶段,会检查 pattern 是否真的是 irrefutable,如果不是 irrefutable pattern,则编译报错。
    • let(或 var/const)之后的 pattern 中引入的新的变量,全部都是 let 修饰(或 var 修饰)的变量。
    • 在 class 和 struct 中定义成员变量时,只能使用 binding pattern(见 [绑定模式])。
  4. 变量类型是可选的,不声明变量类型时需要给变量初始值,编译器将尝试根据初始值推断变量类型;

  5. 变量可以定义在 top-level, 表达式内部,class/struct 类型内部。

需要注意的是:

(1)pattern 和变量类型之间需要使用冒号(:)分隔,pattern 的类型需要和冒号后的类型相匹配。

(2)关键字let/var/const和 pattern 是必选的;

(3)局部变量除了使用上述语法定义之外,还有如下几种情形会引入局部变量:

  • for-in 循环表达式中,forin中间的 pattern,详见 [for-in 表达式]
  • 函数、lambda 定义中的形参,详见 [参数]
  • try-with-resource 表达式中 ResourceSpecifications,详见 [异常]
  • match 表达式中,case 后的 pattern,详见 [模式匹配表达式]

(4)可以使用一对反引号(`)将关键字变为合法的标识符(例如,`open``throw`等)。

下面给出变量定义的一些实例:

let b: Int32                // Define read-only variable b with type Int32.
let c: Int64                // Define read-only variable c with type Int64.
var bb: String              // Define writeable variable bb with type String.
var (x, y): (Int8, Int16)   // Define two writeable variable: x with type Int8, x with type Int16.
var `open` = 1              // Define a variable named `open` with value 1.
var `throw` = "throw"       // Define a variable named `throw` with value "throw".
const d: Int64 = 0		// Define a const variable named d with value 0.

变量的初始化

不可变变量和可变变量

不可变变量和可变变量的初始化均有两种方式:定义时初始化和先定义后初始化。需要注意的是,每个变量在使用前必须进行初始化,否则会报编译错误。关于变量的初始化,举例如下:

func f() {
    let a = 1               // Define and initialize immutable variable a.
    let b: Int32            // Define immutable variable b without initialization.
    b = 10                  // Initialize variable b.
    var aa: Float32 = 3.14  // Define and initialize mutable variable aa.
}

使用 let 定义的不可变变量,只能被赋值一次(即初始化),如果被多次赋值,会报编译错误。使用 var 定义的可变变量,支持多次赋值。

func f() {
    let a = 1             // Define and initialize immutable variable a.
    a = 2                 // error: immutable variable a cannot be reassigned.
    var b: Float32 = 3.14 // Define and initialize mutable b.
    b = 3.1415            // ok: mutable variable b can be reassigned.
}

class C {
    let m1: Int64
    init(a: Int64, b: Int64) {
        m1 = a
        if (b > 0) {
            m1 = a * b  // OK: immutable variable can be reassigned in constructor.
        }
    }
}

全局变量及静态变量初始化

全局变量指定义在 top-level 的变量,静态变量包含定义在 classstruct 中的静态变量。

全局变量和静态变量的初始化必须满足以下规则:

  • 全局变量在声明时必须立即对其进行初始化,否则报错。即,声明必须提供一个初始化表达式。

  • 静态变量在声明时必须立即对其进行初始化,可以采用与全局变量的初始化相同的形式,也可以在静态初始化器中进行初始化(更多细节见[静态初始化器]部分)。

    • 注意,静态变量不能在其他静态变量中初始化:
    class Foo {
        static let x: Int64
        static let y = (x = 1) // it's forbidden
    }
    
  • 初始化表达式 e 不能依赖未初始化的全局变量或静态变量。编译器会进行保守的分析,如果 e 可能会访问到未初始化的全局变量或静态变量,则报错。详细的分析取决于编译器的实现,在规范中没有指定。

全局/静态变量的初始化时机和初始化顺序规则如下:

  • 所有的全局/静态变量都在 main (程序入口) 之前完成初始化;

  • 对于同一个文件中声明的全局/静态变量,初始化顺序根据变量的声明顺序从上到下进行;如果使用了静态初始化器,则根据静态初始化器的初始化顺序规则执行(详见[静态初始化器]部分)

  • 同一个包里不同文件或不同包中声明的全局/静态变量的初始化顺序取决于文件或包之间的依赖关系。如果文件 B.cj 依赖文件 A.cjA.cj 不依赖 B.cj,则 A.cj 中的全局/静态变量的初始化在 B.cj 中的全局/静态变量的初始化之前;

  • 如果文件或包之间存在循环依赖或者不存在任何依赖,那么它们之间的初始化顺序不确定,由编译器实现决定。

    /* The initialization of the global variable cannot depend on the global  
       variables defined in other files of the same package. */
    // a.cj
    let x = 2
    let y = z       // OK, b.cj does not depend on this file directly or indirectly.
    let a = x       // OK.
    let c = A()
    
    /* c.f is an open function, the compiler cannot statically determine whether the
       function meets the initialization rules of global variables, and an error may 
       be reported. */
    let d = c.f()
    
    open class A {
        // static var x = A.z     // Error, A.z is used before its initialization.
        // static var y = B.f     // Error, B.f is used before its initialization.
        static var z = 1
        public open func f(): Int64 {
            return 77
        }
    }
    
    class B {
        static var e = A.z    // OK.
        static var f = x      // OK.     
    }
    
    // b.cj      
    let z = 10      
    // let y = 10   // Error, y is already defined in a.cj.
    
    // main.cj
    main(): Int64 {
        print("${x}")
        print("${y}")
        print("${z}")
        return 1
    }
    

const 变量

详见 const 章节。

修饰符

仓颉提供了很多修饰符,主要分以下两类:

  • 访问修饰符
  • 非访问修饰符

修饰符通常放在定义处的最前端,用来表示该定义具备某些特性。

访问修饰符

详细内容请参考包和模块管理章节[访问修饰符]。

非访问修饰符

仓颉提供了许多非访问修饰符以支持其它丰富的功能

  • open 表示该实例成员可被子类覆盖,或者该类能被子类继承,详见[类]
  • sealed 表示该 class 或 interface 只能在当前包内被继承或实现,详见[类]
  • override 表示覆盖父类的成员,详见[类]
  • redef 表示重新定义父类的静态成员,详见[类]
  • static 表示该成员是静态成员,静态成员不能通过实例对象访问,详见[类]
  • abstract 表示该 class 是抽象类,详见[类]
  • foreign 表示该成员是外部成员,详见[语言互操作]
  • unsafe 表示与 C 语言进行互操作的上下文,详见[语言互操作]
  • sealed 表示该 class 或 interface 只能在当前包内被继承或实现,详见[类]
  • mut 表示该成员是具有可变语义的,详见[函数]

这些修饰符的具体功能详见对应的章节。