当前位置:   article > 正文

仓颉编程入门 -- 程序结构, 值类型和引用类型变量以及作用域

仓颉编程

程序结构

编写仓颉程序时,通常会使用.cj扩展名的文本文件作为源代码的载体,这些文件也被称为源文件。在程序开发的最终阶段,源文件中的源代码将被编译成特定格式的二进制文件,以便执行。

在仓颉程序的顶层作用域内,开发者可以自由地定义变量、函数以及自定义类型(比如结构体struct、类class、枚举enum和接口interface等)。这些在顶层作用域中定义的变量和函数分别被特别称为全局变量和全局函数。

若要将仓颉程序编译成可执行文件,必须确保在顶层作用域中存在一个main函数作为程序的入口点。这个main函数的定义无需使用func等特定的修饰符。main函数可以接受一个Array类型的参数(用于接收程序启动时的命令行参数),也可以不接受任何参数。至于返回值,main函数可以返回整数类型以表示程序执行的状态码,或者返回Unit类型(在某些语言中称为void),表示不返回任何值。

特别地,当需要获取程序启动时传递的命令行参数时,可以通过在main函数声明中指定一个Array类型的参数来实现。

main(): Int64 {

    return 0
}
  • 1
  • 2
  • 3
  • 4

除此之外我们还可以在main函数之上 , 也就是顶级作用于定义相关变量

let a = 2023
func b() {}
struct C {}
class D {}
enum E { F | G }

