仓颉语言 -- 泛型

1、泛型概述

在仓颉编程语言中,泛型指的是参数化类型,参数化类型是一个在声明时未知并且需要在使用时指定的类型类型声明函数声明可以是泛型的。最为常见的例子就是 Array<T>Set<T> 等容器类型。以数组类型为例,当使用数组类型 Array 时,会需要其中存放的是不同的类型,我们不可能定义所有类型的数组,通过在类型声明中声明类型形参,在应用数组时再指定其中的类型,这样就可以减少在代码上的重复。

在仓颉中,classstructenum 的声明都可以声明类型形参,也就是说它们都可以是泛型的

为了方便讨论我们先定义以下几个常用的术语:

  • 类型形参:一个类型或者函数声明可能有一个或者多个需要在使用处被指定的类型,这些类型就被称为类型形参。在声明形参时,需要给定一个标识符,以便在声明体中引用。
  • 类型变元:在声明类型形参后,当我们通过标识符来引用这些类型时,这些标识符被称为类型变元。
  • 类型实参:当我们在使用泛型声明的类型或函数时指定了泛型参数,这些参数被称为类型实参。
  • 类型构造器:一个需要零个、一个或者多个类型作为实参的类型称为类型构造器。

类型形参在声明时一般在类型名称的声明或者函数名称的声明后,使用尖括号 <...> 括起来。例如泛型列表可声明为:

class List<T> {var elem: Option<T> = Nonevar tail: Option<List<T>> = None
}func sumInt(a: List<Int64>) {  }

其中 List<T> 中的 T 被称为类型形参。对于 elem: Option<T> 中对 T 的引用称为类型变元,同理 tail: Option<List<T>> 中的 T 也称为类型变元。函数 sumInt 的参数中 List<Int64> 的 Int64 被称为 List 的类型实参。 List 就是类型构造器,List<Int64> 通过 Int64 类型实参构造出了一个类型 Int64 的列表类型。

2、泛型函数

如果一个函数声明了一个或多个类型形参,则将其称为泛型函数。语法上,类型形参紧跟在函数名后,并用 <> 括起,如果有多个类型形参,则用“,”分离。

2.1 全局泛型函数

在声明全局泛型函数时,只需要在函数名后使用尖括号声明类型形参,然后就可以在函数形参、返回类型及函数体中对这一类型形参进行引用。例如 id 函数定义为:

func id<T>(a: T): T {return a
}

其中 (a: T) 是函数声明的形参,其中使用到了 id 函数声明的类型形参 T,并且在 id 函数的返回类型使用。

再比如另一个复杂的例子,定义如下一个泛型函数 composition,该函数声明了 3 个类型形参,分别是 T1, T2, T3,其功能是把两个函数 f: (T1) -> T2, g: (T2) -> T3 复合成类型为 (T1) -> T3 的函数。

func composition<T1, T2, T3>(f: (T1) -> T2, g: (T2) -> T3): (T1) -> T3 {return {x: T1 => g(f(x))}
}

因为被用来复合的函数可以是任意类型,例如可以是 (Int32) -> Bool, (Bool) -> Int64 的复合,也可以是 (Int64) -> Rune, (Rune) -> Int8 的复合,所以才需要使用泛型函数。

func times2(a: Int64): Int64 {return a * 2
}func plus10(a: Int64): Int64 {return a + 10
}func times2plus10(a: Int64) {return composition<Int64, Int64, Int64>(times2, plus10)(a)
}main() {println(times2plus10(9)) // 28return 0
}

这里,我们复合两个 (Int64) -> Int64 的函数,将 9 先乘以 2,再加 10,结果会是 28。

2.2 局部泛型函数

局部函数也可以是泛型函数。例如泛型函数 id 可以嵌套定义在其它函数中:

func foo(a: Int64) {func id<T>(a: T): T { a }func double(a: Int64): Int64 { a + a }return (id<Int64> ~> double)(a) == (double ~> id<Int64>)(a)
}main() {println(foo(1)) // truereturn 0
}

这里由于 id 的单位元性质,函数 id<Int64> ~> doubledouble ~> id<Int64> 是等价的,结果是 true。

2.3 泛型成员函数

class、struct 与 enum 的成员函数可以是泛型的。例如:

