【From C To Go】1.6 常量与枚举


C语言中,可以使用 宏定义或者 const关键字两种方式定义常量,但这两种方式都有一定的缺陷(至少在我使用的C语言版本),Go语言的常量系统有了新的设计,本文主要介绍Go语言的常量和枚举(iota):

1. 常量

1.1 C语言中的常量

如前所述,C语言使用宏定义和const关键字来定义常量

1.1.1 宏定义

宏定义使用#define预处理指令来创建常量。宏定义在预处理阶段进行简单的文本替换,没有类型检查和作用域的限制。

定义宏常量:
#define PI 3.14
#define MAX_SIZE 100
使用宏常量:
#include <stdio.h>

#define PI 3.14
#define MAX_SIZE 100

int main() {
    printf("Value of PI: %f\n", PI);
    int arr[MAX_SIZE];
    printf("Max size of array: %d\n", MAX_SIZE);
    return 0;
}

在这里插入图片描述

优缺点:
  • 优点:

    • 简单直接,预处理器替换时效率高。
    • 可以用来定义复杂的表达式和参数化宏。
  • 缺点:

    • 没有类型检查,容易出错。
    • 没有作用域的概念,容易引起命名冲突。
    • 调试困难,因为宏在预处理阶段被替换,不会在编译器产生的错误信息中明确显示。

1.1.2 const

const关键字用来定义只读变量,这些变量有类型并且受到编译器的类型检查。const常量具有作用域,与普通变量一样。

定义const变量
const double pi = 3.14;
const int max_size = 100;
使用const变量
#include <stdio.h>

int main() {
    const double pi = 3.14;
    const int max_size = 100;
    
    printf("Value of PI: %f\n", pi);
    int arr[max_size];
    printf("Max size of array: %d\n", max_size);
    return 0;
}

在这里插入图片描述

优缺点
  • 优点:

    • 有类型检查,编译器会确保类型安全。
    • 具有作用域,减少命名冲突的风险。
    • 更容易调试,错误信息更明确。
  • 缺点:

    • 不能定义复杂的替换逻辑(这可以被认为是优点,因避免了宏带来的复杂性)。
    • const定义的变量只能叫做只读变量,并非严格意义上的常量,因为可以使用指针来对const修饰的数据进行修改,如下:
    #include <stdio.h>
    
    int main() {
    const double pi = 3.14;
    printf("Value of PI: %f\n", pi);
    
    double *p = &pi;
    *p        = 3.14159;
    printf("Value of PI: %f\n", pi);
    
    return 0;
    }
    

    在这里插入图片描述
    虽然编译器给出了warning,但pi的值确实被修改了

1.2 Go语言中的常量

Go语言在设计上避免了C语言常量存在的问题:

  • 不允许指针修改常量:Go语言的常量在编译时确定,并且在整个程序运行过程中不能被修改。
  • 类型安全和清晰的作用域:Go语言取消了宏定义,使用类型安全的常量,确保了代码的可读性和安全性。
    在Go语言中,常量通过const关键字来定义。Go语言的常量在编译时确定,其值在运行时不能改变。常量在Go语言中有几个重要的特性和使用规则,包括隐式类型转换等。下面是详细的介绍:

1.2.1 常量定义

基本语法:

const identifier [type] = value
  • identifier 是常量的名称。
  • [type] 是可选的类型说明符。
  • value 是常量的值,必须是一个能够在编译时确定的表达式。

示例:

const Pi = 3.14
const Greeting = "Hello, World"

1.2.2 常量的类型

Go语言中的常量可以是以下基本类型之一:

  • 布尔型:truefalse
  • 数字型:整数、浮点数、复数
  • 字符串型

示例:

const (
    Truth    = true
    BigValue = 12345678901234567890
    Small    = 0.12345
    Name     = "Go Programming"
)

1.2.3 常量组

可以使用括号将多个常量组织在一个组中:

const (
    Pi      = 3.14
    e       = 2.71
    Version = "1.0.0"
)

1.2.4 常量表达式

常量表达式的值在编译时计算。常量表达式可以使用常量和操作符。

