程序验证之Dafny--证明霍尔逻辑的半自动化利器
一、What is Dafny?【来自官网介绍 Dafny 】
1)介绍
Dafny 是一种支持验证的编程语言,配备了一个静态程序验证器。
通过将复杂的自动推理与熟悉的编程习语和工具相结合,使开发者能够编写可证明正确的代码(相对于 {P}S{Q} 这种霍尔三元组的规范而言)。
Dafny 还可以将 Dafny 代码编译到熟悉的开发环境,如 C#、Java、JavaScript、Go 和 Python;使得严格的验证成为开发过程的有机组成部分,从而减少了可能在测试中被遗漏的、代价高昂的后期错误。
2)原理
Dafny验证器根据我们提供的前置条件,后置条件,以及循环不变式,自动生成验证条件(Verification Condition,VC)的逻辑公式。 这些验证条件主要包含,不变式的可达性,归纳性,以及使用循环不变式证明后置条件。 这些逻辑公式都会交给同样是由微软开发的SMT求解器Z3来求解,并依据求解结果,返回验证结果。
二、使用快速入门:
2.1 一个.dfy后缀的文件:
程序主要包含以下几部分:
类型(
types
)方法(
methods
)函数(
functions
)用户自定义的类型包括类(
class
)和归纳数据类型(inductive class
)类class本身也包含一组声明(
declarations
)、介绍字段(introducing fields
)、方法(methods
)和函数(functions
)
注释:// 双斜杠 或者 /* xxxxx */
2.1.1基本定义
在类中,定义字段x为数据类型(types)T
: var x: T
注意事项:
-
数据类型必需手动申明的,不会被自动推断。
-
通过在声明前加上关键 ghost 可以将该字段声明为幽灵(即用于规范而不是执行)字段。
Dafny 的9种数据类型包括:
bool
:布尔值int
:无界整数string
: 字符串class/inductive class
: 用户自定义的类和归纳类、set<T>
:不可变的无序集合seq<T>
:不可变的有序集合array<T>
、array2<T>
、array3<T>
: 多维数组类型object
:所有类型的超类nat
:范围是int一半,非负整数。
2.1.2方法 methods
方法的声明如下:
method M(a: A, b: B, c: C) r eturns (x: X, y: Y, z: Y) //输入输出参数
requires Pre //前置条件
modifies Frame //框架
ensures Post //后置条件
decreases TerminationMetric //变体函数
{
Body //函数体`
}
其中:
- a, b, c : 输入参数
- x, y, z : 输出参数
- Pre: 表示方法 前提条件 的 布尔表达式
- Frame: 表示类对象的集合,可以被方法更新(Frame denotes a set of objects whose fields may be updated by the method)
- Post: 是方法 后置条件 的 布尔表达式
- TerminationMetric: 是方法的变体函数(TerminationMetric is the method’s variant function)
- Body: 是实现方法的语句。
2.1.2.1框架Frame
框架Frame 是单个或多个对象组成的表达式的集合。(见下面例子)
框架Frame
是由类内对象和类外方法内对象两部分组成。(反正就是一堆类对象的集合)
例如,如果 c 和 d 是类C的对象,那么以下每行意思是一样的。
modifies {c, d}
modifies {c} + {d}
modifies c, {d}
modifies c, d
如果方法内啥都没写,那么前置和后置条件默认为真,框架默认为空集。
2.1.2.2变体函数 variant function
变体函数是一个表达式组成的列表,表示由给定表达式组成的字典元组,后跟隐含的“top”元素。
如果省略没写的话,Dafny 将猜测该方法的变体函数,通常是以该方法的参数列表开头的字典元组。
Dafny IDE 将在工具提示中显示猜测。
2.1.2.3ghost 关键字
通过在声明之前加上关键字 ghost
可以将方法声明为 ghost
方法(仅规范而不用于执行)。
2.1.2.4this 关键字
默认情况下,类中的方法都具有隐式接收器参数 this
。可以通过在方法声明之前使用关键字 static 来删除此参数。
类 C 中的静态方法 M 可以由 C.M(...)
调用。
2.1.2.5构造函数/构造体 constructor
在类中,一个方法可以通过将method
关键字替换为constructor
,申明一个构造方法。
构造函数(构造方法)只能在分配对象时调用(参见示例)
对于包含一个或多个构造函数的类,对象创建必须与对构造函数的调用一起完成。
通常,一个方法当然得有一个名字,但是一个类可以有一个没有名字的构造函数,也就是匿名构造函数 constructor (n:int )
constructor (n: int) //constructor 匿名构造器`
modifies this //框架内对象的构造体 this就是this.frame?`{Body
}
2.1.3函数 function
函数具有以下形式:
function F(a: A, b: B, c: C): T
requires Pre //前置条件pre
reads Frame //框架frame
ensures Post //后置条件post
decreases TerminationMetric //变体函数
{Body //函数体
}
a
,b
,c
: 输入的形参,T
: 返回结果的类型,Pre
: 表示函数前提条件的布尔表达式,Frame
: 函数体body需要的对象列表Post
: 函数的后置条件布尔表达式TerminationMetric
: 变体函数Body
: 定义函数的表达式。
前置条件允许函数是部分的(只用前置就行不用写后置),即前置条件表示函数何时定义,并且 Dafny 会验证函数的每次使用都满足前置条件。
通常不需要后置条件,因为函数在函数体内已经给出了完整的定义。
例如:
(写个后置加个保险也行,一般后置就是声明该函数的基本属性,比如Factorial这个函数所有数字都≥1)
function Factorial(n: int): int
requires 0 <= n //前置条件pre
ensures 1 <= Factorial(n) //后置条件post
{if n == 0 then 1 else Factorial(n-1) * n //函数体body
}
要在后置条件中引用函数的结果,请使用函数本身的名称,如示例中所示。
默认情况下,函数是ghost
,不能从可执行(非ghost
)代码中调用。
为了使它从ghost
变成非ghost
,用关键字function method替换 function.
一个返回布尔值的函数可以用关键字声明,然后省略冒号和返回类型。
如果函数或方法被声明为类class成员,则它具有隐式接收器参数 this。可以通过在声明之前加上关键字static
来删除此参数。
类 C 中的静态函数 F 可以被 C.F(...)
调用。
E.g.2:[educoder实训实例]:
method test(n: int) returns (sum: int)requires n >= 0ensures sum * 2 == n * (n+1)
{sum := 0;var i:int := 0;while i <= ninvariant i<=n+1invariant sum*2 == i*(i-1){sum := sum + i;i := i + 1;}
}
2.1.3.1 类 class
一个类定义如下:
class C {// member declarations go here
}
其中类的成员(字段、方法和函数)在花括号内定义(如上所述)。
2.1.3.2数据类型 datatypes
归纳数据类型(inductive datatype)是一种类型,其值是用一组固定的构造函数创建的。
数据类型 为Tree带有构造函数 Leaf 和 Node 的函数声明如下:
datatype Tree = Leaf | Node(Tree, int, Tree)//Leaf为无参构造函数 Node为有参
构造函数由竖线分隔。 无参数构造函数不需要使用括号,如 Leaf 所示。
对于每个构造函数 Ct,数据类型隐式声明了一个布尔成员 Ct?,对于已经使用 Ct 构造函数赋的值的成员,它返回 true。 例如,在代码片段之后:
var t0 := Leaf;
var t1 := Node(t0, 5, t0);
表达式 t1.Node
结果为 true, t0.Node
结果为false。
如果两个数据类型值是使用相同的构造函数和该构造函数的相同参数创建的,则它们是相等的。因此,对于像 Leaf
、t.Leaf
这样的无参数构造函数,Dafny会给出与 t == Leaf
相同的结果。(没看懂,不管了)
构造函数可以选择为其任何参数声明析构函数,这是通过为参数引入名称来完成的。 例如,如果 Tree 被声明为:
datatype Tree = Leaf | Node(left: Tree, data: int, right: Tree)
那么t1.data == 5
和t1.left == t0
在上面的代码片段之后保持不变。(还是没懂)
2.1.3.3泛型 Generics
Dafny同其他语言一样都有泛型,任何类、方法、函数都可以有类型参数,在<>中申明该数据类型T
class MyMultiset<T> {/*...*/
} //类泛型
datatype Tree<T> = Leaf | Node(Tree<T>, T, Tree<T>) //自定义数据泛型
method Find<T>(key: T, collection: Tree<T>) { //方法泛型`/*...*/
}function IfThenElse<T>(b: bool, x: T, y: T): T { //函数泛型/*...*/
}
2.1.3.4声明 Statement
以下是 Dafny 中最常见语句:
var LocalVariables := ExprList;
Lvalues := ExprList;
assert BoolExpr;
print ExprList;if BoolExpr0 {Stmts0
} else if BoolExpr1 {Stmts1
} else {Stmts2
}while BoolExprinvariant Invmodifies Framedecreases Rank
{Stmts
}match Expr {case Empty => Stmts0case Node(l, d, r) => Stmts1
}break;return;
函数/方法的返回值赋值给变量
(就是将函数/方法返回的值或对象 赋给 一个局部变量而已) var LocalVariables := ExprList;
var 语句引入了局部变量。 Lvalues := ExprList;
赋值语句将 ExprList
变量赋给Lvalues
。 这些分配是并行执行的(更重要的是,所有必要的读取都发生在写入之前),因此左侧必须表示不同的 L 值。 每个右侧都可以是以下形式之一的表达式或对象创建:
new T
new T.Init(ExprList)
new T(ExprList)
new T[SizeExpr]
new T[SizeExpr0, SizeExpr1]
第一种形式分配一个类型为 T 的对象。
第二种形式另外在新分配的对象上调用初始化方法或构造函数。
第三种形式是当调用匿名构造函数时的语法。
其他形式分别了T是一维和二维数组对象的匿名构造方法
assert 声明
assert
语句判断后面的表达式结果是否为真(由验证器验证)。
print 打印语句
打印语句将给定打印表达式的值输出到标准输出。字符串中的字符可以转义;例如,对 print
语句感兴趣的是 \n
表示字符串中的换行符。
if 选择语句
if 语句是通常的语句。该示例显示了使用 else if 将备选方案串在一起。像往常一样,else 分支是可选的。
while 循环语句
while
语句是通常的循环,其中invariant
声明给出了一个循环变量modifies
语句限制了循环的框架reduction
语句从循环中引入了一个变体函数。
默认情况下,循环不变式为真,修改框与封闭上下文中的相同(通常是封闭方法的修改子句),并从循环保护中猜测变体函数。(真没看懂)
while BoolExpr //布尔表达式-循环条件`invariant Invmodifies Framedecreases Rank{Statements
}
break语句
break 语句可用于退出循环,而 return 语句可用于退出方法。
2.1.4 表达式 Expressions
Dafny 中的表达式与类 Java 语言中的表达式非常相似。以下是一些值得注意的差异。
2.1.4.1基本运算符
除了短路布尔运算符 &&
(and) 和 ||
(或),Dafny 有一个短路蕴涵运算符 ==>
和一个 if-and-only-if
运算符 <==>
。
正如它们的宽度所暗示的那样,<==>
具有比 ==>
低的绑定力,而后者又比 &&
和 ||
具有更低的绑定力。
Dafny 比较表达式可以是链式的,这意味着相同方向的比较可以串在一起。例如,
0 <= i < j <= a.Length == N
含义相同:
0 <= i && i < j && j <= a.Length && a.Length == N
请注意,布尔相等可以使用 ==
和 <==>
来表示。这些之间有两个区别。首先,==
比 <==>
具有更高的约束力。其次,==
是链接,而 <==>
是关联的。也就是说,a == b == c
与 a == b && b == c
相同,而 a <==> b <==> c
与 a <==> (b <== > c)
,这也与 (a <==> b) <==> c
相同。
整数运算
对整数的运算是常用的运算,除了 /
(整数除法)和 %
(整数模)遵循欧几里德定义,这意味着 %
总是导致非负数。 (因此,当 /
或 %
的第一个参数为负数时,结果与您在 C、Java 或 C# 中得到的结果不同,请参阅 http://en.wikipedia.org/wiki/Modulo_operation。)
集合运算
集合上的操作包括+
(并)、*
(交)和-
(集合差)、集合比较运算符<
(真子集)、<=
(子集)、它们的对偶>
和>=
,以及!!
(脱节)。 S 中的表达式 x
表示 x
是集合 S
的成员,而 x !in S
是一个方便的写法 !(x in S)
。
要从某些元素创建一个集合,请将它们括在花括号中。例如,{x,y} 是由 x 和 y 组成的集合(如果 x == y,则为单例集),{x} 是包含 x 的单例集,{} 是空集。
序列运算
对序列的操作包括 +(连接)和比较运算符 <
(适当的前缀)和 <=
(前缀)。成员资格可以像集合一样检查:x in S
和 x !in S
。序列 S 的长度表示为 |S|,并且此类序列的元素具有从 0 到小于 |S| 的索引。表达式 S[j] 表示序列 S 的索引 j 处的元素。表达式 S[m..n]
,其中 0 <= m <= n <= |S|
,返回一个序列,其元素是S 从索引 m 开始(即,从 S[m]、S[m+1]、……直到但不包括 S[n])。表达式 S[m..]; (通常称为“drop m”)与 S[m..|S|]
相同;也就是说,它返回除 S 的前 m 个元素之外的所有元素的序列。表达式 S[..n]
; (通常称为“take n”)与 S[0..n] 相同,即它返回由 S 的前 n 个元素组成的序列。
如果 j 是序列 S 的有效索引,则表达式 S[j := x]
;是类似于 S 的序列,只是它在索引 j 处有 x。
最后,要从一些元素组成一个序列,请将它们括在方括号中。例如,[x,y] 是由两个元素 x 和 y 组成的序列,使得 [x,y][0] == x 和 [x,y][1] == y
,[x] 是唯一元素是 x 的单例序列,[] 是空序列。
if-then-else判断语句
if-then-else
表达式的形式为:if BoolExpr then Expr0 else Expr1
其中 Expr0
和 Expr1
是相同类型的任何表达式。与 if
语句不同,if-then-else
表达式使用 then
关键字,并且必须包含显式的 else 分支。
三、环境配置及实例
vscode内配置dafny:
打开[.dfy]文件:
插件默认在每次执行文件保存操作后,尝试对Dafny代码进行验证。未验证通过的位置会有红色波浪线标出,鼠标在红色波浪线处悬停会显示错误详情。
无法验证显示:
成功显示:
项目首页 - dafny - GitCode
达夫尼博客 |来自 Dafny 维护者和嘉宾的新闻和教育材料。
简单上手 | Dafny (aaron-clou.github.io)https://aaron-clou.github.io/dafnycommunity/pages/39fb20/#hello-dafny
程序验证(5)- 程序验证案例 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/312501103
dafny-lang/dafny: Dafny is a verification-aware programming language (github.com)https://github.com/dafny-lang/dafny
相关文章:

程序验证之Dafny--证明霍尔逻辑的半自动化利器
一、What is Dafny?【来自官网介绍 Dafny 】 1)介绍 Dafny 是一种支持验证的编程语言,配备了一个静态程序验证器。 通过将复杂的自动推理与熟悉的编程习语和工具相结合,使开发者能够编写可证明正确的代码(相对于 {P}S{Q} 这种…...
Flutter 中的 SafeArea 小部件:全面指南
Flutter 中的 SafeArea 小部件:全面指南 在移动应用开发中,处理设备屏幕的边缘是一个常见的挑战,尤其是考虑到现代设备通常具有不同的屏幕形状,如刘海屏、曲面屏等。为了确保应用内容不会覆盖这些屏幕区域,Flutter 提…...

webpack生成模块关系依赖图示例:查看构建产物的组成部分 依赖关系图
npm i -D webpack-bundle-analyzer core-js babel-loaderwebpack.config.js const BundleAnalyzerPlugin require(webpack-bundle-analyzer).BundleAnalyzerPlugin; module.exports {entry: ./src/index.js,output: {filename: main.js,},// mode: production, // 或者 produ…...
Spacy的安装与使用教程
官网安装指导教程 https://spacy.io/usage 安装指令 需要根据自己系统的cuda版本选择 nvcc -V pip install -U pip setuptools wheel pip install -U spacy[cuda12x] python -m spacy download zh_core_web_sm python -m spacy download en_core_web_sm...

Pathlib,一个不怕迷路的 Python 向导
大家好!我是爱摸鱼的小鸿,关注我,收看每期的编程干货。 一个简单的库,也许能够开启我们的智慧之门, 一个普通的方法,也许能在危急时刻挽救我们于水深火热, 一个新颖的思维方式,也许能…...

详解绝对路径和相对路径的区别
绝对路径和相对路径是用于描述文件或目录在文件系统中位置的两种不同方式。 绝对路径(Absolute Path)是从文件系统的根目录开始的完整路径,可以唯一地确定一个文件或目录的位置。在不同的操作系统中,根目录的表示方式可能略有不同…...

C++二叉搜索树搜索二叉树二叉排序树
C二叉搜索树 1. 二叉搜索树的概念 二叉搜索树(BST,Binary Search Tree),也称为二叉排序树或二叉查找树。它与一般二叉树的区别在于:每个结点必须满足“左孩子大于自己,右孩子小于自己”的规则。在这种规则的约束下,二…...

Java 自然排序和比较器排序区别?Comparable接口和Comparator比较器区别?
注:如果你对排序不理解,请您耐心看完,你一定会明白的。文章通俗易懂。建议用idea运行一下案例。 1)自然排序和比较器排序的区别? 自然排序是对象本身定义的排序规则,由对象实现 Comparable 接口ÿ…...
【CV】opencv调用DIS/LK等计算光流,前一帧和当前帧写反了有什么影响?
当在计算光流时,将前一帧和当前帧输入反了,会导致一系列问题。 在计算光流时,通常是将前一帧作为模板,根据当前帧计算光流。因为光流是描述相邻帧之间像素移动的一种方法,它通过比较两帧之间的像素强度或特征点的移动…...
C语言学习细节|C语言面向对象编程!函数指针如何正确使用
文章目录 1.函数指针定义2.格式3.应用回调函数动态函数调用函数的间接调用 4.结构体与函数指针结合 1.函数指针定义 函数指针就是一个指向函数的指针变量,与指向数据的指针不同,函数指针保存的是函数的地址,这使得程序可以动态地调用不同的函…...

C语言简要(一)
总得让她开心吧 helloworld #include <stdio.h>int main() {printf("hello world!\n");return 0; } 程序框架 #include <stdio.h> int main {return 0; }输出 printf("hello world!\n"); "里面的内容叫做“字符串”,prin…...

那些年我与c++的叫板(一)--string类自实现
引子:我们学习了c中的string类,那我们能不能像以前数据结构一样自己实现string类呢?以下是cplusplus下的string类,我们参考参考! 废话不多说,直接代码实现:(注意函数之间的复用&…...
二刷算法训练营Day08 | 字符串(1/2)
今日任务: 344.反转字符串 541. 反转字符串II卡码网:54.替换数字 151.翻转字符串里的单词卡码网:55.右旋转字符串 详细布置: 1. 344. 反转字符串 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 …...

使用高防IP是应对网络安全的重要措施
使用高防IP(High Defense IP)在现代网络环境中显得尤为重要,这主要源于以下几个方面的原因: 一、网络安全形势严峻 随着互联网的快速发展,网络安全问题日益突出。各种网络攻击手段层出不穷,如分布式拒绝服…...
代码随想录-算法训练营day40【动态规划03:整数拆分、不同的二叉搜索树】
代码随想录-035期-算法训练营【博客笔记汇总表】-CSDN博客 第九章 动态规划part03● 343.整数拆分 ● 096.不同的二叉搜索树 详细布置 今天两题都挺有难度,建议大家思考一下没思路,直接看题解,第一次做,硬想很难想出来。343. 整数…...

MySQL数据库中基本数据管理操作
使用SQL语句实现基本数据管理操作——即DML语句 1.添加数据 insert into 表名(字段名称,字段名称,字段名称)values(数据,数据,数据) 在MySQL数据库中,除了数字&#x…...

记录一下Hql遇到的零碎问题
建表相关 -- 地区维度表 drop table dim_province_full; create table dim_province_full( id string comment 编号, name string comment 省份名称, region_id string comment 大区id, area_code string comment 行政区位码, iso_code string comment 国际编码, iso_3166_2 s…...
Flutter 中的 TextField 小部件:全面指南
Flutter 中的 TextField 小部件:全面指南 在 Flutter 中,TextField 是一个允许用户输入文本的小部件。它非常灵活,支持多种文本输入场景,如单行文本、多行文本、密码输入、数值输入等。TextField 还提供了丰富的定制选项…...

GPT-4o:全面深入了解 OpenAI 的 GPT-4o
GPT-4o:全面深入了解 OpenAI 的 GPT-4o 关于 GPT-4o 的所有信息ChatGPT 增强的用户体验改进的多语言和音频功能GPT-4o 优于 Whisper-v3M3Exam 基准测试中的表现 GPT-4o 的起源追踪语言模型的演变GPT 谱系:人工智能语言的开拓者多模式飞跃:超越…...

2024中国应急(消防)品牌巡展西安站成功召开!惊喜不断
消防品牌巡展西安站 5月10日,由中国安全产业协会指导,中国安全产业协会应急创新分会、应急救援产业网联合主办,陕西消防协会协办的“一切为了安全”2024年中国应急(消防)品牌巡展-西安站成功举办。该巡展旨在展示中国应急(消防&am…...

网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...

.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
生成 Git SSH 证书
🔑 1. 生成 SSH 密钥对 在终端(Windows 使用 Git Bash,Mac/Linux 使用 Terminal)执行命令: ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" 参数说明: -t rsa&#x…...

SpringTask-03.入门案例
一.入门案例 启动类: package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...

关键领域软件测试的突围之路:如何破解安全与效率的平衡难题
在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件,这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下,实现高效测试与快速迭代?这一命题正考验着…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...