class A {func foo<T>(a: T): Unit where T <: ToString {println("${a}")}
}struct B {func bar<T>(a: T): Unit where T <: ToString {println("${a}")}
}enum C {| X | Yfunc coo<T>(a: T): Unit where T <: ToString {println("${a}")}
}main() {var a = A()var b = B()var c = C.Xa.foo<Int64>(10)b.bar<String>("abc")c.coo<Bool>(false)return 0
}

程序输出的结果为:

10
abc
false

这里需要注意的是,class 中声明的泛型成员函数不能被 open 修饰,如果被 open 修饰则会报错,例如:

class A {public open func foo<T>(a: T): Unit where T <: ToString { // Error, open generic function is not allowedprintln("${a}")}
}

为类型使用 extend 声明进行扩展时,扩展中的函数也可以是泛型的,例如我们可以为 Int64 类型增加一个泛型成员函数:

extend Int64 {func printIntAndArg<T>(a: T) where T <: ToString {println(this)println("${a}")}
}main() {var a: Int64 = 12a.printIntAndArg<String>("twelve")
}

程序输出的结果将为:

12
twelve

2.4 静态泛型函数

interfaceclassstructenumextend 中可以定义静态泛型函数,例如下例 ToPair class 中从 ArrayList 中返回一个元组:

import std.collection.*class ToPair {public static func fromArray<T>(l: ArrayList<T>): (T, T) {return (l[0], l[1])}
}main() {var res: ArrayList<Int64> = ArrayList([1,2,3,4])var a: (Int64, Int64) = ToPair.fromArray<Int64>(res)return 0
}

3、泛型接口

泛型可以用来定义泛型接口,以标准库中定义的 Iterable 为例,它需要返回一个 Iterator 类型,这一类型是一个容器的遍历器Iterator 是一个泛型接口,Iterator 内部有一个从容器类型中返回下一个元素的 next 成员函数,next 成员函数返回的类型是一个需要在使用时指定的类型,所以 Iterator 需要声明泛型参数。

public interface Iterable<E> {func iterator(): Iterator<E>
}public interface Iterator<E> <: Iterable<E> {func next(): Option<E>
}public interface Collection<T> <: Iterable<T> {prop size: Int64func isEmpty(): Bool
}

4、泛型类

泛型接口中介绍了泛型接口的定义和使用,本节我们介绍泛型类的定义和使用。如 Map 的键值对就是使用泛型类来定义的。

可以看一下 Map 类型中的键值对 Node 类型就可以使用泛型类来定义

public open class Node<K, V> where K <: Hashable & Equatable<K> {public var key: Option<K> = Option<K>.Nonepublic var value: Option<V> = Option<V>.Nonepublic init() {}public init(key: K, value: V) {this.key = Option<K>.Some(key)this.value = Option<V>.Some(value)}
}

由于键与值的类型有可能不相同,且可以为任意满足条件的类型,所以 Node 需要两个类型形参 K 与 V ,K <: Hashable, K <: Equatable 是对于键类型的约束,意为 K 要实现 Hashable 与 Equatable 接口,也就是 K 需要满足的条件。对于泛型约束,详见泛型约束章节。

5、泛型结构体

struct 类型的泛型与 class 是类似的,下面我们可以使用 struct 定义一个类似于二元元组的类型:

struct Pair<T, U> {let x: Tlet y: Upublic init(a: T, b: U) {x = ay = b}public func first(): T {return x}public func second(): U {return y}
}main() {var a: Pair<String, Int64> = Pair<String, Int64>("hello", 0)println(a.first())println(a.second())
}

程序输出的结果为:

hello
0

在 Pair 中我们提供了 first 与 second 两个函数来取得元组的第一个与第二个元素。

6、泛型枚举

在仓颉编程语言中,泛型 enum 声明的类型里被使用得最广泛的例子之一就是 Option 类型,关于 Option 详细描述可以详见 Option 类型章节。 Option 类型是用来表示在某一类型上的值可能是个空的值。这样,Option 就可以用来表示在某种类型上计算的失败。这里是何种类型上的失败是不确定的,所以很明显,Option 是一个泛型类型,需要声明类型形参。

package core // `Option` is defined in core.public enum Option<T> {Some(T)| Nonepublic func getOrThrow(): T {match (this) {case Some(v) => vcase None => throw NoneValueException()}}...
}

