Go语言控制结构优化代码可读性与性能
Go语言控制结构基础概述
在Go语言中,控制结构是构建程序逻辑的关键组件。它们决定了程序执行的流程,包括顺序执行、条件执行以及循环执行。Go语言提供了几种基本的控制结构,如if - else
语句、switch - case
语句和for
循环。理解这些基础结构是优化代码可读性与性能的第一步。
if - else语句
if - else
语句是最常见的条件控制结构。它根据条件表达式的真假来决定执行哪一部分代码块。其基本语法如下:
if condition {
// 如果条件为真执行这里的代码
} else {
// 如果条件为假执行这里的代码
}
例如,判断一个数是否为正数:
package main
import "fmt"
func main() {
num := 10
if num > 0 {
fmt.Println(num, "是正数")
} else {
fmt.Println(num, "不是正数")
}
}
在这个例子中,num > 0
是条件表达式。如果表达式为真,就会打印“是正数”;否则,打印“不是正数”。
if
语句还支持在条件判断前进行简短声明,这种特性非常实用,例如:
package main
import "fmt"
func main() {
if num := 10; num > 0 {
fmt.Println(num, "是正数")
} else {
fmt.Println(num, "不是正数")
}
}
这里num
的作用域只在if - else
块内部,这有助于保持代码的局部性,增强代码的可读性,并且在一定程度上避免变量命名冲突。
switch - case语句
switch - case
语句是一种多路分支选择结构,它可以根据一个表达式的值来选择执行不同的代码块。switch
语句的语法如下:
switch expression {
case value1:
// 如果expression等于value1执行这里
case value2:
// 如果expression等于value2执行这里
default:
// 如果expression不等于任何一个case的值执行这里
}
例如,根据星期几的数字输出对应的星期名称:
package main
import "fmt"
func main() {
day := 3
switch day {
case 1:
fmt.Println("星期一")
case 2:
fmt.Println("星期二")
case 3:
fmt.Println("星期三")
case 4:
fmt.Println("星期四")
case 5:
fmt.Println("星期五")
case 6:
fmt.Println("星期六")
case 7:
fmt.Println("星期日")
default:
fmt.Println("无效的数字")
}
}
在Go语言中,switch
语句非常灵活,表达式可以省略,此时会默认使用true
,例如:
package main
import "fmt"
func main() {
num := 10
switch {
case num > 0:
fmt.Println(num, "是正数")
case num < 0:
fmt.Println(num, "是负数")
default:
fmt.Println(num, "是零")
}
}
这种形式类似于多个if - else
的组合,但代码结构更加紧凑,可读性更强。
for循环
for
循环是Go语言中唯一的循环结构,它可以实现迭代执行代码块。for
循环有三种基本形式:
- 传统的
for
循环,类似于C语言的for
循环:
for init; condition; post {
// 循环体
}
例如,计算1到10的累加和:
package main
import "fmt"
func main() {
sum := 0
for i := 1; i <= 10; i++ {
sum += i
}
fmt.Println("1到10的累加和为:", sum)
}
这里i := 1
是初始化语句,i <= 10
是条件表达式,i++
是后置语句。
while
风格的for
循环,省略初始化和后置语句:
for condition {
// 循环体
}
例如,计算10以内的偶数和:
package main
import "fmt"
func main() {
sum := 0
num := 2
for num <= 10 {
sum += num
num += 2
}
fmt.Println("10以内的偶数和为:", sum)
}
- 无限循环形式:
for {
// 循环体
if someCondition {
break
}
}
例如,从控制台读取输入,直到输入“exit”:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("请输入内容(输入exit退出): ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if input == "exit" {
break
}
fmt.Println("你输入的是:", input)
}
}
这种形式在需要持续执行某些操作,直到满足特定条件退出时非常有用。
控制结构对代码可读性的影响
代码可读性是衡量代码质量的重要指标之一。良好的控制结构可以使代码逻辑清晰,易于理解和维护。
if - else语句对可读性的影响
- 嵌套
if - else
的问题 嵌套的if - else
语句在逻辑复杂时会使代码可读性急剧下降。例如:
package main
import "fmt"
func main() {
num := 10
if num > 0 {
if num%2 == 0 {
fmt.Println(num, "是正偶数")
} else {
fmt.Println(num, "是正奇数")
}
} else {
if num%2 == 0 {
fmt.Println(num, "是负偶数")
} else {
fmt.Println(num, "是负奇数")
}
}
}
这个例子中,多层嵌套的if - else
使得代码的逻辑结构不清晰,阅读起来很费劲。为了提高可读性,可以采用提前返回或者使用逻辑运算符来合并条件。
- 提前返回优化可读性 通过提前返回,可以减少嵌套层次,使代码逻辑更加清晰。例如:
package main
import "fmt"
func main() {
num := 10
if num <= 0 {
if num%2 == 0 {
fmt.Println(num, "是负偶数")
} else {
fmt.Println(num, "是负奇数")
}
return
}
if num%2 == 0 {
fmt.Println(num, "是正偶数")
} else {
fmt.Println(num, "是正奇数")
}
}
这样,当num
小于等于0时,直接处理并返回,后续代码只需要处理num
大于0的情况,减少了一层嵌套。
- 使用逻辑运算符合并条件 另一种优化方式是使用逻辑运算符合并条件。例如:
package main
import "fmt"
func main() {
num := 10
if num > 0 && num%2 == 0 {
fmt.Println(num, "是正偶数")
} else if num > 0 && num%2 != 0 {
fmt.Println(num, "是正奇数")
} else if num < 0 && num%2 == 0 {
fmt.Println(num, "是负偶数")
} else if num < 0 && num%2 != 0 {
fmt.Println(num, "是负奇数")
} else {
fmt.Println(num, "是零")
}
}
通过逻辑运算符&&
合并条件,使代码结构更加扁平,可读性得到提升。
switch - case语句对可读性的影响
- 清晰的多路分支结构
switch - case
语句在处理多路分支时,天然具有较好的可读性。例如,处理不同类型的图形面积计算:
package main
import (
"fmt"
"math"
)
type Shape struct {
kind string
value float64
}
func calculateArea(shape Shape) float64 {
switch shape.kind {
case "circle":
return math.Pi * shape.value * shape.value
case "square":
return shape.value * shape.value
case "triangle":
return 0.5 * shape.value * shape.value
default:
return 0
}
}
func main() {
circle := Shape{kind: "circle", value: 5}
square := Shape{kind: "square", value: 4}
triangle := Shape{kind: "triangle", value: 6}
fmt.Printf("圆形面积: %.2f\n", calculateArea(circle))
fmt.Printf("正方形面积: %.2f\n", calculateArea(square))
fmt.Printf("三角形面积: %.2f\n", calculateArea(triangle))
}
在这个例子中,switch - case
语句根据shape.kind
的值来选择不同的面积计算方式,代码结构清晰,易于理解。
- 避免复杂表达式
虽然
switch
语句的表达式可以很灵活,但为了保持可读性,应尽量避免使用过于复杂的表达式。例如,不要在switch
表达式中进行大量的计算或者复杂的函数调用。如果确实需要复杂逻辑,可以在switch
外部进行预处理,然后在switch
中使用预处理的结果。
for循环对可读性的影响
- 简洁的迭代逻辑
for
循环的简洁性使得迭代操作的逻辑非常清晰。例如,遍历数组并打印元素:
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5}
for _, num := range numbers {
fmt.Println(num)
}
}
这里使用range
关键字遍历数组,_
表示忽略索引值,只关心数组元素。这种方式简洁明了,很容易理解代码的意图。
- 多层嵌套循环的处理 多层嵌套循环在处理矩阵等二维数据结构时经常用到,但过多的嵌套会降低代码可读性。例如,打印一个九九乘法表:
package main
import "fmt"
func main() {
for i := 1; i <= 9; i++ {
for j := 1; j <= i; j++ {
fmt.Printf("%d×%d=%d\t", j, i, i*j)
}
fmt.Println()
}
}
为了提高多层嵌套循环的可读性,可以将内层循环封装成一个函数,或者添加注释说明每一层循环的作用。例如:
package main
import "fmt"
func printMultiplicationRow(i int) {
for j := 1; j <= i; j++ {
fmt.Printf("%d×%d=%d\t", j, i, i*j)
}
fmt.Println()
}
func main() {
for i := 1; i <= 9; i++ {
printMultiplicationRow(i)
}
}
这样,通过将内层循环封装成函数,使得外层循环的逻辑更加清晰,整体代码的可读性得到提升。
控制结构对性能的影响
除了代码可读性,控制结构对程序性能也有重要影响。不同的控制结构在执行效率上可能存在差异,了解这些差异有助于编写高性能的Go代码。
if - else语句对性能的影响
- 条件判断的复杂度
if - else
语句的性能主要取决于条件判断的复杂度。简单的比较操作(如==
、<
、>
等)通常执行速度很快,因为现代CPU对这些操作有很好的优化。例如:
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
for i := 0; i < 10000000; i++ {
if i%2 == 0 {
// 空操作,仅模拟条件判断
}
}
elapsed := time.Since(start)
fmt.Println("执行时间:", elapsed)
}
在这个例子中,i%2 == 0
是一个简单的条件判断,即使在循环中执行大量次数,也不会对性能造成太大影响。
然而,如果条件判断中包含复杂的函数调用或者大量的计算,性能就会显著下降。例如:
package main
import (
"fmt"
"math"
"time"
)
func complexCalculation(x int) bool {
// 模拟复杂计算
result := 0
for j := 0; j < x; j++ {
result += int(math.Pow(float64(j), 2))
}
return result%2 == 0
}
func main() {
start := time.Now()
for i := 0; i < 10000000; i++ {
if complexCalculation(i) {
// 空操作,仅模拟条件判断
}
}
elapsed := time.Since(start)
fmt.Println("执行时间:", elapsed)
}
这里complexCalculation
函数包含复杂的计算,每次在if
条件中调用该函数会导致性能大幅下降。为了优化性能,可以提前计算好需要的值,或者避免在条件判断中进行不必要的复杂计算。
- 分支预测
现代CPU具有分支预测功能,它会尝试预测
if - else
语句的执行分支,以提高指令执行的效率。如果分支预测准确率高,程序性能会得到提升;反之,如果分支预测失败,会导致流水线清空,性能下降。
例如,对于一个随机数的条件判断:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
start := time.Now()
for i := 0; i < 10000000; i++ {
num := rand.Intn(100)
if num > 50 {
// 空操作,仅模拟条件判断
}
}
elapsed := time.Since(start)
fmt.Println("执行时间:", elapsed)
}
由于num
是随机生成的,CPU很难准确预测if
分支的走向,可能会导致分支预测失败,影响性能。在实际编程中,尽量使条件判断的结果具有一定的可预测性,有助于提高分支预测准确率,提升性能。
switch - case语句对性能的影响
- 实现方式与性能
switch - case
语句在Go语言中的实现方式会影响其性能。当switch
表达式是整数类型或者字符串类型时,编译器会根据具体情况选择不同的实现策略。
对于整数类型的switch
,如果case
的值分布比较密集且范围较小,编译器会生成类似数组索引的跳转表,这种方式在查找匹配的case
时速度非常快。例如:
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
for i := 0; i < 10000000; i++ {
num := i % 10
switch num {
case 0:
// 空操作,仅模拟条件判断
case 1:
// 空操作,仅模拟条件判断
case 2:
// 空操作,仅模拟条件判断
case 3:
// 空操作,仅模拟条件判断
case 4:
// 空操作,仅模拟条件判断
case 5:
// 空操作,仅模拟条件判断
case 6:
// 空操作,仅模拟条件判断
case 7:
// 空操作,仅模拟条件判断
case 8:
// 空操作,仅模拟条件判断
case 9:
// 空操作,仅模拟条件判断
}
}
elapsed := time.Since(start)
fmt.Println("执行时间:", elapsed)
}
在这个例子中,num
的值范围是0到9,分布密集,编译器可以生成高效的跳转表,使得switch
语句执行速度很快。
然而,如果case
的值分布稀疏或者范围很大,编译器可能会采用线性搜索的方式,性能就会相对较差。例如:
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
for i := 0; i < 10000000; i++ {
num := i * 100
switch num {
case 100:
// 空操作,仅模拟条件判断
case 10000:
// 空操作,仅模拟条件判断
case 1000000:
// 空操作,仅模拟条件判断
}
}
elapsed := time.Since(start)
fmt.Println("执行时间:", elapsed)
}
这里case
的值分布稀疏,编译器可能会采用线性搜索,导致性能不如密集分布的情况。
- 字符串类型的switch
对于字符串类型的
switch
,编译器会根据字符串的长度和case
的数量等因素选择不同的实现方式。一般来说,当case
数量较少时,线性搜索可能是一种选择;当case
数量较多时,可能会采用更复杂的哈希表等方式来提高查找效率。
例如,根据不同的命令字符串执行不同操作:
package main
import (
"fmt"
"time"
)
func main() {
commands := []string{"start", "stop", "restart", "status"}
start := time.Now()
for i := 0; i < 10000000; i++ {
command := commands[i%len(commands)]
switch command {
case "start":
// 空操作,仅模拟条件判断
case "stop":
// 空操作,仅模拟条件判断
case "restart":
// 空操作,仅模拟条件判断
case "status":
// 空操作,仅模拟条件判断
}
}
elapsed := time.Since(start)
fmt.Println("执行时间:", elapsed)
}
在这个例子中,字符串switch
的性能取决于具体的实现方式,但总体来说,在合理的case
数量下,性能是可以接受的。
for循环对性能的影响
- 循环变量与性能
for
循环中的循环变量定义和使用方式会影响性能。例如,在循环内部频繁创建对象或者进行复杂的内存分配操作,会导致性能下降。
package main
import (
"fmt"
"time"
)
type Data struct {
value int
}
func main() {
start := time.Now()
for i := 0; i < 10000000; i++ {
data := &Data{value: i}
// 空操作,仅模拟对象创建
_ = data
}
elapsed := time.Since(start)
fmt.Println("执行时间:", elapsed)
}
在这个例子中,每次循环都创建一个新的Data
对象,频繁的内存分配和垃圾回收会消耗大量性能。为了优化,可以在循环外部创建对象,然后在循环内部复用。
package main
import (
"fmt"
"time"
)
type Data struct {
value int
}
func main() {
data := &Data{}
start := time.Now()
for i := 0; i < 10000000; i++ {
data.value = i
// 空操作,仅模拟对象复用
_ = data
}
elapsed := time.Since(start)
fmt.Println("执行时间:", elapsed)
}
这样通过复用对象,减少了内存分配和垃圾回收的次数,提高了性能。
- 循环次数与性能 循环次数也是影响性能的重要因素。如果循环次数非常大,即使每次循环的操作很简单,也可能会消耗大量时间。在这种情况下,可以考虑采用并行计算等方式来提高效率。
例如,计算1到100000000的累加和:
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
sum := 0
for i := 1; i <= 100000000; i++ {
sum += i
}
elapsed := time.Since(start)
fmt.Println("累加和:", sum)
fmt.Println("执行时间:", elapsed)
}
这个单线程的循环计算会花费较长时间。可以使用Go语言的并发特性来优化,例如:
package main
import (
"fmt"
"sync"
"time"
)
func sumRange(start, end int, wg *sync.WaitGroup, resultChan chan int) {
sum := 0
for i := start; i <= end; i++ {
sum += i
}
resultChan <- sum
wg.Done()
}
func main() {
start := time.Now()
numPartitions := 4
partSize := 100000000 / numPartitions
var wg sync.WaitGroup
resultChan := make(chan int, numPartitions)
for i := 0; i < numPartitions; i++ {
start := i*partSize + 1
end := (i + 1) * partSize
if i == numPartitions-1 {
end = 100000000
}
wg.Add(1)
go sumRange(start, end, &wg, resultChan)
}
go func() {
wg.Wait()
close(resultChan)
}()
totalSum := 0
for sum := range resultChan {
totalSum += sum
}
elapsed := time.Since(start)
fmt.Println("累加和:", totalSum)
fmt.Println("执行时间:", elapsed)
}
通过将计算任务分成多个部分并行执行,大大提高了计算效率,减少了执行时间。
控制结构优化的综合策略
为了同时提高代码的可读性和性能,需要采用一些综合策略来优化控制结构。
合理选择控制结构
- 根据逻辑复杂度选择
当逻辑判断比较简单,只有一两个条件时,
if - else
语句是很好的选择。例如,判断一个数是否为偶数:
package main
import "fmt"
func main() {
num := 10
if num%2 == 0 {
fmt.Println(num, "是偶数")
} else {
fmt.Println(num, "是奇数")
}
}
然而,当有多个互斥的条件分支时,switch - case
语句通常能使代码更清晰。例如,根据不同的HTTP状态码返回相应的提示信息:
package main
import (
"fmt"
)
func getHttpStatusMessage(statusCode int) string {
switch statusCode {
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
default:
return "Unknown Status Code"
}
}
func main() {
statusCode := 200
message := getHttpStatusMessage(statusCode)
fmt.Println("状态码", statusCode, "的信息:", message)
}
- 根据性能需求选择
如果对性能要求极高,并且条件判断的结果具有一定的规律性,例如整数类型的
switch
且case
值分布密集,使用switch - case
可能会有更好的性能。而对于复杂的条件逻辑,if - else
语句通过合理的优化(如提前返回、合并条件等)也能达到较好的性能。
优化条件判断
- 简化条件表达式 尽量避免在条件表达式中进行复杂的计算或者函数调用。如果确实需要,提前计算好结果并存储在变量中,然后在条件判断中使用该变量。例如:
package main
import (
"fmt"
"math"
)
func main() {
num := 10
result := int(math.Pow(float64(num), 2))
if result%2 == 0 {
fmt.Println(num, "的平方是偶数")
} else {
fmt.Println(num, "的平方是奇数")
}
}
- 提高分支预测准确率 使条件判断的结果具有一定的可预测性,有助于提高分支预测准确率。例如,对于一个根据用户权限进行操作的功能,可以先对常见的权限进行判断,因为大多数用户可能具有相同的常见权限,这样可以提高分支预测的成功率。
优化循环结构
- 减少循环内部的开销 避免在循环内部进行不必要的内存分配、对象创建和复杂计算。如果可能,将这些操作移到循环外部。例如,在循环中读取文件时,尽量复用缓冲区,而不是每次循环都创建新的缓冲区。
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, _ := os.Open("test.txt")
defer file.Close()
scanner := bufio.NewScanner(file)
buffer := make([]byte, 1024)
scanner.Buffer(buffer, 1024)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}
}
这里通过复用缓冲区buffer
,减少了每次读取文件时的内存分配开销。
- 合理使用并发 对于大规模的循环计算任务,可以考虑使用Go语言的并发特性来提高效率。通过将任务分成多个部分并行执行,可以充分利用多核CPU的优势,减少总体执行时间。但需要注意并发带来的资源竞争和同步问题,合理使用锁或者通道来进行同步。
总结控制结构优化实践案例
案例一:游戏开发中的角色行为控制
在一个简单的2D游戏中,需要根据角色的状态(如站立、行走、跳跃、攻击等)来决定角色的行为。这里可以使用switch - case
语句来实现。
package main
import (
"fmt"
)
type Character struct {
state string
}
func (c *Character) performAction() {
switch c.state {
case "standing":
fmt.Println("角色站立")
case "walking":
fmt.Println("角色行走")
case "jumping":
fmt.Println("角色跳跃")
case "attacking":
fmt.Println("角色攻击")
default:
fmt.Println("未知状态")
}
}
func main() {
character := Character{state: "walking"}
character.performAction()
}
在这个例子中,switch - case
语句使得根据角色状态执行不同行为的逻辑非常清晰,易于理解和维护。同时,由于状态值是固定且有限的,switch - case
的性能也能得到保证。
案例二:数据处理中的条件过滤
假设有一个数据集,需要根据不同的条件进行过滤。例如,根据年龄和性别来筛选人员信息。
package main
import (
"fmt"
)
type Person struct {
name string
age int
gender string
}
func filterPeople(people []Person) []Person {
var result []Person
for _, person := range people {
if person.age >= 18 && (person.gender == "male" || person.gender == "female") {
result = append(result, person)
}
}
return result
}
func main() {
people := []Person{
{"Alice", 20, "female"},
{"Bob", 15, "male"},
{"Charlie", 25, "male"},
{"David", 10, "male"},
{"Eve", 30, "female"},
}
filteredPeople := filterPeople(people)
for _, person := range filteredPeople {
fmt.Printf("姓名: %s, 年龄: %d, 性别: %s\n", person.name, person.age, person.gender)
}
}
在这个案例中,if
语句用于条件过滤。通过合理合并条件,提高了代码的可读性。同时,由于if
条件中的操作都是简单的比较,对性能影响较小。
案例三:数值计算中的循环优化
计算一个大型矩阵的乘法。矩阵乘法是一个典型的需要多层循环的操作,优化循环结构可以显著提高性能。
package main
import (
"fmt"
"time"
)
func multiplyMatrices(matrixA, matrixB [][]int) [][]int {
rowsA := len(matrixA)
colsA := len(matrixA[0])
colsB := len(matrixB[0])
result := make([][]int, rowsA)
for i := range result {
result[i] = make([]int, colsB)
}
for i := 0; i < rowsA; i++ {
for j := 0; j < colsB; j++ {
for k := 0; k < colsA; k++ {
result[i][j] += matrixA[i][k] * matrixB[k][j]
}
}
}
return result
}
func main() {
matrixA := [][]int{
{1, 2},
{3, 4},
}
matrixB := [][]int{
{5, 6},
{7, 8},
}
start := time.Now()
result := multiplyMatrices(matrixA, matrixB)
elapsed := time.Since(start)
for _, row := range result {
fmt.Println(row)
}
fmt.Println("执行时间:", elapsed)
}
在这个例子中,虽然是简单的矩阵乘法,但通过合理安排循环顺序(按照矩阵乘法的数学定义),保证了内存访问的局部性,提高了性能。同时,多层循环的结构清晰,通过注释可以进一步提高可读性。如果矩阵规模非常大,可以考虑使用并行计算来进一步优化性能。
通过这些实际案例可以看出,合理选择和优化控制结构在Go语言编程中对于提高代码可读性和性能至关重要。在实际开发中,应根据具体的需求和场景,灵活运用各种优化策略,编写高质量的Go代码。
结论
在Go语言编程中,控制结构是构建程序逻辑的核心部分。if - else
语句、switch - case
语句和for
循环各自具有独特的语法和应用场景,对代码的可读性和性能有着显著的影响。
从可读性角度来看,避免复杂的嵌套if - else
,合理使用提前返回、逻辑运算符合并条件等技巧,可以使if - else
语句的逻辑更加清晰。switch - case
语句通过其清晰的多路分支结构,在处理多个互斥条件时具有天然的可读性优势,但要注意避免复杂的表达式。for
循环的简洁性使得迭代逻辑一目了然,对于多层嵌套循环,可以通过封装内层循环等方式提高可读性。
在性能方面,if - else
语句的性能主要受条件判断复杂度和分支预测的影响,应尽量简化条件表达式并提高分支预测准确率。switch - case
语句的性能取决于表达式类型和case
值的分布情况,对于整数类型且case
值密集分布的情况性能较好。for
循环的性能优化主要在于减少循环内部的开销,如避免频繁的内存分配和对象创建,以及合理使用并发来处理大规模的循环计算任务。
综合来看,优化控制结构需要根据具体的需求和场景,合理选择控制结构,并运用各种优化策略。通过简化条件判断、提高分支预测准确率、减少循环开销等方式,可以在提高代码可读性的同时,显著提升程序的性能。实际开发中的案例,如游戏角色行为控制、数据处理中的条件过滤和数值计算中的循环优化,进一步证明了控制结构优化的重要性和有效性。
总之,深入理解Go语言控制结构对代码可读性与性能的影响,并在实践中不断优化,是编写高质量、高效Go代码的关键所在。开发者应根据不同的应用场景,灵活运用各种优化技巧,以达到代码可读性与性能的最佳平衡。