示例:

const (
    x = 10
    y = x + 5  // 常量表达式
    z = y / 2
)

const (
    a = 3.14 * 2  // 浮点数常量表达式
    b = "Hello, " + "Go!"  // 字符串常量表达式
)

Go语言的常量设计使代码更加安全、简洁和易于维护,同时避免了宏定义的复杂性和潜在问题。

2. 枚举

2.1 C语言enum

在C语言中,enum(枚举)是一种用户定义的类型,用于为一组相关的整数常量指定符号名称。枚举使代码更具可读性和维护性,因为它们允许程序员使用有意义的名称而不是纯粹的数字。

2.1.1 定义和使用enum

基本语法:

enum enum_name {
    identifier1,
    identifier2,
    identifier3,
    ...
};
  • enum_name 是枚举类型的名称(可以省略)。
  • identifier1, identifier2, identifier3, ... 是枚举常量的名称。

示例:

#include <stdio.h>

enum Day {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
};

int main() {
    enum Day today = Wednesday;
    printf("Day: %d\n", today); // 输出: Day: 3
    return 0;
}

在这里插入图片描述

2.1.2 默认值和显式赋值

默认情况下,枚举中的第一个标识符的值为0,后续标识符的值依次递增。但可以显式地为枚举常量赋值。

示例:

#include <stdio.h>

enum Color {
    Red,    // 默认值为0
    Green,  // 默认值为1
    Blue    // 默认值为2
};

enum Direction {
    North = 1,
    East,   // 值为2
    South,  // 值为3
    West    // 值为4
};

int main() {
    enum Color favoriteColor = Green;
    printf("Favorite Color: %d\n", favoriteColor); // 输出: Favorite Color: 1

    enum Direction travelDirection = South;
    printf("Travel Direction: %d\n", travelDirection); // 输出: Travel Direction: 3

    return 0;
}

在这里插入图片描述

2.1.3 枚举的使用

枚举常量可以用于声明枚举类型的变量,并赋值和比较这些变量。

示例:

#include <stdio.h>

enum Status {
    OK = 0,
    WARNING = 1,
    ERROR = 2
};

void checkStatus(enum Status status) {
    if (status == OK) {
        printf("Status is OK.\n");
    } else if (status == WARNING) {
        printf("Status is WARNING.\n");
    } else if (status == ERROR) {
        printf("Status is ERROR.\n");
    } else {
        printf("Unknown Status.\n");
    }
}

int main() {
    enum Status currentStatus = WARNING;
    checkStatus(currentStatus); // 输出: Status is WARNING.
    return 0;
}

在这里插入图片描述

2.1.4 匿名枚举

在不需要枚举类型名称的情况下,可以定义匿名枚举。匿名枚举常用于定义一组相关的常量。

示例:

#include <stdio.h>

enum {
    LOW,
    MEDIUM,
    HIGH
};

int main() {
    int level = MEDIUM;
    printf("Level: %d\n", level); // 输出: Level: 1
    return 0;
}

在这里插入图片描述

2.1.5 位域和枚举

枚举类型常与位域一起使用,以创建高效的存储结构。

示例:

#include <stdio.h>

enum Level {
    LOW = 1,
    MEDIUM = 2,
    HIGH = 4
};

struct Status {
    unsigned int level : 3;
};

int main() {
    struct Status s;
    s.level = MEDIUM;
    printf("Status Level: %d\n", s.level); // 输出: Status Level: 2
    return 0;
}

在这里插入图片描述

2.1.6 总结

  • enum 用于定义一组相关的整数常量,以提高代码的可读性和维护性。
  • 默认情况下,枚举常量从0开始递增,但可以显式赋值。
  • 枚举常量可以用于声明变量并用于比较和赋值。
  • 可以定义匿名枚举来简单定义一组常量。
  • 枚举常常与位域结合使用,创建高效的存储结构。

通过使用枚举,代码变得更加易读和易于维护,有助于避免使用魔法数字(magic numbers)。

2.2 Go语言iota