可以看到,Option<T> 分成两种情况,一种是 Some(T),用来表示一个正常的返回结果,另一种是 None 用来表示一个空的结果。其中的 getOrThrow 函数会是将 Some(T) 内部的值返回出来的函数,返回的结果就是 T 类型,而如果参数是 None,那么直接抛出异常。

例如:如果我们想定义一个安全的除法,因为在除法上的计算是可能失败的。如果除数为 0,那么返回 None ,否则返回一个用 Some 包装过的结果:

func safeDiv(a: Int64, b: Int64): Option<Int64> {var res: Option<Int64> = match (b) {case 0 => Nonecase _ => Some(a/b)}return res
}

这样,在除数为 0 时,程序运行的过程中不会因除以 0 而抛出算术运算异常。

7、泛型类型的子类型关系

实例化后的泛型类型间也有子类型关系。例如当我们写出下列代码时,

interface I<X, Y> { }class C<Z> <: I<Z, Z> { }

根据第 3 行,便知 C<Bool> <: I<Bool, Bool> 以及 C<D> <: I<D, D> 等。这里的第 3 行可以解读为“于所有的(不含类型变元的) Z 类型,都有 C<Z> <: I<Z, Z> 成立”。

但是对于下列代码

open class C { }
class D <: C { }interface I<X> { }

I<D> <: I<C> 是不成立的(即使 D <: C 成立),这是因为在仓颉语言中,用户定义的类型构造器在其类型参数处是不型变

型变的具体定义为:如果 A 和 B 是(实例化后的)类型,T 是类型构造器,设有一个类型参数 X(例如 interface T<X>),那么

  • 如果 T(A) <: T(B) 当且仅当 A = B,则 T 是不型变的。
  • 如果 T(A) <: T(B) 当且仅当 A <: B ,则 T 在 X 处是协变的。
  • 如果 T(A) <: T(B) 当且仅当 B <: A ,则 T 在 X 处是逆变的。

因为现阶段的仓颉中,所有用户自定义的泛型类型在其所有的类型变元处都是不变的,所以给定 interface I<X> 和类型 A、B,只有 A = B,我们才能得到 I<A> <: I<B>;反过来,如果知道了 I<A> <: I<B>,也可推出 A = B内建类型除外:内建的元组类型对其每个元素类型来说,都是协变的;内建的函数类型在其入参类型处是逆变的,在其返回类型处是协变的。)

不型变限制了一些语言的表达能力,但也避免了一些安全问题,例如“协变数组运行时抛异常”的问题(Java 便有这个问题)。

8、类型别名

当某个类型的名字比较复杂或者在特定场景中不够直观时,可以选择使用类型别名的方式为此类型设置一个别名。

type I64 = Int64

类型别名的定义以关键字 type 开头接着是类型的别名(如上例中的 I64),然后是等号 =最后是原类型(即被取别名的类型,如上例中的 Int64)。

只能在源文件顶层定义类型别名,并且原类型必须在别名定义处可见。例如,下例中 Int64 的别名定义在 main 中将报错,LongNameClassB 类型在为其定义别名时不可见,同样报错。

main() {type I64 = Int64 // Error, type aliases can only be defined at the top level of the source file
}class LongNameClassA { }
type B = LongNameClassB // Error, type 'LongNameClassB' is not defined

一个(或多个)类型别名定义中禁止出现(直接或间接的)循环引用

type A = (Int64, A) // Error, 'A' refered itselftype B = (Int64, C) // Error, 'B' and 'C' are circularly refered
type C = (B, Int64)

类型别名并不会定义一个新的类型,它仅仅是为原类型定义了另外一个名字,它有如下几种使用场景:

  1. 作为类型使用,例如:
type A = B
class B {}
var a: A = B() // Use typealias A as type B
  1. 当类型别名实际指向的类型为 class、struct 时,可以作为构造器名称使用
type A = B
class B {}
func foo() { A() }  // Use type alias A as constructor of B
  1. 当类型别名实际指向的类型为 class、interface、struct 时,可以作为访问内部静态成员变量或函数的类型名
