百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

Swift 不完全函数(part 1):如何避免

liuian 2025-07-02 22:17 4 浏览

作为我重返 Cocoa with Love 的第一篇正式文章,我想介绍的是“不完全函数”(带前置条件的函数)。

这是一个不寻常的 App 编程博客主题,它超出了 API 设计或者按合同设计的范围,带前置条件的函数并没有在大范围内经过讨论。并不是说我们的函数没有前置条件。而是因为我们习惯于在小范围内测试 App,我们永远不会考虑传递给函数的所有可能值。我们的函数可能包含了许多隐含的条件(包括对更大范围内的程序状态的依赖),我们只是忽略了它们或者没有在文档中加以说明(因此极易出现违反的情况)。

实际上,预处理并避免不完全函数能够让我们的程序无论在如何情况下都能可靠地运行。

目录:

  • 背景: 类型约束 VS 运行时期望

  • 背景: 前置条件

  • 背景: Swift 中的前置条件

  • 不完全函数

  • 隐式不完全函数

  • 不完全函数带来的问题

  • 用完全函数取代不完全函数

  • 构造可以失败,调用不能失败

  • 避免不完全函数的其它方法

  • 不完全函数存在的必要性

  • 结论

背景:类型约束 VS 运行时期望

任何一个函数,都需要满足两个条件:

1. 类型约束:一个函数的参数类型和返回值必须和其签名相符。编译器会强制要求类型约束,确保调用者和函数都符合这个要求。

2. 运行时期望:通过推断,描述函数最终要达到的目标。确保运行时期望符合函数式编程(及测试)的原则。

如果两者发生冲突时会怎样?

让我们来看一个 Int 转换成 Bool 例子(这是类型约束)。与 C 语言中 0 表示 false 而非 0 表示 true 不同,这个函数要严格一点:0 表示 false,1 表示 true(这是运行时期望)。

functoBool(x:Int)->Bool{
ifx==0{
returnfalse
}elseifx==1{
returntrue
}
}

这个函数满足运行时期望:0 转换成 false,1 转换成 true,但会在最后的 } 符号处提示错误:

> Missing return in a function expected to return ‘Bool’

编译器认为我们没有处理每一种 x 值的可能情况,函数有可能跳过两个 if 而不会返回值(违反了类型约束)。

我们可以修改这个函数,让它处理 x 的每种可能取值:

functoBool(x:Int)->Bool{
ifx==0{
returnfalse
}
returntrue
}

但现在,我们将 -1 之类的值转换成 true 了,这样又违反了运行时期望。

这样,类型约束和运行时期望发生了自相矛盾。

背景:前置条件

这种冲突的出现是因为运行时期望中隐含了一个额外的条件,这个条件没有包含在类型约束中。我们把这个额外的条件称之为前置条件。在我们简单的 toBool 函数中,这个前置条件是:x 的值要么是 0 要么是 1。

> 本文主要讨论参数的前置条件(对于简单例子,它更容易理解)。但有很多前置条件实际上是依赖于外部程序状态。比如:要调用这个方法,必须先初始化一个模块,这就是一种前置条件。如果在发起请求之前需要先启动服务器,也是一个前置条件。如果只允许对某个对象的值一次设置一个,那也是一个前置条件。

这个问题说起来简单,但却导致一个问题:编译器不知道前提条件,因此前提条件有可能在运行时被违反。如果前置条件不满足,这个函数该怎么做?

唯一的、安全的做法是触发一个致命错误(中断程序)。

这听起来也不太“安全”,但这是唯一的防止情况变得更严重的做法。如果函数的运行时期望和实际结果不符,这表明依赖于这个函数的所有事情都处于一种不确定的状态。一旦程序处于一种不确定状态,任何部分都会往不正确的方向滑下去,任何动作都会是错的。可能 toBool 函数会用于询问“你想删除磁盘上的所有数据吗?”,也可能是用于决定是否要退出循环,但现在它仍然处于循环之中,正在不停地消耗内存直到计算机崩溃。