main() {
    println(a)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在非顶层作用域中,确实不能直接定义如struct、class、enum和interface等自定义类型,因为这些类型的定义通常需要在全局或模块级别进行,以确保它们在整个程序或模块中的可见性和可用性。然而,在非顶层作用域(如函数内部、类的方法内部等)中,可以定义变量和函数,这些分别被称为局部变量和局部函数。局部变量和局部函数的作用域被限制在它们被声明的那个作用域块内。

对于定义在自定义类型(如类)中的变量和函数,它们具有特殊的地位,分别被称为成员变量(也称为属性或字段)和成员函数(也称为方法)。成员变量用于存储与对象状态相关的信息,而成员函数则定义了对象可以执行的操作。

在仓颉(或类似支持函数式编程和面向对象编程的语言)中,方法的嵌套是可能的,即一个方法内部可以定义另一个方法。但需要注意的是,这种嵌套方法的可见性和作用域通常受到外部方法的限制,且其使用场景可能相对有限。

此外,一个.cj文件中可以定义多个类、接口、结构体等自定义类型,以及全局变量、全局函数等。这种组织方式有助于将相关的代码和数据封装在一起,提高代码的可读性和可维护性。同时,通过合理的命名和文件组织,可以进一步促进模块化编程,使得大型项目的管理变得更加容易。

// example.cj
func a() {
    let b = 2023
    func c() {
        println(b)
    }
    c()
}

class A {
    let b = 2024
    public func c() {
        println(b)
    }
}

main() {
    a()
    A().c()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

值类型和引用类型变量

在使用仓颉编程语言(或大多数现代编程语言)时,确实需要理解值类型(Value Types)和引用类型(Reference Types)之间的关键差异,这些差异对程序的行为和性能有着重要影响。以下是您提到的几点差异的详细解释:

赋值行为的不同:
值类型:当给一个值类型变量赋值时,会创建一个该值的一个副本,并将这个副本的引用(或更准确地说,是直接的值)赋给新的变量。原始变量的值保持不变,除非它被显式地修改。这意味着如果修改了值类型变量的一个副本,原始变量不会受到影响。
引用类型:与值类型不同,引用类型变量存储的是对对象(如类的实例、数组等)的引用(即内存地址)。当给一个引用类型变量赋值时,实际上是改变了这个变量所引用的对象,而不是对象本身的内容。因此,如果两个引用类型变量引用了同一个对象,并通过其中一个变量修改了对象的状态,那么通过另一个变量访问这个对象时也会看到这些修改。
let 关键字的行为:
在仓颉编程语言(以及许多其他编程语言)中,let 关键字用于声明一个常量,即一旦初始化后就不能再被赋予新的值。然而,这个“不能再被赋予新的值”的限制对于引用类型来说,是指不能改变引用关系(即不能将常量重新指向另一个对象),但并不限制通过该常量引用的对象内部的修改。换句话说,如果有一个由 let 声明的引用类型变量,虽然不能将该变量重新指向另一个对象,但仍然可以通过该变量修改它所引用的对象的属性或字段。
类型分类:
在仓颉编程语言中,基础数据类型(如整数、浮点数、布尔值等)和结构体(struct)通常被视为值类型。这是因为它们在赋值时会创建数据的副本,并且它们的值直接存储在变量本身中。
类(class)和数组(Array)等类型则属于引用类型。它们存储的是对存储在堆上的对象的引用,而不是对象本身。这允许多个引用类型变量引用同一个对象,并在它们之间共享数据。
理解这些差异对于编写高效、可预测和易于维护的代码至关重要。

struct Copy {
    var data = 2012
}

class Share {
    var data = 2012
}

main() {
    let c1 = Copy()
    var c2 = c1
    c2.data = 2023
    println("${c1.data}, ${c2.data}")

    let s1 = Share()
    let s2 = s1
    s2.data = 2023
    println("${s1.data}, ${s2.data}")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

对于值类型的 Copy 类型变量,在赋值时总是获取 Copy 实例的拷贝,如 c2 = c1,随后对 c2 成员的修改并不影响 c1。对于引用类型的 Share 类型变量,在赋值时将建立变量和实例之间的引用关系,如 s2 = s1,随后对 s2 成员的修改会影响 s1。

如果将以上程序中的 var c2 = c1 改成 let c2 = c1,则编译会报错

作用域

在探讨仓颉程序元素的命名时,我们认识到不仅变量需要命名,函数、自定义类型等同样需要,以便在程序中通过名字来引用它们。然而,随着程序规模的扩大,命名问题变得愈发复杂:

简短的名字在大型程序中容易导致命名冲突,即不同元素使用了相同的名字。
运行时环境中,某些代码片段可能使得某些程序元素暂时无效,直接引用它们会引发错误。
在特定的逻辑结构中,为了维护元素间的层级关系,通常不直接通过名字访问子元素,而是通过其父元素间接访问。
为解决这些问题,现代编程语言引入了“作用域”的概念,它限定了名字与程序元素之间绑定关系的有效范围。作用域之间可以相互独立、并列,也可以形成嵌套或包含关系。每个作用域都明确规定了在该范围内可访问的程序元素及其名字,具体遵循以下规则:

在当前作用域内定义的程序元素,其名字在当前作用域及其所有内层作用域中均有效,允许直接通过名字访问。
内层作用域中定义的程序元素名字,在外层作用域中是不可见的,即外层作用域无法直接访问内层作用域中的元素。
内层作用域有权重新定义外层作用域中已存在的名字,此时内层作用域中的定义会“遮蔽”外层作用域中的同名定义,表明内层作用域在名字解析上具有更高的优先级。
在仓颉编程语言中,通过一对大括号“{}”可以创建一个新的作用域,这种作用域可以嵌套,即在一个作用域内部再次使用大括号定义新的作用域。这些嵌套的作用域均遵循上述作用域规则。特别地,仓颉源文件中最外层、未被任何大括号包围的代码区域,被称为“顶层作用域”,它是该文件中作用域级别最低的部分。

// test.cj
let element = "仓颉"
main() {
    println(element)
    let element = 9
    if (element > 0) {
        let element = 2023
        println(element)
    }
    println(element)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/空白诗007/article/detail/1009841
推荐阅读
相关标签
  

闽ICP备14008679号