type A = B
class B {static var b : Int32 = 0;static func foo() {}
}
func foo() {A.foo() // Use A to access static method in class BA.b
}
  1. 当类型别名实际指向的类型为 enum 时,可以作为 enum 声明的构造器的类型名
enum TimeUnit {Day | Month | Year
}
type Time = TimeUnit
var a = Time.Day  
var b = Time.Month   // Use type alias Time to access constructors in TimeUnit

需要注意的是,当前用户自定义的类型别名暂不支持在类型转换表达式中使用,参考如下示例:

type MyInt = Int32
MyInt(0)  // Error, no matching function for operator '()' function call

8.1 泛型别名

类型别名也是可以声明类型形参的,但是不能对其形参使用 where 声明约束,对于泛型变元的约束我们会在后面给出解释。

当一个泛型类型的名称过长时,我们就可以使用类型别名来为其声明一个更短的别名。例如,有一个类型为 RecordData ,我们可以把他用类型别名简写为 RD :

struct RecordData<T> {var a: Tpublic init(x: T){a = x}
}type RD<T> = RecordData<T>main(): Int64 {var struct1: RD<Int32> = RecordData<Int32>(2)return 1
}

在使用时就可以用 RD<Int32> 来代指 RecordData<Int32> 类型。

9、泛型约束

泛型约束的作用是在函数、class、enum、struct 声明时明确泛型形参所具备的操作与能力。只有声明了这些约束才能调用相应的成员函数。在很多场景下泛型形参是需要加以约束的。以 id 函数为例:

func id<T>(a: T) {return a
}

我们唯一能做的事情就是将函数形参 a 这个值返回,而不能进行 a + 1,println(“${a}”) 等操作,因为它可能是一个任意的类型,比如 (Bool) -> Bool,这样就无法与整数相加,同样因为是函数类型,也不能通过 println 函数来输出在命令行上。而如果这一泛型形参上有了约束,那么就可以做更多操作了。

约束大致分为接口约束子类型约束。语法为在函数、类型的声明体之前使用 where 关键字来声明,对于声明的泛型形参 T1, T2,可以使用 where T1 <: Interface, T2 <: Type 这样的方式来声明泛型约束,同一个类型变元的多个约束可以使用 & 连接。例如:where T1 <: Interface1 & Interface2

例如,仓颉中的 println 函数能接受类型为字符串的参数,如果我们需要把一个泛型类型的变量转为字符串后打印在命令行上,可以对这个泛型类型变元加以约束,这个约束是 core 中定义的 ToString 接口,显然它是一个接口约束:

package core // `ToString` is defined in core.public interface ToString {func toString(): String
}

这样我们就可以利用这个约束,定义一个名为 genericPrint 的函数:

func genericPrint<T>(a: T) where T <: ToString {println(a)
}main() {genericPrint<Int64>(10)  // 10return 0
}

如果 genericPrint 函数的类型实参没有实现 ToString 接口,那么编译器会报错。例如我们传入一个函数做为参数时:

func genericPrint<T>(a: T) where T <: ToString {println(a)
}main() {genericPrint<(Int64) -> Int64>({ i => 0 })return 0
}

如果我们对上面的文件进行编译,那么编译器会抛出泛型类型参数与满足约束的错误。因为 genericPrint 函数的泛型的类型实参不满足约束 (Int64) -> Int64 <: ToString

除了上述通过接口来表示约束,还可以使用子类型来约束一个泛型类型变元。例如:当我们要声明一个动物园类型 Zoo,但是我们需要这里声明的类型形参 T 受到约束,这个约束就是 T 需要是动物类型 Animal 的子类型, Animal 类型中声明了 run 成员函数。这里我们声明两个子类型 Dog 与 Fox 都实现了 run 成员函数,这样在 Zoo 的类型中,我们就可以对于 animals 数组列表中存放的动物实例调用 run 成员函数:

import std.collection.*abstract class Animal {public func run(): String
}class Dog <: Animal {public func run(): String {return "dog run"}
}class Fox <: Animal {public func run(): String {return "fox run"}
}class Zoo<T> where T <: Animal {var animals: ArrayList<Animal> = ArrayList<Animal>()public func addAnimal(a: T) {animals.append(a)}public func allAnimalRuns() {for(a in animals) {println(a.run())}}
}main() {var zoo: Zoo<Animal> = Zoo<Animal>()zoo.addAnimal(Dog())zoo.addAnimal(Fox())zoo.allAnimalRuns()return 0
}