我们想让程序停止,这样可以将我们的注意力集中到程序出现错误的地方——而不用强行将我们带到后面的问题中去,导致我们不得不一点点追溯到问题的源头。一个致命错误不但简化了调试步骤,而且让问题一发生就能被捕获。

背景: Swift 中的前置条件

因此我们需要在前置条件不满足的情况下触发一个致命错误。Swift 有一个函数 precondition 就是用来干这个的:测试条件是否满足,并在不满足的情况下触发一个致命错误。我们的函数可以改成:

functoBool(x:Int)->Bool{
precondition(x==0||x==1,"Thisfunctioncanonlyconvert0or1toBool")
ifx==0{
returnfalse
}
/*x==1*/
returntrue
}

> 注:本文大部分地方我会使用 `precodition` 函数,但还有许多类似的能够触发致命错误的函数,包括 assert、assertionFailure、precondition、preconditionFailure、fatalError,或者其它标准库中用于“捕获错误”的内联函数,比如Builtin.int_trap 和 Builtin.condfail。

现在,这段代码看起来有点别扭。我故意让类型约束和运行时期望发生冲突,然后又故意不修改它们任何一个,勉为其难地要用前置条件来解决问题。你可能会想,谁会这样写函数啊,你从来不会像这样来用一个函数。

事实上在 Swift 标准库中,几乎每个 Swift 程序都在间接地使用这种方式,包括了各种和 precodition 函数类似的断言。比如在 Swift 中最常见的 Array 的下标操作(它带有一前置条件:数组下标必须在范围内),又比如 Swift 的 ! 操作(它也有一个前置条件:self 不能为 nil),以及任何隐式解包操作(同样有一个 self 不能为 nil 的前置条件)和默认的整数运算和转换(也会触发致命错误)。

在你的代码中可以使用各种其它前置条件:当某些隐含的、未经检查的条件不能满足时,会导致函数的行为异常或出现不致命的错误。要将每样东西都掌握在手中是非常困难的,但你需要考虑到函数中是否有不明显的、未经检查的条件,然后用一个 precondition 检查来记录该条件,以确保以后你不会违反它。

不完全函数

前置条件会让函数中的某个参数的取值范围缩小为函数签名中指明的范围的一部分。也就是数学中的“不完全函数”。

接下来会介绍一些数学名词。正确使用这些术语是很重要的。接下来的内容很精彩,请敛声屏气以待。

在数学术语中,不完全函数是一种函数,它将一个域(输入值的可能集合)映射到一个陪域(输出值的可能集合),其中(输入域中)一部分值可能未定义(没有被映射)。在不完全函数中,已定义的输入值的子集称作已定义域。而所有输入值都已定义的函数,则叫做完全函数。

不完全函数的例子是除法。一个 5 除以任何实数的函数如下所示:

当 x 等于 0 时,函数未定义,因为在经典数学中,任何数除 0 都是没有意义的。

如果用 Swift 实现这个函数,对于“未定义”的这部分值,我们可以用前置条件(precondition)强制要求函数只对 x 的 "已定义域" 进行计算:

funcdivideFiveBy(x:Real)->Real{
precondition(x!=0)
return5/x
}

隐式的不完全函数

在 Swift 标准库中并没有 Real 类型。我们可以使用 Double 类型,但 Double 不能用在这个地方(请看后面的“改变行为”一节)。不过这里我们可以用 Int 来代替 Real:

funcdivideFiveBy(x:Int)->Int{
return5/x
}

我们的前置条件呢?它仍然存在。我们不需要写 precondition 语句是因为它已经包含在 / 操作符中了。Int 的中缀运算符 / 使用了“经过检查的”除法(在标准库中实现为 _overflowChecked),因此如果进行除 0 运算会引发一个致命的错误。因为在 Swift 中,Int 类型就像数学中的 Real 类型,除 0 都是没有意义的。

