本编是Swifter - Swift 开发者必备 Tips 阅读笔记
一、下标
- 在绝大多数语言中, 使用下标来读写类似数组或者是字典这样的数据结构的做法,似乎已经是业界标准。
- 在 Swift 中,
Array
和Dictionary
当然也实现了下标读写:
var arr = [1,2,3]
arr[2] // 3
arr[2] = 4 // arr = [1,2,4]
var dic = ["cat":"meow", "goat":"mie"]
dic["cat"] // {Some "meow"}
dic["cat"] = "miao" // dic = ["cat":"miao", "goat":"mie"]
- 做为一门代表了先进生产力的语言, Swift是允许我们定义下标的。这不仅包含了对自己写的类型进行下标自定义, 也包括了对那些已经支持下标访问的类型进行扩展。
- 我们来看看向已有类型添加下标访问的情况吧, 比如说
Array
- 我们可以在定义文件中, 找到
Array
已经支持的下标访问类型
subscript(index: Int) -> T
subscript(subRange: Range<Int>) -> Slice<T>
- 我们可以发现, 我们很难一次性取出某几个特定位置的元素, 比如在一个数组内, 我想取出
index
为0
,2
,3
的时候, 现有的体系就会比较吃力。这是我们往往会枚举数组, 然后在循环中判断是否是我们想要的位置 - 在Swift中, 我们有更好的做法, 比如说实现一个接受
数组做为下标
输入的读取方法:
extension Array {
subscript(input: [Int]) -> ArraySlice<Element> {
get {
var result = ArraySlice<Element>()
for i in input {
result.append(self[i])
}
return result
}
set {
for (index, i) in input.enumerated() {
assert(i < self.count, "下标越界")
self[i] = newValue[index]
}
}
}
}
- 我们现在可以同时取出多个位置的元素:
var array = [1, 2, 3, 4, 5, 6] as [Any]
array[[1, 2, 4]] = ["2", "3", "5"]
print(array) // [1, "2", "3", 4, "5", 6]
let result = array[[2, 3, 5]]
print(result) // ["3", 4, 6]
- 当然, 我们可以把传入的数组改为可变参数:
extension Array {
subscript(input: Int...) -> ArraySlice<Element> {
get {
var result = ArraySlice<Element>()
for i in input {
result.append(self[i])
}
return result
}
set {
for (index, i) in input.enumerated() {
assert(i < self.count, "下标越界")
self[i] = newValue[index]
}
}
}
}
注意: 虽然我们在这里实现了下标为数组的版本,但是我并不推荐使用这样的形式。不论从易用性还是可读性上来说,参数列表的形式会更好。但是存在一个问题,那就是在只有一个输入参数的时候参数列表会导致和现有的定义冲突。(参数列表中, 如果只输入一个参数, 会调用系统已经定义的方法, 而不会调用我们上面在分类中实现的方法, 不过语义上有冲突)
二、嵌套函数
- 方法终于成为了一等公民,也就是说,我们可以将方法当作变量或者参数来使用了。更进一步地,我们甚至可以在一个方法中定义新的方法,这给代码结构层次和访问级别的控制带来了新的选择。
- 想想看有多少次我们因为一个方法主体内容过长,而不得不将它重构为好几个小的功能块的方法,然后在原来的主体方法中去调用这些小方法。
- 举个例子,我们在写一个网络请求的类
Request
时,可能面临着将请求的参数编码到url
里的任务。因为输入的参数可能包括单个的值,字典,或者是数组,因此为了结构漂亮和保持方法短小,我们可能将情况分开,写出这样的代码:
func appendQuery(url: String,
key: String,
value: AnyObject) -> String {
if let dictionary = value as? [String: AnyObject] {
return appendQueryDictionary(url, key, dictionary)
} else if let array = value as? [AnyObject] {
return appendQueryArray(url, key, array)
} else {
return appendQuerySingle(url, key, value)
}
}
func appendQueryDictionary(url: String,
key: String,
value: [String: AnyObject]) -> String {
func appendQueryDictionary(url: String,
key: String,
value: [String: AnyObject]) -> String {
//...
return result
}
func appendQueryArray(url: String,
key: String,
value: [AnyObject]) -> String {
//...
return result
}
func appendQuerySingle(url: String,
key: String,
value: AnyObject) -> String {
//...
return result
}
- 这些具体负责一个个小功能块的方法也许一辈子就被调用这么一次,但是却不得不存在于整个类型的作用域中。
- 虽然我们会将它们标记为私有方法,但是事实上它们所承担的任务往往和这个类型没有直接关系,而只是会在这个类型中的某个方法中被用到。
- 更甚至这些小方法也可能有些复杂,我们还想进一步将它们分成更小的模块,我们很可能也只有将它们放到和其他方法平级的地方。
- 这样一来,本来应该是进深的结构,却被整个展平了,导致之后在对代码的理解和维护上都很成问题。
- 在 Swift 中,我们对于这种情况有了很好的应对,我们可以在方法中定义其他方法,也就是说让方法嵌套起来。
- 事实上后三个方法都只会在第一个方法中被调用,它们其实和
Request
没有直接的关系,所以将它们放到appendQuery
中去会是一个更好的组织形式:
func appendQuery(url: String,
key: String,
value: AnyObject) -> String {
func appendQueryDictionary(url: String,
key: String,
value: [String: AnyObject]) -> String {
//...
return result
}
func appendQueryArray(url: String,
key: String,
value: [AnyObject]) -> String {
//...
return result
}
func appendQuerySingle(url: String,
key: String,
value: AnyObject) -> String {
//...
return result
}
if let dictionary = value as? [String: AnyObject] {
return appendQueryDictionary(url, key, dictionary)
} else if let array = value as? [AnyObject] {
return appendQueryArray(url, key, array)
} else {
return appendQuerySingle(url, key, value)
}
}
另一个重要的考虑是虽然 Swift 提供了不同的访问权限,但是有些方法我们完全不希望在其他地方被直接使用。最常见的例子就是在方法的模板中:我们一方面希望灵活地提供一个模板来让使用者可以通过模板定制他们想要的方法,但另一方面又不希望暴露太多实现细节,或者甚至是让使用者可以直接调用到模板。一个最简单的例子, 类似这样的代码:
func makeIncrementor(addNumber: Int) -> ((inout Int) -> Void { func incrementor(inout variable: Int) -> Void { variable += addNumber; } return incrementor; }
三、命名空间
命名空间是用来组织和重用代码的。
- 如同名字一样的意思,NameSpace(名字空间),之所以出来这样一个东西,是因为人类可用的单词数太少,并且不同的人写的程序不可能所有的变量都没有重名现象,对于库来说,这个问题尤其严重,如果两个人写的库文件中出现同名的变量或函数(不可避免),使用起来就有问题了。
- 为了解决这个问题,引入了名字空间这个概念,通过使用 namespace xxx;你所使用的库函数或变量就是在该名字空间中定义的,这样一来就不会引起不必要的冲突了。
- 在 Swift 中,由于可以使用命名空间了,即使是名字相同的类型,只要是来自不同的命名空间的话,都是可以和平共处的。
- 和 C# 这样的显式在文件中指定命名空间的做法不同,Swift 的命名空间是基于 module 而不是在代码中显式地指明,每个 module 代表了 Swift 中的一个命名空间。也就是说,同一个 target 里的类型名称还是不能相同的。
- 在我们进行 app 开发时,默认添加到 app 的主 target 的内容都是处于同一个命名空间中的,我们可以通过创建 Cocoa (Touch) Framework 的 target 的方法来新建一个 module,这样我们就可以在两个不同的 target 中添加同样名字的类型了:
// MyFramework.swift
// 这个文件存在于 MyFramework.framework 中
public class MyClass {
public class func hello() {
print("hello from framework")
}
}
// MyApp.swift
// 这个文件存在于 app 的主 target 中
class MyClass {
class func hello() {
print("hello from app")
}
}
- 在使用时,如果出现可能冲突的时候,我们需要在类型名称前面加上 module 的名字 (也就是 target 的名字):
MyClass.hello()
// hello from app
MyFramework.MyClass.hello()
// hello from framework
- 因为是在 app 的 target 中调用的,所以第一个
MyClass
会直接使用 app 中的版本,第二个调用我们指定了MyFramework
中的版本。 - 另一种策略是使用类型嵌套的方法来指定访问的范围。常见做法是将名字重复的类型定义到不同的 struct 中,以此避免冲突。这样在不使用多个 module 的情况下也能取得隔离同样名字的类型的效果:
struct MyClassContainer1 {
class MyClass {
class func hello() {
print("hello from MyClassContainer1")
}
}
}
struct MyClassContainer2 {
class MyClass {
class func hello() {
print("hello from MyClassContainer2")
}
}
}
- 使用时:
MyClassContainer1.MyClass.hello()
MyClassContainer2.MyClass.hello()
- 其实不管哪种方式都和传统意义上的命名空间有所不同,把它叫做命名空间,更多的是一种概念上的宣传。
- 不过在实际使用中只要遵守这套规则的话,还是能避免很多不必要的麻烦的,至少唾手可得的是我们不再需要给类名加上各种奇怪的前缀了。
获取当前 App 中主 target 的命名空间
import Foundation extension Bundle { class var namespace: String { return Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "" } }
网友评论