程序的输出为:

dog run
fox run

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/49887.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络安全基础知识及安全意识培训(73页可编辑PPT)

引言&#xff1a;在当今数字化时代&#xff0c;网络安全已成为企业和个人不可忽视的重要议题。随着互联网的普及和技术的飞速发展&#xff0c;网络威胁日益复杂多变&#xff0c;从简单的病毒传播到高级持续性威胁&#xff08;APT&#xff09;、勒索软件攻击、数据泄露等&#x…

【Python】Facebook开源时间序列数据预测模型Prophet

文章目录 一、简介二、项目的文件解读三、Prophet类主要方法和参数3.1 主要参数3.2 主要方法 四、用法示例 一、简介 Prophet 是由 Facebook 开发的一个开源工具&#xff0c;用于时间序列数据的预测。它特别适用于处理具有强季节性和趋势的时间序列数据&#xff0c;并且对节假…

09-软件易用性

易用性是用户体验的一个重要方面&#xff0c;网站建设者一般会沉溺于自己的思维习惯&#xff0c;而造成用户使用的不畅。易用性不仅是专业UI/UE人员需要研究&#xff0c;对于网站建设其他岗位的人也应该了解一定的方法去检验和提升网站的易用性。通常对易用性有如下定义: 易理解…

【iOS】isMemberOfClassisKindOfClass

目录 前言class方法isMemberOfClass和isKindOfClass实例方法分析类方法分析 实例验证总结 前言 认识这两个方法之前&#xff0c;首先要了解isa指向流程和继承链&#xff08;【iOS】类对象的结构分析&#xff09;关系&#xff0c;以便理解得更透彻 上经典图&#xff1a; 要注意…

AM62x和rk3568的异同点

AM62x 和 RK3568 是两款不同的处理器&#xff0c;分别来自 Texas Instruments&#xff08;TI&#xff09;和 Rockchip。它们在设计目标、架构、性能和应用领域等方面存在一些异同。以下是这两款处理器的对比&#xff1a; 1. 基本架构 AM62x&#xff1a; 架构&#xff1a;基于…

与大数据相关的 Python 第三方库和工具

Python 在大数据领域有着广泛的应用&#xff0c;以下是一些与大数据相关的 Python 第三方库和工具&#xff1a; 1. **Pandas**&#xff1a;Pandas 是 Python 中最常用的数据处理和分析库之一&#xff0c;提供了高效的数据结构和数据分析工具&#xff0c;可以进行数据清洗、转换…

机器学习数学基础(1)--线性回归与逻辑回归

声明&#xff1a;本文章是根据网上资料&#xff0c;加上自己整理和理解而成&#xff0c;仅为记录自己学习的点点滴滴。可能有错误&#xff0c;欢迎大家指正。 1 线性回归和逻辑回归与机器学习的关系 线性回归属于机器学习 – 监督学习 – 回归 – 线性回归&#xff0c; 逻辑…

Maven概述

目录 1.Maven简介 2.Maven开发环境搭建 2.1下载Maven服务器 2.2安装&#xff0c;配置Maven 1.配置本地仓库地址 2.配置阿里云镜像地址 2.3在idea中配置maven 2.4在idea中创建maven项目 3.pom.xml配置 1.项目基本信息 2.依赖信息 3.构建信息 4.Maven命令 5.打包Jav…

企业微信报错,api forbidden 错误码 48002

业务场景是这边后端页面点同步就去企微接口拉取客户数据&#xff0c;然后报错如下。 后端抓包返回的json如下 {“errcode”:48002,“follow_user”:[],“errmsg”:“api forbidden, hint: [1721869790252850672734303], from ip: 203.88.203.216, more info at https://open.w…

数据结构(链表)

&#x1f30f;个人博客主页&#xff1a;心.c 前言&#xff1a; 最近练习算法回去学了链表&#xff0c;收获挺大的&#xff0c;大概内容整理了一下&#xff0c;语言是用c写的&#xff0c;所以在这里分享给大家&#xff0c;希望大家可以有所收获 &#x1f525;&#x1f525;&…

2024年技校大数据实验室建设及大数据实训平台整体解决方案