下面是另一个不完全函数的例子,当 someArrayIndex 参数等于某些值时,会触发致命错误:

funcsomeArrayFunction(someArrayIndex:Int)->Element{
returnmyArray[someArrayIndex]
}

类似的例子还有下面这个,当实例对象处于某种状态时,会触发致命错误:

structsomeStructWithAnOptionalMember{
varoptionalSomeType:SomeType?
funcaccessor->SomeType{
returnoptionalSomeType!
}
}

不完全函数带来的问题

我怎么知道 Int 的 / 运算符使用了“经过检查的”除法,以及当第二个操作数为 0 时它会导致一个致命错误呢?

唯一的答案是查看文档。Swift 编程语言将除法运算的约束描述为:

> 在 Swift 中,数学运算符默认不进行溢出处理。溢出会被当做错误抛出。

也就是说,如果你在做整数除法时将 0 作为第二个操作数时,这个函数会向标准输出中输出一条错误信息,然后中断程序的执行。

这就让不完全函数的使用极其依赖文档和测试(二者都极易导致疏忽大意):

  • 必须在文档中清晰描述不完全函数的约束

  • 函数的调用者必须阅读和理解该文档

  • 必须在指定的取值范围内对所有可能的值进行广泛测试以保证函数被正确使用

除了满足第一条和第二条(至少会在调试的时候尽量注意),我们还需要注意第三条。

不幸的是:在一个复杂程序中,调试和测试不可能检查出所有可能的问题。调试和测试非常适合用于检验某种既定的场景,但除非你测试得非常彻底,否则使用者总是能够碰到你从来没有测试过的问题。如果你的程序使用了不完全函数,就有可能发生运行时错误。

> 不完全函数的最大风险是在发布版本中。因为它们并不会像测试时一样出现那么多的问题。在测试时,我们总是想尽量早和多地发现问题。在测试代码中我们可以对数组下标进行操作、对Optional类型进行强制解包 ! 以及使用一些好用但恶劣的不完全函数。

用完全函数取代不完全函数

出于以下原因,我们应当避免不完全函数:

  • 它们带有编译器无法校验的约束

  • 它们能通过测试,但如果发布之后数据发生改变,它们还是有可能发生致命错误

说得更清楚一点:并非我们不要检查前置条件。而是当函数中包含前置条件时,我们应当立即对它们进行检验,否则你的程序就会变得“不确定”。

问题在于前置条件的存在。

如果一个函数带有前置条件时,这个函数就是不完全函数。我们应该将函数设计成不带前置条件的完全函数。也就是说,我们需要为每一个可能的输入值映射一个明确的结果。

我们回顾一下 5 除以任何实数的函数。前面我们将它设计为一个不完全函数,对于 x = 5 的情况,我们缺少了定义。我们将它修改成一个完全函数:

用对程序员更友好的方式表达:我们改变了函数的类型签名。我们不再使用任意值的 Real 作为参数,我们定义了一个新类型 X,用它表示除了 0 以外的任意实数。 现在,这个函数已经对 X 中的所有可能取值进行了映射,因此函数就变成了完全函数。

用 Swift 语言表示大致为:

structNonZeroInt{
letvalue:Int
init?(fromInt:Int){
guardfromInt!=0else{returnnil}
value=fromInt
}
}
funcdivideFiveBy(x:NonZeroInt)->Int{
return5/x.value
}

divideFiveBy 函数的运行时约束不存在了,我们用一个新的类型 NonZeroInt 替换了它,NonZeroInt 在编译时即可满足函数的运行时约束。

你应该明白我前面所说的了,你只需要将前置条件看成是类型签名的完整集合的子集就可以了。我们通过定义一个新的类型来取代前置条件,在这个类型中,我们要去掉前置条件中应该排除的那些值。

构造可以失败,调用不能失败

在命令式语言比如 Swift 中,不完全函数是一个不常见的术语,但在函数式语言比如 Haskell 中,这就很常见了。毫无疑问,在 Haskell 中也有许多关于如何避免不完全函数的内容。