在Go语言中,iota是一个常量生成器,用于创建一组相关常量时自动递增。它在每个const声明块中从零开始,逐行递增。iota非常适合用于定义枚举类型的常量,使代码更加简洁和易于维护。

2.2.1 基本用法

iota从0开始,并且每使用一次它的值增加1。下面是一个基本示例:

package main

import "fmt"

const (
    A = iota // 0
    B        // 1
    C        // 2
)

func main() {
    fmt.Println(A) // 输出: 0
    fmt.Println(B) // 输出: 1
    fmt.Println(C) // 输出: 2
}

在这里插入图片描述

在上面的代码中,每一行中使用iota,它的值都会递增。

2.2.2 跳过某些值

你可以使用空白标识符_来跳过某些值:

package main

import "fmt"

const (
    X = iota // 0
    _        // 1 (跳过)
    Y        // 2
    Z        // 3
)

func main() {
    fmt.Println(X) // 输出: 0
    fmt.Println(Y) // 输出: 2
    fmt.Println(Z) // 输出: 3
}

在这里插入图片描述

2.2.3 表示位掩码

iota常用于位掩码的定义,通过左移操作符来实现:

package main

import "fmt"

const (
    Flag1 = 1 << iota // 1 << 0 which is 1
    Flag2             // 1 << 1 which is 2
    Flag3             // 1 << 2 which is 4
    Flag4             // 1 << 3 which is 8
)

func main() {
    fmt.Printf("%b\n", Flag1) // 输出: 1
    fmt.Printf("%b\n", Flag2) // 输出: 10
    fmt.Printf("%b\n", Flag3) // 输出: 100
    fmt.Printf("%b\n", Flag4) // 输出: 1000
}

在这里插入图片描述

2.2.4 复杂的常量表达式

iota可以参与更复杂的常量表达式:

package main

import "fmt"

const (
    a = iota       // 0
    b = iota * 10  // 10
    c = iota * 10  // 20
    d = 100 + iota // 103
)

func main() {
    fmt.Println(a) // 输出: 0
    fmt.Println(b) // 输出: 10
    fmt.Println(c) // 输出: 20
    fmt.Println(d) // 输出: 103
}

在这里插入图片描述

2.2.5 重置iota

每遇到一个新的const关键字,iota都会被重置为0:

package main

import "fmt"

const (
    a = iota // 0
    b = iota // 1
)

const (
    c = iota // 0
    d = iota // 1
)

func main() {
    fmt.Println(a) // 输出: 0
    fmt.Println(b) // 输出: 1
    fmt.Println(c) // 输出: 0
    fmt.Println(d) // 输出: 1
}

在这里插入图片描述

2.2.6 定义枚举类型

iota常用于定义枚举类型,使得枚举值更具可读性和维护性:

package main

import "fmt"

type Direction int

const (
    North Direction = iota
    East
    South
    West
)

func main() {
    fmt.Println(North) // 输出: 0
    fmt.Println(East)  // 输出: 1
    fmt.Println(South) // 输出: 2
    fmt.Println(West)  // 输出: 3
}

在这里插入图片描述

2.2.7 在同一行使用多个iota

在同一行使用多个iota时,iota的值在每行都会递增:

package main

import "fmt"

const (
    a, b = iota, iota + 10 // a=0, b=10
    c, d                   // c=1, d=11
)

func main() {
    fmt.Println(a, b) // 输出: 0 10
    fmt.Println(c, d) // 输出: 1 11
}

在这里插入图片描述

2.2.8 iota本质

当使用iota关键字时,它会被编译器视作一个递增的常量生成器。它的本质是一个编译时计数器,每次出现在const声明中时都会递增。它的底层实现在编译阶段,而不是在运行时。这使得Go语言的iota更加高效和可预测。

Go语言的编译器在编译时会遍历const声明,并在每次遇到iota时自动递增。这意味着在生成的代码中,实际上并没有iota的存在。相反,它们会被直接替换为相应的数字。

以下是一个简化的示例,展示了iota的底层实现过程:

// 编译前的代码
const (
    A = iota // 0
    B        // 1
    C        // 2
)

