联合类型(sum types)

定义联合类型

V的联合类型在V编译器代码中大量采用

有些使用接口的场景,不一定要全部用接口来实现,使用联合类型的代码看起来更简洁,清晰

相对于go和rust来说,联合类型给V加分不少,用起来很舒服

语法类似typescript,使用type 和 | 来定义一个联合类型

  1. //定义联合类型,表示类型Expr可以是这几种类型的其中一种
  2. type Expr = Foo | BoolExpr | BinExpr | UnaryExpr

使用pub关键字,定义公共的联合类型

  1. pub type Expr = Foo | BoolExpr | BinExpr | UnaryExpr

使用场景

联合类型相对于接口来说,比较适合于一个类型是已知的几种类型的其中一种,已知类型的数量是有限的,固定的,相对封闭的,不需要考虑未知类型的扩展性

比如,x.json2模块中,使用联合类型来包含所有json的节点类型,json节点类型种类是相对固定的,使用了联合类型这种数据结构来表示,后续的代码就变得很简洁清晰

  1. //代码位置:vlib/x/json2/decoder.v
  2. pub type Any = string | int | i64 | f32 | f64 | any_int | any_float | bool | Null | []Any | map[string]Any

还有另一个最大的场景就是V编译器自身,使用了联合类型来包含所有的AST抽象语法树的类型,毕竟AST的节点类型也是有限的,固定的,相对封闭的,后续AST的逻辑代码也变得简洁清晰很多

  1. //代码位置:vlib/v/ast/ast.v
  2. pub type TypeDecl = AliasTypeDecl | FnTypeDecl | SumTypeDecl | UnionSumTypeDecl
  3. pub type Expr = AnonFn | ArrayInit | AsCast | Assoc | AtExpr | BoolLiteral | CTempVar |
  4. CallExpr | CastExpr | ChanInit | CharLiteral | Comment | ComptimeCall | ConcatExpr | EnumVal |
  5. FloatLiteral | Ident | IfExpr | IfGuardExpr | IndexExpr | InfixExpr | IntegerLiteral |
  6. Likely | LockExpr | MapInit | MatchExpr | None | OrExpr | ParExpr | PostfixExpr | PrefixExpr |
  7. RangeExpr | SelectExpr | SelectorExpr | SizeOf | SqlExpr | StringInterLiteral | StringLiteral |
  8. StructInit | Type | TypeOf | UnsafeExpr
  9. pub type Stmt = AssertStmt | AssignStmt | Block | BranchStmt | CompFor | ConstDecl | DeferStmt |
  10. EnumDecl | ExprStmt | FnDecl | ForCStmt | ForInStmt | ForStmt | GlobalDecl | GoStmt |
  11. GotoLabel | GotoStmt | HashStmt | Import | InterfaceDecl | Module | Return | SqlStmt |
  12. StructDecl | TypeDecl

而接口更适合用来支持未知类型的扩展性

使用联合类型

  • 联合类型作为函数的参数或返回值,也可以作为变量声明,结构体字段
  • 使用match语句,进行类型的进一步判断
  • 使用match语句,如果需要修改联合类型的值,需要在变量前加mut
  • 使用is关键字联合类型具体是哪一种类型
  • 使用as关键字将联合类型转换为另一种类型,当然要转换的类型在联合类型定义的类型范围内
  1. module main
  2. struct User {
  3. name string
  4. age int
  5. }
  6. fn (m &User) str() string {
  7. return 'name:$m.name,age:$m.age'
  8. }
  9. // 联合类型声明
  10. type MySum = User | int | string
  11. fn (ms MySum) str() string {
  12. if ms is int { // 使用is关键字,判断联合类型具体是哪种类型
  13. println('ms type is int')
  14. }
  15. match ms { // 对接收到的联合类型,使用match语句进行类型判断,每个match分支的ms变量都会被自动造型为分支中对应的类型
  16. int { return ms.str() }
  17. string { return ms }
  18. User { return ms.str() }
  19. }
  20. }
  21. fn add(ms MySum) { // 联合类型作为参数
  22. match ms { // 可以对接收到的联合类型,使用match语句进行类型判断,每个match分支的ms变量都会被自动造型为分支中对应的类型
  23. int { println('ms is int,value is $ms') }
  24. string { println('ms is string,value is $ms') }
  25. User { println('ms is User,value is $ms.str()') }
  26. }
  27. }
  28. fn sub(i int, s string, u User) MySum { // 联合类型作为返回值
  29. return i
  30. // return s //这个也可以
  31. // return User{name:'tom',age:3} //这个也可以
  32. }
  33. fn main() {
  34. i := 123
  35. s := 'abc'
  36. u := User{
  37. name: 'tom'
  38. age: 33
  39. }
  40. mut res := MySum{} // 声明联合类型变量
  41. res = i
  42. println(res) // 输出123
  43. res = s
  44. println(res) // 输出abc
  45. res = u
  46. println(res) // 输出name:tom,age:33
  47. match res { // 判断具体类型
  48. int { println('res is:$res.str()') }
  49. string { println('res is:$res') }
  50. User { println('res is:$res.str()') }
  51. }
  52. match mut res { // 如果需要在分支中修改res,需要加mut
  53. int {
  54. res = MySum(3)
  55. println('new res value is: $res')
  56. }
  57. string {
  58. res = 'abc'
  59. println('new res value is: $res')
  60. }
  61. User {
  62. res = User{
  63. name: 'jack'
  64. age: 12
  65. }
  66. println('new res value is:$res.str()')
  67. }
  68. }
  69. user := res as User // 也可以通过as,进行显示造型
  70. println(user.name)
  71. add(i)
  72. add(s)
  73. }