在前面的例子中,我们创建了一个新类型 NonZeroInt,但它的构造函数有可能构造不成功(返回了空值)。换句话说,我们将校验的功能从 divideFiveBy 函数中拿开,然后放到别的地方。当然,这带来两个好处:

  • 编译器会强制要求我们要检查 NonZeroInt?(fromInt:) 方法的返回值

  • 我们在构造的时候就检查,而不是在使用的时候才检查

第一点避免函数成为不完全函数,第二点不过是凑数的。

实际上,我们不应当将一个 Int 构造为 NonZeroInt 紧接着传递给 divideFiveBy,相反我们应当完全不使用 Int;NonZeroInt 应当从某个源构造。这个源可能来自于一个 settings 文件,可能来自于使用者输入,有可能来自于网络连接。无论如何,这个值一出现,我们就立马知道它是不是有效。如果无效,我们就可以提示发生了什么问题。相比较于将一个无效的 0 传来传去,一直到传递给 divideFiveBy 函数,却发现早就无法知道这个参数是哪里传来的,这已经是一个巨大的进步了。

假设数据传递的路径是一个管道:如果数据无法通过管道,则在管道的一开始就被拒绝,而不是进到一半的时候才发现无法通过。理论上,只有构造时会出现失败,而调用是则是一个“完全函数”(不可能失败)。

其它防止不完全函数的方法

避免不完全函数包括满足类型约束以及运行时期望。

定义一个新的,更具体的类型,用于将对数据的运行时期望的所有条件都封装起来,是解决这个问题的最好方式。如我所说的,将任何数据放在构造的时候检查,才是处理错误条件的最好时机。

但是,有大量不属于最佳体验的情况:

  • 从算法上很难实现提前对数据约束进行检查

  • 无法在构造时访问要检查的数据的状态信息

  • 无法控制数据管道的早期设计

  • 你会在多处地方构造数据,但只在一个地方使用它,因此在使用的时候改变它要比在构造的时候改变它容易。

幸好我们还有其它办法。

改变返回类型

将不完全函数改变成完全函数的最简单办法是,是修改返回类型的类型签名,让它传递一个失败条件。我们用不着触发致命错误,我们可以将条件传回给调用者,由调用者负责对结果进行处理。

Swift 的 Optional 就是一个很好的例子,我们用 toBool 函数进行说明:

functoBool(x:Int)->Bool?{
switchx{
case0:returnfalse
case1:returntrue
default:nil
}
}

在 Swift 标准库中有一个例子,是字典的下标操作符。和数组下标不同,字典下标操作符返回的是一个 ELement?。也就是说,允许你访问一个不存在的键。

我喜欢对 CollectionType 进行扩展,从而让数组或其它集合类型能够返回 Optional 类型。

extensionCollectionType{
///Returnstheelementatthespecifiedindexiffitiswithinbounds,otherwisenil.
publicfuncat(index:Index)->Generator.Element?{
returnindices.contains(index)?self[index]:nil
}
}

这里,at 这个词来自于某个 C++ 函数,该函数在某个值有值的时候返回值,为空则抛出异常。Swift 的 throws 方法和 C++ 的 throws 类似,但这里通过一个单行语句返回一个Optional值,语法看起来更紧凑,这种方案来自于 Dictionary。

当然我们也可以用 Swift 的错误处理机制。比如:

enumArtithmeticError{caseDivideByZero}
funcdivideFiveBy(x:Int)throws->Int{
switchx{
case0:throwArtithmeticError.DivideByZero
default:return5/x
}
}

在 O-C 中,异常(通常)是值不可恢复的错误(比如不完全函数)。但是在 Swift 中,错误则意味着可以被捕获的对象,实际上它们也必须被捕获。而且,在 Swift 中抛出一个错误只需要提供一个不同的返回类型——除了写法不同,它的语义和返回一个Optional类型类似。