随着信息技术的迅猛发展&#xff0c;大数据已成为推动产业升级和社会进步的重要力量。为适应市场需求&#xff0c;培养高素质的大数据技术人才&#xff0c;技校作为职业教育的重要阵地&#xff0c;亟需加强大数据实验室的建设与实训平台的打造。本方案旨在提出一套全面、可行的…

Synchronized关键字和乐观锁(CAS)

一、Sychronized关键字 在Java中&#xff0c;synchronized 是一个关键字&#xff0c;用于实现线程同步。当一个方法或一个代码块被synchronized修饰时&#xff0c;它被称为同步方法或同步代码块。这意味着每次只有一个线程可以进入该方法或代码块&#xff0c;其他线程必…

二维码的生成与识别(python)

二维码生成 from PIL import Image import qrcode from qrcode.image.styledpil import StyledPilImage from qrcode.image.styles.colormasks import SolidFillColorMask from qrcode.image.styles.moduledrawers import SquareModuleDrawer# 创建二维码对象 qr qrcode.QRCo…

Windows系统笔记本无法连接Wi-Fi常见原因及解决办法

在现代生活中&#xff0c;Wi-Fi已成为我们连接互联网不可或缺的方式之一。 然而&#xff0c;有时我们的Windows系统笔记本可能会遇到无法连接Wi-Fi的问题。 这种情况可能由多种原因引起&#xff0c;包括硬件故障、驱动问题、系统设置等。 以下是针对Windows 10和Windows 11系…

【STM32】stm32中GPIO_ReadInputDataBit()是什么意思

GPIO_ReadInputDataBit()函数用于读取指定GPIO端口的某一引脚上的电平状态&#xff0c;并返回该引脚的电平是高电平&#xff08;1&#xff09;还是低电平&#xff08;0&#xff09;。 在STM32单片机中&#xff0c;GPIO&#xff08;General-Purpose Input/Output&#xff09;端…

vue3在元素上绑定自定义事件弹出虚拟键盘

最近开发中遇到一个需求: 焊接机器人的屏幕上集成web前端网页, 但是没有接入键盘。这就需要web端开发一个虚拟键盘,在网上找个很多虚拟键盘没有特别适合,索性自己写个简单的 图片: 代码: (代码可能比较垃圾冗余,也没时间优化,凑合看吧) 第一步:创建键盘组件 为了方便使用…

【Django】 读取excel文件并在前端以网页形式显示-安装使用Pandas

文章目录 安装pandas写views写urls安装openpyxl重新调试 安装pandas Pandas是一个基于NumPy的Python数据分析库&#xff0c;可以从各种文件格式如CSV、JSON、SQL、Excel等导入数据&#xff0c;并支持多种数据运算操作&#xff0c;如归并、再成形、选择等。 更换pip源 pip co…

Flink SQL 实时读取 kafka 数据写入 Clickhouse —— 日志处理(三)

文章目录 前言Clickhouse 表设计adlp_log_local 本地表adlp_log 分布式表 Flink SQL 说明创建 Source Table (Kafka) 连接器表创建 Sink Table (Clickhouse) 连接器解析 Message 写入 Sink 日志查询演示总结 前言 在之前的文章中&#xff0c;我们总结了如何在 Django 项目中进…

构建智慧水利系统,优化水资源管理:结合物联网、云计算等先进技术,打造全方位、高效的水利管理系统,实现水资源的最大化利用

本文关键词&#xff1a;智慧水利、智慧水利工程、智慧水利发展前景、智慧水利技术、智慧水利信息化系统、智慧水利解决方案、数字水利和智慧水利、数字水利工程、数字水利建设、数字水利概念、人水和协、智慧水库、智慧水库管理平台、智慧水库建设方案、智慧水库解决方案、智慧…

spring-boot3.x整合Swagger 3 (OpenAPI 3) +knife4j

1.简介 OpenAPI阶段的Swagger也被称为Swagger 3.0。在Swagger 2.0后&#xff0c;Swagger规范正式更名为OpenAPI规范&#xff0c;并且根据OpenAPI规范的版本号进行了更新。因此&#xff0c;Swagger 3.0对应的就是OpenAPI 3.0版本&#xff0c;它是Swagger在OpenAPI阶段推出的一个…