// 编译后的代码
const (
    A = 0
    B = 1
    C = 2
)

// 编译前的代码
const (
    D = iota // 0
    E        // 1
    F        // 2
)

// 编译后的代码
const (
    D = 0
    E = 1
    F = 2
)

在编译过程中,iota会根据其所在的const声明块中的位置被替换为相应的值。这确保了在运行时,常量的值已经确定,而不需要对iota进行任何运行时计算。

2.2.9 总结

  • iota是Go语言中的一个常量生成器,用于创建递增的常量值。
  • 每遇到一个新的const声明,iota会重置为0。
  • iota非常适合用于定义枚举类型和位掩码。
  • 可以与空白标识符结合使用来跳过某些值。
  • 可以参与复杂的常量表达式,进一步增强了其灵活性。

通过合理使用iota,可以使Go语言代码更加简洁和易于维护。

3. Go语言的常量类型转换

在Go语言中,常量有一个独特的特性,即它们在未指定具体类型时是“无类型”的。这种设计允许常量在使用时根据上下文进行隐式类型转换,从而提高了代码的灵活性和可读性。

3.1 无类型常量

无类型常量在声明时没有显式指定类型,而是根据使用环境自动推断类型。无类型常量包括:

  • 无类型布尔常量:例如 true, false
  • 无类型整数常量:例如 123, 0x1F
  • 无类型浮点常量:例如 1.23, 3.14e-10
  • 无类型复数常量:例如 1i, 2+3i
  • 无类型字符串常量:例如 "hello"

示例:

const Pi = 3.14    // 无类型浮点常量
const Truth = true // 无类型布尔常量

隐式类型转换

当无类型常量被赋值或传递给一个需要特定类型的变量时,Go编译器会自动将常量转换为该类型。

示例:

package main

import "fmt"

const Pi = 3.14

func main() {
    var radius float64 = 5
    var diameter = 2 * Pi * radius // Pi 被隐式转换为 float64
    fmt.Println(diameter) // 输出: 31.400000000000002
}

在这里插入图片描述

在上述代码中,Pi 是一个无类型浮点常量。在计算 diameter 时,Pi 被隐式转换为 float64 类型

3.2 常量的类型推断规则

  1. 赋值给变量: 无类型常量会根据变量的类型进行隐式转换。

    const Pi = 3.14
    var f float64 = Pi // Pi 被隐式转换为 float64
    var i int = Pi     // 编译错误:不能将 float64 类型的常量隐式转换为 int 类型
    
  2. 作为函数参数: 无类型常量会根据参数的类型进行隐式转换。

    func printFloat64(f float64) {
        fmt.Println(f)
    }
    
    const Pi = 3.14
    
    func main() {
        printFloat64(Pi) // Pi 被隐式转换为 float64
    }
    
  3. 算术运算: 在算术运算中,无类型常量会根据参与运算的其他操作数的类型进行隐式转换。

    const Pi = 3.14
    var radius float64 = 5
    var diameter = 2 * Pi * radius // Pi 被隐式转换为 float64
    
  4. 显式转换: 如果需要将无类型常量转换为特定类型,可以使用显式类型转换。

    const Pi = 3.14
    var i int = int(Pi) // 显式转换
    

3.3 无类型常量的好处

  1. 灵活性: 无类型常量可以在不同的上下文中使用,而不需要显式声明其类型。
  2. 代码简洁: 避免了频繁的显式类型转换,使代码更简洁易读。
  3. 类型安全: Go语言的编译器确保类型安全,当隐式转换不安全或不可行时会报错。

3.4 总结

无类型常量和隐式类型转换是Go语言的一大特色,主要特点包括:

  • 无类型常量:声明时没有显式指定类型,根据使用上下文自动推断类型。
  • 隐式类型转换:根据变量类型、函数参数类型或运算操作数类型进行自动转换。
  • 显式转换:在需要时可以显式地将无类型常量转换为特定类型。

通过这些特性,Go语言常量的使用更加灵活,同时保持了类型安全和代码的简洁性。

  • 39
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值