改变行为

根据情况,这可能是指修改运行时期望,让每个输入都能映射到一个有效的输出。在第一个例子里,如果我们对布尔值的定义采用的是 C 语言的方式(任何非 0 值都等于 true),则我们的 toBool 函数根本不需要前置条件。

我们还可以改变 divideFiveBy 函数,让它做一些不完全准确但也可以说得通的事情,这取决于调用的方式:

funcdivideFiveBy(x:Int)->Int{
switchx{
case0:returnInt.max
default:return5/x
}
}

下面模仿 Swift 标准库的 Double 除法:

funcdivideFiveBy(x:Double)->Double{
return5/x
}

和 Int 的版本不同,这个函数是一个完全函数,而不是不完全函数。

如果 x==0,Double 的 / 操作符会返回 Double.infinity (IEEE 754 “正无穷大”) ,这和数学里面的定义不一样,但仍然解决了某些问题。当然,用这种方式去改变行为,可能会掩盖这样一个事实:当前结果是“不正确”的(例如:你更应该处理分母为 0 的情况,而不是将其放大到“正无穷大”)。

将依赖组件放到一起

使用不完全函数有一个常见的理由,你正在使用数据的两个部分,二者需要一致(比如一个数组和一个下标索引),但你将它们单独创建和保存,这就破坏它们的一致性———可能它们是分开创建的,它们的构造函数违反了原来的一致性。

对于需要保持同步的分离数据,我们可以不使用前置条件,而是将所需的数据保存到一个能够满足这个约束的数据结构中。

在下面的通过下标对数组进行索引的方法中,我们让下标无论何时都有效并防止下标越界。

enumAlwaysValidArrayIndexError:ErrorType{caseNoAcceptableIndex}
structAlwaysValidArrayIndex{
//Storetheindex
varindex:Int

//Togetherwiththearray
letarray:Array

//Constructiongivesthefirstindexinthearray(orthrowsifthearrayisempty)
init(firstIndexInArraya:Array)throws{
guard!a.isEmptyelse{throwAlwaysValidArrayIndexError.NoAcceptableIndex}
array=a
index=array.startIndex
}

//Onlyallowtheindextobeadvancedifthere'ssomewheretoadvance
mutatingfuncadvancethrows{
guardarray.count>indexelse{throwAlwaysValidArrayIndexError.NoAcceptableIndex}
index+=1
}

//Wecandeferenceusingtheindexalonesincethearrayisheldinternally
funcelementAtIndex->T{
returnarray[index]
}
}

看起来有点奇怪,但这和 Swift 字符串的 advance 类似。在使用
StringString.CharacterView.Index 时,你需要通过 String.startIndex 来构造一个 Index,而这个 Index 是保存在字符串的内部的 _StringCore 中,这样就可以正确地遍历每一个 Unicode 字符,同时保持 Index 有效。

关于对 Swift 字符串索引方式的一点抱怨

悲催的是,尽管内部保存了 _StringCore,而且也会随时进行校验,当前进到字符串最后一个字符之后的字符时,仍然会导致一个致命错误(而不是优雅地返回一个 nil)。而更糟糕的是:不是通过符串自身来访问字符,而是需要指定一个下标来访问字符。第二个问题使
String.CharacterView.Index 和 String 再次变得不同步了(因为你可以将一个字符串的 Index 用于另一个字符串上),导致潜在的致命错误(下标越界)或者像下面这个例子一样产生无效的 Unicode字符。下面这个例子中,“Unrelated string” 的 Index 被偏移了 1,然后这个偏移被用于访问一个 Emoji 字符串的中字符,从而导致这个偏移在 Emoji 字符串中是无效的。

> 译者注:不同的字符串 advance 的结果是不同的。比如说单字节字符串一次 advance 的结果导致 Index + 1,而 Unicode 字符一次 advance 后的结果导致 Index + 2,因此你用 "Unrelated string" advance 之后的结果去访问 Emoji 字符串当然导致乱码(二者相差 1 )。正确的做法是用 Emoji 字符串 advance 之后的 Index 去访问 Emoji 字符串。

