快速检查(QuickCheck)(译)

article/2025/10/11 15:01:54

source: http://www.jianshu.com/p/bb93972bac53


快速检查(QuickCheck)(译)

144
作者 低智商游戏
2016.02.01 00:26* 字数 4778 阅读 239 评论 0

本文译自 objc.io出品的书籍《Functional Programming in swift》第六章,objc.io 由 Chris Eidhof, Daniel Eggert 和 Florian Kugler 成立于柏林。成 objc.io 的目的是针对深入的、跟所有 iOS 和 OS X 开发者相关的技术话题创造一个正式的平台。objc .io 出过24期的期刊,每一期都针对特定的主题提出解决方案, 可以到objc中国查看这些文章的中文版本。本书延续了 objc.io 一贯的风格,讲解得很深入,可惜译者水平有限,无法将书中的精彩之处忠实的表达出来。 想购买正版书籍请到
https://www.objc.io/books/functional-swift/

近年来,测试在Object-C中越来越普遍。许多流行的库都可以使用持续化集成工具自动进行测试。编写单元测试的标准框架是 XCTest。另外,还可以使用许多第三方框架(例如Specta,Kiwi和FBSnapshotTestCase。几个针对swift的测试框架现在也在开发中。

所有这些框架都遵循类似的模式:测试由一些代码片段以及一个期望的值组成。执行该代码片段,将执行结果和预期的值相比较。不同的库测试的级别也不同----一些测试单独的函数,一些测试类,还有一些执行集成测试(运行整个程序)。在本章中,我们为swift函数创建了一个小型的基于属性的测试库。

编写单元测试时,输入数据是静态的,由程序员进行定义。例如,使用单元测试测试addition方法,我么可能编写测试来核实 1+1是否与2相等。如果更改之后的addition函数运行的结果不为2,测试就会失败。还可以更进一步,我们可以测试加法运算的交换律---也就是说, 测试 a+b 和b+a相等。要测试这一点,我们需要编写一个测试case验证 42+7 等于7+42

QuickCheck是用于随机测试的Haskell库。QuickCheck不用编写独立的单元测试代码(单元测试是测试一个函数对于一些输入是否能输出正确的结果),而是允许你描述函数的抽象属性并生成测试改变这些属性。在本章中,我们建立QuickCheck的swift版本。

最好通过例子进行讲解。假设我们想验证加法符合交换律。要验证这一点,对于整数x, y,我们编写一个程序检查x+y 是否等于 y+x。

func plusIsCommutative(x: Int, y: Int) -> Bool { return x + y == y + x
}

使用QuickCheck验证交换律和调用check函数一样简单

check("Plus should be commutative", plusIsCommutative) > "Plus should be commutative" passed 100 tests.
> ()

check函数通过一次次的使用两个随机的整数调用plusIsCommutative来完成功能。如果结果不为true,它会打印出引起测试失败的两个输入参数。这里的关键是我们能够使用一个返回布尔型的函数(如plusIsCommutative)来描述代码中的抽象属性(如交换律)。check函数使用这个属性生成单元测试,比你自己手写的单元测试代码拥有更好的覆盖率。

当然,并不是所有的测试都能通过。例如,我们编写一条语句判断减法是否符合交换律:

func minusIsCommutative(x: Int, y: Int) -> Bool { return x - y == y - x
}

运行QuickCheck作用于这个函数,测试会失败:

check("Minus should be commutative", minusIsCommutative)> "Minus should be commutative" doesn't hold: (0, 1)
> ()

使用swift尾随闭包的语法,我们也可以直接编写测试,而不用再定义属性。

check("Additive identity") { (x: Int) in x + 0 == x } > "Additive identity" passed 100 tests.
> ()

当然,还有其他相似的标准算术运算的属性可以测试,我们会介绍更有趣的测试和属性。在这之前,我们将介绍QuickCheck是怎样实现的

创建QuickCheck

要实现QuickCheck的swift版本,需要完成以下的事情

  • 首先,需要一种方法生成不同类型的随机值
  • 使用随机值生成器,我们需要实现check函数,并使用合适的方式将生成的随机数传递改改函数
  • 如果测试失败,我们需要使测试失败的输入尽可能的小。比如,如果使用含有100个元素的数组测试时失败了,我们需要试一下更小的数组会不会导致失败
  • 最后,我们需要做一些额外的工作确保check函数可以作用在通用类型上。

生成随机数

首先,定义一个协议用于产生随机值。这个协议只包含一个函数,arbitrary,返回类型的Self的值,比如,实现Arbitrary协议的类或者结构体:

protocol Arbitrary {class func arbitrary() -> Self
}

我们编写Int类型的一个实现。我们使用标准库的arc4random函数并将其返回值强转为Int型。这一这只能生成正整数。真正的实现需要产生负数和整数,但是本章我们让事情尽可能简单。

extension Int: Arbitrary {static func arbitrary() -> Int {return Int(arc4random()) }
}

现在就可以产生随机整数了:

Int.arbitrary()
> 2158783973

要生成随机的字符串,我们需要做更多的工作。我们首先产生随机的字符:

extension Character: Arbitrary {static func arbitrary() -> Character {return Character(UnicodeScalar(random(from: 65, to: 90))) }func smaller() -> Character? { return nil } 
}

然后,我们生成长度为0 ~ 40 ----x-----的随机字符串----使用下面定义的random函数。然后我们生成x个随机的字符,然后将这些字符reduce为一个字符串。注意我们只生成了大写的字符串。在产品性质的库中,需要产生更长的随机字符串。

func tabulate<A>(times: Int, f: Int -> A) -> [A] { return Array(0..<times).map(f)
}func random(#from: Int, #to: Int) -> Int {return from + (Int(arc4random()) % (to-from))
}extension String: Arbitrary {static func arbitrary() -> String {let randomLength = random(from: 0, to: 40)let randomCharacters = tabulate(randomLength) { _ inCharacter.arbitrary() }return reduce(randomCharacters, "") { $0 + String($1) } }
}

tabulate函数首先使用 0 到times-1 填充一个数组,然后使用map函数得出一个f(0), f(1), ..., f(times-1)填充的数组。String的arbitrary扩展使用tabulate函数生成一个随机的字符串数组。
和生成Int随机数的调用方式相同:

String.arbitrary()
> XMVDXQEIRYNRJTWELHESXHIGPSPOFETEEX

实现check函数

现在准备实现check函数的第一个版本。check1函数包含一个循环,这个循环的每一个迭代均产生随机的输入,用于作为参数属性。如果发现了反例,该随机数被打印出来,循环结束,函数返回。如果没有发现反例,check1函数货报告通过了测试(注意我们将函数命名为check1,因为我们稍后就要编写该函数的最终版本)

func check1<A: Arbitrary>(message: String, prop: A -> Bool) -> () { for _ in 0..<numberOfIterations {let value = A.arbitrary() if !prop(value) {println("\"\(message)\" doesn't hold: \(value)")return} }println("\"\(message)\" passed \(numberOfIterations) tests.") 
}

我们本来还可以使用reduce 或者 map这种更函数式的方式编写这个函数,而不是使用for循环。在这个例子中,使用for 循环十分合适:我们想要执行一个操作固定的次数,并且在找到反例时退出循环----使用for循环简直完美。
下面演示怎样使用这个函数来测试属性:

func area(size: CGSize) -> CGFloat { return size.width * size.height
}check1("Area should be at least 0") { size in area(size) >= 0 }> "Area should be at least 0" doesn't hold: (-459.570969794777,4403.85297392585) 
> ()

这个例子很好的说明了QuickCheck的有用之处---它可以帮助我们发现边界条件。如果size的width和height其中一个为负数,那么area函数酒会返回负数。当作为CGRec的一部分时, CGSize可以为负数。如果编写传统的单元测试代码,很容易漏掉这种情况,因为size通常都是正数。

让值变的更小

当使用check1检测字符串时,有可能会返回很长的错误消息:

check1("Every string starts with Hello") { (s: String) in s.hasPrefix("Hello")
}> "Every string starts with Hello" doesn't hold: MAEYXBOKFDUALXOLSQTEWJEQNAP 
> ()

理想状态下,失败的输入越短越好。通常,反例越小,越容易发现问题出自哪儿。在这个例子中,这个反例仍然很易于理解---但并不总是这样。想想一个测试数组或者字典的复杂的例子---如果测试失败,可能很难定位问题----如果作为输入参数的数组或者字典简短一些,问题会更易于跟踪。通常,用户可以试着裁剪触发测试失败的输入并尝试重新运行测试---然而,我们可以让用户省下这些麻烦,我们自动进行这个过程。

要做到这一点,需要实现另一个协议,称为Smaller。这个协议只做一件事情--它会视图缩短反例

protocol Smaller {func smaller() -> Self?
}

注意smaller的返回值时可选类型。当不知道如何更进一步的缩短反例时,返回nil。例如,没有办法缩短一个空的数组,在这种情况下,返回nil

在我们的例子中,例如整数,我们会将该值处以2直到0为止。

extension Int: Smaller { func smaller() -> Int? {return self == 0 ? nil : self / 2 }
}

现在可以测试了:

100.smaller()
> Optional(50)

对于字符串,我们丢弃第一个字符(除非是空字符串):

extension String: Smaller { func smaller() -> String? {return self.isEmpty ? nil : dropFirst(self)} 
}

要在check函数中使用check函数,我们需要能够缩短check产生的任意一个测试数据。我们重新定义Arbitrary,让它继承自Smaller协议:

protocol Arbitrary: Smaller { class func arbitrary() -> Self
}

重复缩短(Repeatedly Shrinking)

现在我们重新定义check函数使得它能够缩短触发测试失败的输入数据。我们使用iterateWhile函数,该函数以返回布尔型的函数(condition) 和初始值为参数,并重复执行这个函数,直到条件为假。

func iterateWhile<A>(condition: A -> Bool, initialValue: A,next: A -> A?) -> A {if let x = next(initialValue) { if condition(x) {return iterateWhile(condition, x, next) }}return initialValue 
}

使用iterateWhile,我们以重复缩短反例的长度知道测试通过

ffunc check2<A: Arbitrary>(message: String, prop: A -> Bool) -> () { for _ in 0..<numberOfIterations {let value = A.arbitrary() if !prop(value) {let smallerValue = iterateWhile({ !prop($0) }, value) { $0.smaller()}println("\"\(message)\" doesn't hold: \(smallerValue)") return} }println("\"\(message)\" passed \(numberOfIterations) tests.") 
}

这段代码多了许多的工作:产生随机输入值,检查它们是否满足property条件,如果发现反例,就重复的缩短这个反例。之所以定义iterateWhile函数,而不是直接使用while循环,是为了使控制流程尽可能简单。

随机数组

到现在为止,check2函数只支持Int和String。我们可以定义其它类型的扩展,比如布尔型。生成随机数组的工作变得复杂起来。让我们写一个实用版本的例子用来激励大家:

func qsort(var array: [Int]) -> [Int] {if array.isEmpty { return [] }let pivot = array.removeAtIndex(0)let lesser = array.filter { $0 < pivot }let greater = array.filter { $0 >= pivot } return qsort(lesser) + [pivot] + qsort(greater)
}

同时编写一个属性检查我们编写的qsort版本和编译器内建的sort函数是否具有相同的功能:

check2("qsort should behave like sort") { (x: [Int]) inreturn qsort(x) == x.sorted(<)
}

然而,编译器会警告[Int]没有实现Arbitrary协议。在实现Arbitrary协议之前,首先实现Smaller。作为第一步,我们提供一个简化版本:去掉数组的第一个元素:

extension Array: Smaller { func smaller() -> [T]? {if !self.isEmpty {return Array(dropFirst(self)) }return nil} 
}

我们还可以编写一个生成随机长度数组(数组中的元素需要遵循Arbitrary协议,每个元素都是随机的)的函数:

func arbitraryArray<X: Arbitrary>() -> [X] {let randomLength = Int(arc4random() % 50)return tabulate(randomLength) { _ in return X.arbitrary() }
}

下面定义数组的扩展。这个扩展使用arbitraryArray函数生成数组的一个Arbitrary实例。然而,要要定义数组的Arbitrary实例,我们需要确保数组的每一个元素也是Arbitrary实例。例如,要产生随机数的数组,首先需要确保能够产生随机数。一般的,需要编写类似如下的函数,确保数组的元素也遵循arbitrary协议

extension Array<T: Arbitrary>: Arbitrary { static func arbitrary() -> [T] {... }
}

不幸的是,不可能将这种限定作为一种类型约束表达出来,这使得无法编写扩展 (extension)是的数组遵循Arbitrary协议。因此,我们需要修改check2函数。

check2<A>函数的问题是它要求类型A必须实现Arbitrary协议。我们将要去掉这个约束,取而代之的,需要两个函数,smaller和arbitrary,作为参数传递给该函数。

首先定义一个辅助的结构体,该结构体包含两个我们需要的函数:

struct ArbitraryI<T> {let arbitrary: () -> T let smaller: T -> T?
}

然后编写一个辅助型的函数,以ArbitraryI型的结构体作为参数。checkHelper函数的定义和check2函数类似,唯一的区别是arbitrary函数和smaller函数定义的位置不同。在check2函数中,是通过对通用类型进行约束(通用类型必须实现某个协议)。而checkHelper函数则是显式的使用传递进来的ArbitraryI结构体提供的函数。

func checkHelper<A>(arbitraryInstance: ArbitraryI<A>,prop: A -> Bool, message: String) -> () {for _ in 0..<numberOfIterations {let value = arbitraryInstance.arbitrary() if !prop(value) {let smallerValue = iterateWhile({ !prop($0) }, value, arbitraryInstance.smaller)println("\"\(message)\" doesn't hold: \(smallerValue)")return} }println("\"\(message)\" passed \(numberOfIterations) tests.") 
}

这是一种标准的技术:我们使用参数传递需要的信息,而不是通过协议来实现。这样做更加灵活,我们不在使用swift推断需要的信息,而是全盘掌握这些信息。

现在可以使用checkHelper函数重新定义check2函数了。我们可以将Arbitrary协议中的定义封装到ArbitraryI结构体中并调用checkHelper:

func check<X: Arbitrary>(message: String,prop: X -> Bool) -> () {let instance = ArbitraryI(arbitrary: { X.arbitrary() }, smaller: { $0.smaller() })checkHelper(instance, prop, message) 
}

如果有一个类型,我们无法定义需要的Arbitrary实例,数组就是这样的一个例子。我们可以重载check函数并构造需要的ArbitraryI结构体:

func check<X: Arbitrary>(message: String,prop: [X] -> Bool) -> () {let instance = ArbitraryI(arbitrary: arbitraryArray,smaller: { (x: [X]) in x.smaller() })checkHelper(instance, prop, message) 
}

现在,我们可以运行check来验证QuickSort的实现。回生成大量随机的数组并传递给我们的测试:

check("qsort should behave like sort") { (x: [Int]) in return qsort(x) == x.sorted(<)
}> "qsort should behave like sort" passed 100 tests. 
> ()

使用QuickCheck

有点违反直觉,但是有许多证据表明测试技术可以影响代码的设计。依赖于测试驱动设计(test-driven design)的工程师不仅适用测试验证它们的代码是否正确,而且会要求你使用测试驱动的方式 编写代码,代码的设计也变得简单。这很容易理解-----如果一个类不需要复杂的设置过程,很容易就可以写出测试代码,也意味着该类具有很弱的耦合性。

对QuickCheck来说,也是同样的道理。对已有的代码进行QuickCheck测试是很困难的,特别是那些已经存在的对其他类具有严重依赖的或者使用多种状态的面向对象的架构。然而,如果你使用QuickCheck进行测试驱动开发,你会发现它对代码设计的影响很大。QuickCheck强迫你考虑函数必须满足的抽象属性并允许你给出一个高层次的说明。单元测试可用验证3 + 0是否等于 0 + 3,QuickCheck可以测试出交换律更通用的情况。通过首先考虑高层的QuickCheck说明,你的代码会强调模块性以及引用透明( referential transparency)。QuickCheck很难应用于基于状态的函数或者API。结果,预先使用QuickCheck编写测试代码会让你的代码保持清洁。

下一步

这个库远没有完成,但是已经非常实用了。也就是是,还有许多需要提高的地方:

  • 缩短函数太简单了。例如,在数组的例子中,我们仅仅是去掉数组的第一个元素。然而,我们本来还可以选择移除一个不同元素,或者让数组中的元素更小一些(或者两个都做)。目前的实现返回一个可选的缩短值,然而我们也许希望生成一组值。在后面的章节中,我们将会演示如何生成一组结果,我们就可以在这里使用相同的技术了。
  • Arbitrary实例太简单。对于不同的数据类型,我们希望拥有更复杂的arbitrary实例。比如,当生成随机的枚举类型的值时,我们可以以不同的频率生成特定case的值。我们还可以生成特定约束的值,比如排好序的数组或者非空的数组。当编写多个Arbitrary实例时,定义几个辅助函数以帮助我们编写这些实例时可能的。
  • 对产生的测试数据进行归类。如果我们产生了很多长度为1 的数组,我们可以将其归类为琐碎的测试用例。Haskell库支持分类,因此它关于分类的主意可以直接拿来用
  • 我们可能需要控制产生的随机值的大小,在Haskell版本的QuickCheck中,Arbitrary协议增加了一个size参数,以限定生成的随机输入的大小。check函数首先测试下小的值,与小并快的测试对应。当越来越多的测试通过时,check函数会增加size以便尝试大的,更加复杂的反例。
  • 我们还可以以显式的种子初始化随机生成器,这样就可以重播测试用例。这可以更容易生成失败的用例。

显然,这不是所有可以改进的地方,要将其改进成一个完善的类库,还有许多大大小小的事情可以改进。

函数式编程(Swift)


http://chatgpt.dhexx.cn/article/oTSOfcrU.shtml

相关文章

quickbi使用

在maxcomputer中建表时应该注意四种建表方式不能使用临时查询的方式建立表结构&#xff0c;这种无法提交到生产环境&#xff0c;虽然在数据地图中可以查询到&#xff0c;但没提交生产环境中 暂时不知道这种情况&#xff1a;通过数据地图可以看到测试表通过临时查询简报方式也在…

阴阳师模型提取

0.声明 本文中的开发工具、第三方资源均用于学习用途&#xff0c;而非商业用途。本文中所有资源截止发稿时已全部删除 1.前言 最近一直在玩阴阳师。玩的时候我们不难观察到&#xff0c;现代游戏的制作中&#xff0c;画面、游戏体验都大大改善&#xff0c;除此之外&#xff0…

QUICKBMS 中文指南

我正打算做一个使用Quickbmsqu去解那些没有解包器的文档的教程。6 ?- s/ g/ B8 }, a$ ` 起步会很简单,然后越来越难,直到你学会并且编写你自己的脚本。 我们需要如下的四个工具: 1、16进制编辑器,比如HxD。(我用WINHEX) 2、Quick BMS http://aluigi.org/papers/quickbm…

quick bms的使用

下载工具quick bms&#xff0c;官网&#xff1a;http://aluigi.altervista.org/quickbms.htm 新建脚本 Script File -> New 选择你要操作的文件 Archive File -> Open Archive 选择输出目录 Folders -> Select Out Folder 编写脚本&#xff0c;点Quick Save保存脚…

乘方及方根运算电路

乘方及方根运算电路 图5.4-36是乘方运算电路。由于相乘器相移的影响&#xff0c;高次方运算会带来很大误差&#xff0c;通常串接的相乘器数量不超过2~3个。 图5.4-37是开方运算电路。要使此开方电路能稳定地工作&#xff0c;输入电压U1必须为负值。如果U1为正&#xff0c;将形…

比例运算电路(multisim实现)

目录 1.同相比例运算电路 ​ 输入与输出关系&#xff1a;​ 2.反相比例运算电路 输入与输出关系&#xff1a;​ 1.同相比例运算电路 仿真波形&#xff1a; 输入与输出关系&#xff1a; 2.反相比例运算电路 R2扫描波形 输入与输出关系&#xff1a;

模电总结二:基本运算电路

一、比例运算放大电路 二、加法运算放大电路 三、减法运算电路 四、积分运算电路 五、微分运算电路

大学模电实验合集丨实验六 比例求和运算电路

目录 一、实验目的 二、实验仪器 三、实验要求 五、根据实验报告回答下列问题 一、实验目的 1.掌握用集成运算放大电路组成比例、求和电路的特点及性能。 2.学会上述电路的测试和分析方法。 二、实验仪器 1.示波器 2.信号发生器 3.数字万用表 三、实验要求 在 MultiS…

【模拟电子技术Analog Electronics Technology 24】—— 信号的运算与处理(1):基本的运算电路分析

写在前面&#xff1a;在本篇博文中&#xff0c;我将分析几种常见的基本运算电路&#xff1a;比例运算电路&#xff0c;加减运算电路&#xff0c;微分和积分运算电路&#xff0c;指数和对数运算电路输入与输出的关系。但是&#xff0c;掌握单一的运算电路并不是我们的最终目的&a…

计算机系统基础(三)——运算电路基础

运算电路基础 数字逻辑电路基础运算电路基础数字逻辑电路基础 从C语言表达式到逻辑电路C语言支持的基本数据类型从C表达式到运算类指令从运算类指令到运算电路 C语言中各类运算算术运算按位运算逻辑运算移位运算截断和扩展运算 整数加减运算补码加减运算器无符号整数加法溢出判…

运算放大电路(比例运算电路)

0.反向比例运算电路 这是典型的并联反馈负反馈电路&#xff0c;输入电压U1通过电阻R作用于反向输入端&#xff0c;故输出电压与输入电压反向&#xff0c;同相输入端通过补偿电阻R接地&#xff0c;其值为U10时反相输入端的等效电阻&#xff0c;即各支路电阻的并联&#xff0c;因…

正弦运算电路

在很久之前&#xff0c;我在寻找一个能够进行正弦信号的电路。这个电路的意思不是说DDS正弦信号发生器。而是说&#xff0c;当输入一个信号x时&#xff0c;输出信号ysin⁡(x)&#xff0c;也就是说&#xff0c;输入信号xπ/2 V时&#xff0c;输出y1V&#xff0c;输入信号xπV时&…

模电笔记 基本运算电路

理想运放 “虚断” 理想运放的输入电阻为无穷大&#xff0c;流入两个输入端的电流近似为0 “虚短” 条件&#xff1a;理想运放工作于线性区&#xff08;存在负反馈&#xff09; 虚短&#xff1a;两个输入端的电位近似相等&#xff0c;净输入电压近似为0 比例运算电路 反…

实验二十一、积分运算电路的输出波形分析

一、题目 利用 Multisim 分析图1所示两个积分运算电路的输出波形&#xff0c;输入电压为 200 Hz、幅值为 1 V 的方波信号。 图 1 图1\,\, 图1 二、仿真电路 在 Multism 中搭建图1所示的两个电路&#xff0c;如图2所示。为了防止电路中的直流增益过大&#xff0c;故在电容上…

【反向和同向比例运算电路】

反向和同向比例运算电路 1.1 反相比例电路 1. 基本电路 电压并联负反馈输入端虚短、虚断   特点&#xff1a;   反相端为虚地&#xff0c;所以共模输入可视为0&#xff0c;对运放共模抑制比要求低   输出电阻小&#xff0c;带负载能力强   要求放大倍数较大时&#x…

减法器运算电路公式推导

前言 本人是菜鸟&#xff0c;有错误欢迎斧正&#xff0c;近期在看仪表放大器&#xff0c;其第二级放大电路就是一个减法器&#xff0c;大学教的我也忘的差不多了&#xff0c;于是开始网上冲浪&#xff0c;可是网上都是根据具体电路分析&#xff0c;或者分析的跳跃度很大&#…

数字电路基础(五)算术运算电路

数字电路基础&#xff08;五&#xff09;算术运算电路 一、二进制加法电路 1.半加器和全加器 &#xff08;1&#xff09;半加器 半加器是一个只考虑两个一位二进制数相加&#xff0c;而不考虑低位进位的运算电路。如下图所示是半加器的逻辑图&#xff1a; A A A和 B B B端…

积分运算电路的设计方法详细介绍

本文为大家介绍积分运算电路的设计。 积分运算电路的特性分析 下图为以集成运算放大器为核心元件的基本反相积分运算电路&#xff0c;输入电压uI经电阻R加至运算放大器的反相输入端&#xff0c;C为反馈电容&#xff0c;引入电压并联负反馈&#xff0c;R‘为平衡电阻&#xff…

积分和微分运算电路

目录 前言一、积分运算电路1.一般的积分运算电路2.稳定的积分运算电路 二、微分运算电路1.一般的微分运算电路2.稳定的微分运算电路 结语 前言 本文将分析积分运算电路和微分运算电路&#xff0c;运算电路基于通用型集成运放。文章从输入电阻、放大倍数、对称性、功能性、稳定…

【Multisim仿真】运放电路:反相比例运算电路

【Multisim仿真】运放电路&#xff1a;反相比例运算电路 相关内容《【Proteus仿真】集成运算放大器搭建以及基本功能电路》Multisim仿真演示 示波器设置 双击示波器图标&#xff0c;调节X轴扫描为500S/Di或1ms/DIV&#xff0c;A通道幅度为10mV/Div&#xff1b;B通道幅100mV/Div…