获取联合类型的具体类型

联合类型使用内置方法type_name()来返回变量的具体类型

  1. module main
  2. struct Point {
  3. x int
  4. }
  5. type MySumType = Point | f32 | int
  6. fn main() {
  7. // 联合类型使用type_name()
  8. sa := MySumType(32)
  9. sb := MySumType(f32(123.0))
  10. sc := MySumType(Point{
  11. x: 43
  12. })
  13. println(sa.type_name()) // int
  14. println(sb.type_name()) // f32
  15. println(sc.type_name()) // Point
  16. }

联合类型相等判断

  1. type Str = string | ustring
  2. struct Foo {
  3. v int
  4. }
  5. struct Bar {
  6. v int
  7. }
  8. type FooBar = Foo | Bar
  9. fn main() {
  10. s1 := Str('s')
  11. s2 := Str('s')
  12. u1 := Str('s'.ustring())
  13. u2 := Str('s'.ustring())
  14. println( s1 == s1 ) //联合类型判断相等或不等:同类型,同值
  15. println( s1 == s2 )
  16. println( u1 == u1 )
  17. println( u1 == u2 )
  18. //类型不同,值相同也不等
  19. foo := FooBar( Foo{v: 0} )
  20. bar := FooBar( Bar{v: 0} )
  21. println( foo.v == bar.v )
  22. println( foo != bar )
  23. }

for is类型循环判断

用于联合类型的类型循环判断(感觉没啥用,就是一个语法糖而已)

  1. module main
  2. struct Milk {
  3. mut:
  4. name string
  5. }
  6. struct Eggs {
  7. mut:
  8. name string
  9. }
  10. type Food = Eggs | Milk
  11. fn main() {
  12. mut f := Food(Eggs{'test'})
  13. //不带mut
  14. for f is Eggs {
  15. println(typeof(f).name)
  16. break
  17. }
  18. //等价于
  19. for {
  20. if f is Eggs {
  21. println(typeof(f).name)
  22. break
  23. }
  24. }
  25. //带mut
  26. for mut f is Eggs {
  27. f.name = 'eggs'
  28. println(f.name)
  29. break
  30. }
  31. //等价于
  32. for {
  33. if mut f is Eggs {
  34. f.name = 'eggs'
  35. println(f.name)
  36. break
  37. }
  38. }
  39. }

as类型转换

  1. module main
  2. type Mysumtype = bool | f64 | int | string
  3. fn main() {
  4. x := Mysumtype(3)
  5. x2 := x as int
  6. println(x2)
  7. }

联合类型嵌套

联合类型还可以嵌套使用,支持更复杂的场景

  1. struct FnDecl {
  2. pos int
  3. }
  4. struct StructDecl {
  5. pos int
  6. }
  7. struct IfExpr {
  8. pos int
  9. }
  10. struct IntegerLiteral {
  11. val string
  12. }
  13. type Expr = IfExpr | IntegerLiteral
  14. type Stmt = FnDecl | StructDecl
  15. type Node = Expr | Stmt //联合类型嵌套

联合类型方法

可以像结构体那样,给联合类型添加方法

  1. module main
  2. fn main() {
  3. mut m := Mysumtype{}
  4. m = int(11)
  5. println(m.str())
  6. }
  7. type Mysumtype = int | string
  8. pub fn (mysum Mysumtype) str() string { // 联合类型的方法
  9. return 'from mysumtype'
  10. }

访问公共字段

联合类型可以在方法中访问所有类型的公共字段,并且支持联合类型的多级嵌套

截图中是实际的V编译器中的例子:

stmt.pos是所有Stmt联合类型的公共字段,有了这个特性以后,代码就可以简洁到像截图中的第一个方法那样

图像

联合类型作为结构体字段

结构体字段的类型也可以是联合体类型

  1. struct MyStruct {
  2. x int
  3. }
  4. struct MyStruct2 {
  5. y string
  6. }
  7. type MySumType = MyStruct | MyStruct2
  8. struct Abc {
  9. bar MySumType // bar字段的类型也可以是联合类型
  10. }
  11. fn main() {
  12. x := Abc{
  13. bar: MyStruct{123}
  14. }
  15. if x.bar is MyStruct {
  16. println(x.bar)
  17. }
  18. match x.bar { // match匹配也可以使用
  19. MyStruct { println(x.bar.x) } // 也会自动造型为具体类型
  20. else {}
  21. }
  22. }

子类不允许是指针类型

使用联合类型时,有一点需要注意:联合类型的子类中不允许出现指针类型,编译器会检查并报错

  1. struct Abc {
  2. val string
  3. }
  4. struct Xyz {
  5. foo string
  6. }
  7. type Alphabet1 = Abc | string | &Xyz //不允许指针类型