但愿这个问题能在以后的 Swift 标准库中得到解决(哪怕是能够在使用错误的 Indx 进行字符索引时断言失败也行)。

改变设计

最后一种避免不完全函数的方法是不要使用某些常见的设计模式。意思是说:只使用函数库中的完全函数。如果我们的程序中只使用完全函数,则我们的函数很可能也是完全函数。

最简单的例子是在操作数组时,用 for ... in 循环、map 和 filter 函数而不要使用下标索引。还有,尽量用 if let、switch 和 flatMap 代替Optional类型的强制解包,以避免任何潜在的问题。

不完全函数存在的必要性

我曾经说了“不完全函数的坏话”。也介绍了许多避免它们的方法。

但为什么还会有不完全函数存在?这是因为几个原因。当然我不完全认可这些原因。

审美学

使用不完全函数的最大理由是审美原因:接口的设计者实在是不想定义新的类型、返回Optional或者声明一个会抛出异常的函数。

出于这种原因,在 Swift 标准库中存在大量的不完全函数,它们看起来就像古典的 C 运算符函数,它们会在 C 语言可能出现不安全的内存行为的地方进行一些透明的安全检查。包括数组下标操作,
ImplicitlyUnwarppedOptional 和可溢出的算术运算;它们被设计成和它们的 C 语言版本一样,在内部使用了运行时检查。存在一些历史的或社会的原因:人们希望通过数组索引返回不为空的值。人们希望在他们需要的时候能够强制解包Optional类型。人们不想在进行数学计算时考虑溢出问题。

使用前置条件要比返回一个 Optional(或者其他方法)更危险和更容易崩溃,但这就是人类的思考方式。

带简单条件的内部函数

为了减少额外的工作,前置条件包含需要被同步的多个值,或者某个对象的方法以指定顺序调用。对于内部函数——我们是唯一需要遵循和知道这些前置条件的人——根本不值得花功夫去替换前置条件,尤其是这些前置条件既简单又明显,我们确定我们不会违反它。

唯一要明确的是,一旦使用了前置条件,就不应当违反它。

方法重写

如果有一个可重写的方法要求父类做某些事情(比如调用 super),我们经常要依赖于前置条件或者其它类似检查以确保条件满足。

有一个限制是面向对象编程构成接口的方式:子类完全在控制之内,而父类仅仅是接受子类给它的控制。如果父类想在子类中加入一个约束,它只能在事后检查这个约束(即“后置条件”,但技术上仍然是通过前置条件来实现)。

实际上不可能到达的代码路径

真正的需要执行某些代码的条件过于复杂,以至于这些代码基本不可能执行。尤其当我们对函数返回值进行检查时:我们确实想检查所有的错误结果,但我们无法设计出真正抵达这些错误路径的测试案例。与其写一个无法测试的可恢复的代码,我们还不如在这个代码路径上用一个 preconditionFailure 或者 fatalError,以明确说明这个分支不可能执行。

例如,在某些 C 函数中,存在内存分配失败的返回路径。在现代操作系统中,通常不可能出现内存分配失败(在内存分配失败之前,OS 会 kill 掉这个进程),因此编写检查和处理这种情况的代码完全是在浪费我们的时间。

强制性正确

某些情况下接口设计者想随意地让使用者看到一些错误。有一种观点认为,保守性编程将粗心的使用者拒之于门外,实际上鼓励了糟糕的编程,并让使用者不能理解到底为什么出错;相反,我们应当强制让糟糕的程序员去解决他们犯下的错误。

我认为这种观点在 C 语言类的语言中是有用的,C 语言返回的 int 类型的错误经常被使用者忽略,只有致命错误才会引起他们注意。在 Swift 中,我认为这种做法是不恰当的。使用者不可能忽略一个 Swift Optional 或者 throws,他们会通过返回结果中的“无效的参数”知道自己犯错了,就像他们从前置条件失败中知道的一样——事实上,哪怕使用者不知道有前置条件错误存在,但 throws 关键字是必需被处理的,因此一个从来没见过的错误仍然会在运行时得到正确的处理。

逻辑检查

assert 函数通常用于测试“软”后置条件(它返回 false 而不是更严重的 failure)以及其它的编程逻辑。

如果你不加以注意,assert 在 Debug 模式(用 '-Onone' 进行编译)下和 precondition 没任何区别,但在 Release 模式下它没有作用(用 '-O' 进行编译)。这种不同的行为导致的影响比较复杂,但本质上 assert 仍然适用于在 Debug 下的致命的测试条件,因此它在函数中的作用仍然等于不完全函数。

实际上,assert 在前置条件和逻辑测试之间摇摆不定,前者不适合于在 Release 下进行测试(导致不确定的行为),后者只应当在你的测试代码中而不是正常的代码中。

我个人觉得,如果 Release 下当前置条件的计算量太过于繁重时 assert 是个不错的选择。在其他情况下,你应当用 precondition(因为你确实想让条件成立)或者将代码移到测试代码中(这不是一个真正的前置条件,你可以用特定的方式进行校验)。

结论

带有一个或多个前置条件的函数是不完全函数(只有类型签名中指定的值的子集是有效的)。在使用不完全函数时,每个前置条件都代表了一个你可能会犯下的潜在的错误。和类型约束不同(程序员可以在编译时发现这个错误),前置条件错误会在运行时引发一个致命错误。

致命错误是不对的,你可以通过避免不完全函数来避免错误的发生。

不要在不检查前置条件的情况下取消不完全函。因为这会比让程序崩溃更加可怕。不检查前置条件会导致不确定的行为,导致错误传播,可能导致“更糟糕”的情况发生。它还会妨碍我们进行调试,使错误不能被快速找到。如果你的函数有约束条件,请检查它们!

我们可以通过修改设计和消灭前置条件来避免不完全函数。

只有在类型约束允许的值不能满足运行时期望的时候才需要前置条件。如果你修改了类型约束(让输入类型中的每个值都满足运行时期望)或者改变运行时期望(处理类型约束中的每一个值),则可以不需要前置条件。

也可以简单地返回一个 Optional 而不是一个简单值。或者定义一个输入类型,让它在构造时就检查是否满足条件(如果不满足则返回 nil)。使用 Swift 特有的条件语句进行解包操作和错误处理,大量使用这种条件语句的代价非常低,因此不完全函数应该是非常少见的。

尽管如此,不完全函数仍然有存在的必要。有一些很奇特的原因,它们必须存在,而在另外一些场景下,它们使用得频繁。由于在 Swift 标准库中使用了不完全函数,所以在某种程度上,几乎所有的 Swift 程序都在使用不完全函数,因此你需要注意。

因此,你可能会需要创建自己的不完全函数。在下一部分,我将展示通过捕捉 precondition 来测试不完全函数,这样你就可以确认所创建的不完全函数是否和预期一样能够触发致命错误。

相关推荐

软件测试/测试开发丨Pytest 自动化测试框架(五)

公众号搜索:TestingStudio霍格沃兹测试开发的干货都很硬核测试报告在项目中是至关重要的角色,一个好的测试报告:可以体现测试人员的工作量;开发人员可以从测试报告中了解缺陷的情况;测试经理可以...

python爬虫实战之Headers信息校验-Cookie

一、什么是cookie上期我们了解了User-Agent,这期我们来看下如何利用Cookie进行用户模拟登录从而进行网站数据的爬取。首先让我们来了解下什么是Cookie:Cookie指某些网站为了辨别...

软件测试 | 结合Allure生成测试报告

简介测试报告在项目至关重要,测试人员可以在测试报告中体现自己的工作量,开发人员可以从测试报告中了解缺陷的情况,测试经理可以从测试报告中看到测试人员的执行情况及测试用例的覆盖率,项目负责人可以通过测...

使用FUSE挖掘文件上传漏洞(文件上传漏洞工具)

关于FUSEFUSE是一款功能强大的渗透测试安全工具,可以帮助广大研究人员在最短的时间内迅速寻找出目标软件系统中存在的文件上传漏洞。FUSE本质上是一个渗透测试系统,主要功能就是识别无限制可执行文件上...

第42天,我终于意识到,爬虫这条路,真的好艰难

昨天说到学爬虫的最初四行代码,第四行中的print(res.text),我没太懂。为啥最后的输出的结果,不是显示百度网页全部的源代码呢?这个世界上永远不缺好心人。评论区的大神告诉我:因为只包含静态h...

详解Pytest单元测试框架,轻松搞定自动化测试实战

pytest是目前企业里面使用最多、最流行的Python的单元测试框架,那我们今天就使用这个框架来完成一个网易163邮箱登录的自动化实战案例。下面我们先把我们案例需要的工具进行相关的介绍:01pyt...

干货|Python大佬手把手带你破解哔哩哔哩网滑动验证(上篇)

/1前言/有爬虫经验的各位小伙伴都知道,正常我们需要登录才能获取信息的网站,是比较难爬的。原因就是在于,现在各大网站为了反爬,与爬虫机制斗智斗勇,一般的都加入了图片验证码、滑动验证码之类的干扰,让...

Python 爬虫-如何抓取需要登录的网页

本文是Python爬虫系列第四篇,前三篇快速入口:Python爬虫-开启数据世界的钥匙Python爬虫-HTTP协议和网页基础Python爬虫-使用requests和B...

使用Selenium实现微博爬虫:预登录、展开全文、翻页

前言想实现爬微博的自由吗?这里可以实现了!本文可以解决微博预登录、识别“展开全文”并爬取完整数据、翻页设置等问题。一、区分动态爬虫和静态爬虫1、静态网页静态网页是纯粹的HTML,没有后台数据库,不含程...

从零开始学Python——使用Selenium抓取动态网页数据

1.selenium抓取动态网页数据基础介绍1.1什么是AJAX  AJAX(AsynchronouseJavaScriptAndXML:异步JavaScript和XML)通过在后台与服务器进...

PHP自动测试框架Top 10(php单元测试工具)

对于很多PHP开发新手来说,测试自己编写的代码是一个非常棘手的问题。如果出现问题,他们将不知道下一步该怎么做。花费很长的时间调试PHP代码是一个非常不明智的选择,最好的方法就是在编写应用程序代码之前就...

10款最佳PHP自动化测试框架(php 自动化测试)

为什么测试如此重要?PHP开发新手往往不会测试自己编写的代码,我们中的大多数通过不断测试我们刚刚所编写浏览器窗口的新特性和功能来进行检测,但是当事情出现错误的时候我们往往不知道应该做些什么。为我们的代...

自动化运维:Selenium 测试(seleniumbase搭建自动化测试平台)

本文将以Buddy中的Selenium测试流水线示例,来看看自动化测试就是如此简单易用!Selenium是一套用于浏览器测试自动化的工具。使用Buddy专有服务,您可以直接在Buddy中运行Selen...

Selenium自动化测试(selenium自动化测试工具)

Selenium是一系列基于web的自动化测试工具。它提供了一系列测试函数,用于支持Web自动化测试。这些函数非常灵活,它们能够通过多种方式定位界面元素,并可以将预期结果与系统实际表现进行比较。作为一...

技术分享 | Web自动化之Selenium安装

本文节选自霍格沃兹测试开发学社内部教材Web应用程序的验收测试常常涉及一些手工任务,例如打开一个浏览器,并执行一个测试用例中所描述的操作。但是手工执行的任务容易出现人为的错误,也比较费时间。因此,将...