2016-03-23 23:12:46 feng2qing 阅读数 1801

//动态获取命名空间(CFBundleExecutable这个键对应的值就是项目名称,也就是命名空间)

let nameSpace = NSBundle.mainBundle().infoDictionary!["CFBundleExecutable"] as! String


//将字符串转化为类

//默认情况下,命名空间就是项目名称,但是命名空间是可以修改的

let cls:AnyClass? = NSClassFromString(nameSpace + "." + viewControllerName)


//通过类创建对象

//anyClass转换为指定的类型

let viewControllerCls = cls as! UIViewController.Type


//通过class创建对象

let vc = viewControllerCls.init()

2015-06-17 07:28:38 broccoliii 阅读数 0
Any:
Any是一个空协议集合的别名,它表示没有实现任何协议,因此它可以是任何类型,包括类实例与结构体实例。
Any是一个别名。

/// 一个任何类型都遵守的协议类型

typealias Any = protocol<>


AnyObject:

AnyObject是一个成员为空的协议,任何对象都实现了这个协议。

看一下AnyObject的定义:

@objc protocol AnyObject {

}


AnyClass:

AnyClassAnyObject.Type的别名而已。

看一AnyClass的定义:

typealias AnyClass = AnyObject.Type


@objc是一个声明特性,该特性用于修饰任意可以在ObjC中访问或使用的声明,比如非嵌套类,协议,类和协议中的属性和方法,初始化器,析构器,以及下标。如果你将objc特性应用于一个类或协议,它也会隐式地应用于那个类或协议的成员。对于标记了objc特性的类,编译器会隐式地为它的子类添加objc特性。


AnyObject使用@objc声明特性修饰,是因为它要做为ObjcSwift混编的使者,两者传递变量很多类型便是AnyObjct

不同于AnyAnyObject是一个没有声明任何成员的空协议,是Any表示一个空的协议集合。


总结:

AnyObject是一个协议,Any是零个协议!
AnyObject用于任何类实例,而Any用于任何变量。

看一下AnyObject的定义:

@objc protocol AnyObject {

}

看一AnyClass的定义:

typealias AnyClass = AnyObject.Type

AnyClass-它确实仅是一个别名而已。Any也是别名,是protocol<>的别名。

2015-10-16 16:00:13 woaifen3344 阅读数 12916

原文出处:Swift通过类名动态创建对象的方式

前言

最近一些朋友问到我在Swift中如何通过类字符串名称的方式创建类实例的问题,起初以为与Objective-C的差不多吧,事实上还是有很大的差别的。下面是帮助朋友们之后,也随便总结而写下的文章。

注意:本篇文章中所涉及到的Swift代码都是Swift2.0的语法。

先看ObjC中的方式

我们可以通过Class类型就可以调用alloc来分配内存,调用init方法来初始化。如:

Class cl = NSClassFromString(@"ViewController");
UIViewController *vc = [[cl alloc] init];

通常我们这么写法是用于循环创建的场景,通过公共基类接收,就可以指向所创建的对应的类名称的内存。

Swift中的方式

今天是由于一位朋友突然询问我这么一个问题:

swift中怎么通过类名称创建对象呢?

一时并无法回答,因为一看到Swift中NSClassFromString返回的是AnyClass类型,而这个AnyClass类型为public typealias AnyClass = AnyObject.Type,这个Type具体是什么呢?为什么option+点击进不去,无法查看呢?AnyObject其实只是一个空协议,难道.Type是自动有的吗?这个本人也不清楚。

看看下面的方式:

var str = NSString.self()// 或者NSString.self.init()

str = "TestNSString"
print(str)

下面我们定义一个类:

class MyClass: NSObject {
 var member = 10

 override required init() {
   print("init")
 }
}

我们定义的类是继承于NSObject,这时我们这么测试:

// 打印出10
print( MyClass.self().member)

说明继承于NSObject后, 调用self()就可以创建对象了。

另外,我们使用NSClassFromString来创建试试:

 let className = NSStringFromClass(MyClass)
 print(className)
 let classType = NSClassFromString(className) as? MyClass.Type
 if let type = classType {
   let my = type.init()
   print(my.member)
 }

第一个打印就打印出__lldb_expr_83.MyClass

第二个打印打印出10。type为MyClass.Type,通过MyClass.Type.init()是可以创建类对象的。

我们还可以通过MyClass.self.init()来创建对象,print(MyClass.self.init().member)打印出来也是10

注意:所创建的MyClass类中的init方法前面必须是required的,因为这么创建方式是使用meta type来创建的,如果不添加required,编译时就会报错。当我们修改继承方式,把NSObject改成AnyObject,其结果也一样,AnyObject只是协议,遵守协议。说明与继承方式无关。

总结:

在Swift中,要创建对象有以下几种方式:

  • 1、NSString.self()// 或者NSString.self.init()
  • 2、let myClass = MyClass.Type.init()
  • 3、let myClass = MyClass.self.init()
  • 4、let type = NSClassFromString("MyClass") as! MyClass.Type然后通过type.init()来创建对象

参考:[http://stackoverflow.com/questions/24049673/swift-class-introspection-generics
](http://stackoverflow.com/questions/24049673/swift-class-introspection-generics
)

此处省略10000字…请移步微信公众号阅读全文

公众号搜索「iOS开发技术分享」快速关注微信号:iOSDevShares

QQ群:324400294

image

2016-03-22 15:38:00 weixin_30448603 阅读数 0

前言:

在一些大型的项目中常常在加载页面的时候根据服务器请求下来的数据动态创建视图控制器对象,最近的一个项目就有这一方面的需求,加载页面之前需要先请求服务器的数据得到一个JSON字典,根据里面字符串的名字创建对应的控制器,当然项目工程中有预留对应的控制器。之前在OC中利用字符串动态创建对象可以利用方法NSClassFromString获取到对应的类。在swift中获取类关键步骤是通过这个方法,但是跟OC有了很大的不同,多了一个命名空间.

 

 创建一个对象,将对象打印出来之后在控制台可以看到类名之前多了一串字符串,该字符串就是命名空间。

swift动态创建对象:

为此要动态创建对象的时候需要先获取命名空间,步骤如下:

 1         //动态获取命名空间:
 2         let nameSpace = NSBundle.mainBundle().infoDictionary!["CFBundleExecutable"] as! String
 3         //根据命名空间和传过来的控制器名字获取控制器的类
 4         let controllerClass:AnyClass = NSClassFromString(nameSpace + "." + controllerName)!
 5         
 6         //告诉编译器真实的控制器类型,比如这个控制器本质是UITableViewController则:
 7         let realClass = controllerClass as! UITableViewController.Type
 8         
 9         //实例化这个控制器出来
10         let childController = realClass.init()

 

转载于:https://www.cnblogs.com/develop-SZT/p/5306756.html

2017-01-05 10:05:13 kmyhy 阅读数 2211

原文:Building a Custom Collection in Swift
作者:Eric Cerney
译者:kmyhy

数组、字典和集合是常见的集合类型,它们都内置在 Swift 标准库中。但如果它们不能满足你的 App 的需要的时候怎么办?

一种最常见的办法是使用 Array 或 Dictionary,然后用一堆业务逻辑去保存你的数据结构。但这种方式太过于直接且难于维护。

这样,创建自定义集合类型就变得有意义了。在本文,你将学习用 Swift 的 collection 协议创建自定义集合类型。

当文本结束,你会拥有一个强大的自定义集合类型,拥有 Swift 内置集合的所有功能。

注:本文用 Swift 3.0。小于次的版本无法编译,因为 Swift 标准库发生了剧烈改变。

开始

在本文中,你将从头开始创建一个“多集合”(Bag)类型。

一个 Bag 对象就像一个 Set,用于存储不会重复的对象。在一个 Set 集合中,重复对象会被忽略。在一个 Bag 中,每个对象都会被算进去。

一个好例子是购物清单。你拥有一个清单,每个商品都和一个数量关联。如果添加了重复的商品,则我们会增加已有商品的数量,而不是重新插入一条商品记录。

在介绍 collection 协议前,首先来实现一个基本的 Bag。
创建一个新的 Playground:在 Xcode 中,选择 File\New\Playground… 然后给 playground 命名为 Bag。 你可以选择任何平台,因为本教程是和平台无关的,你只需要选择 Swift 语言就可以了。
点击 Next,选择一个地方保存 playground,然后点 Create。

编辑 playground 文件为:

struct Bag<Element: Hashable> {

}

Bag 结构是一个泛型结构,需要元素类型必须是 Hashable 的。Hashable 允许你对元素进行比较,在 0(1)时间复杂度上只存储唯一值。也就是说,无论内容有多复杂,Bag 存取速度相同。你通过定义一个结构体,强制让它具备值语义(C++ 术语),这就和 Swift 标准库保持一致了。

然后为 Bag 添加属性:

// 1
fileprivate var contents: [Element: Int] = [:]

// 2
var uniqueCount: Int {
  return contents.count
}

// 3
var totalCount: Int {
  return contents.values.reduce(0) { $0 + $1 }
}

这是 Bag 的基本属性:

  • 用一个作为内部存储结构。因为字典的键是唯一的,你可以用它来存储数据。字典的值则表示每个元素的个数。fileprivate 关键字表示这个属性是隐藏的,在外部不可访问。
  • uniqueCount 返回了字典的每一种对象的统计值,并不累加它们每一个的数量。例如,一个 Bag 中有 4 个橙子和 2 个苹果只会返回 2。
  • totalCount 返回的是 Bag 中所有对象的总计。以同一个例子为例,totalCount 返回值为 6。

现在,需要几个方法以便增减 Bag 中的内容。在属性声明下面加入:

// 1
mutating func add(_ member: Element, occurrences: Int = 1) {
  // 2
  precondition(occurrences > 0, "Can only add a positive number of occurrences")

  // 3
  if let currentCount = contents[member] {
    contents[member] = currentCount + occurrences
  } else {
    contents[member] = occurrences
  }
}

代码解释如下:

  • add(_:occurrences:) 方法提供了增加元素的方法。它需要两个参数:泛型参数 Element 和一个 Optional 的元素个数。如果 Bag 实例以常量形式 let 定义而不是变量 var 形式定义的话,则这个方法无效。
  • precondition(_:_:)方法的第一个参数是一个 Boolean 表达式,如果为 false,则程序会中断,并在 Debug 窗口输出第二个参数的内容。这个方法有一个前置条件,以保证 Bag 能够被正确使用。这个方法检查了调用 add 方法时符合我们的预设。
  • if 语句判断元素是否已经存在,如果存在,则累加它的计数器,否则加入新的元素。

另外,你还需要一个删除元素的方法。在 add 方法后新增方法:

mutating func remove(_ member: Element, occurrences: Int = 1) {
  // 1
  guard let currentCount = contents[member], currentCount >= occurrences else {
    preconditionFailure("Removed non-existent elements")
  }

  // 2
  precondition(occurrences > 0, "Can only remove a positive number of occurrences")

  // 3
  if currentCount > occurrences {
    contents[member] = currentCount - occurrences
  } else {
    contents.removeValue(forKey: member)
  }
}

remove(_:occurrences:) 方法使用的参数和 add 方法一模一样,只不过做了相反的事情:

  • 首先判断元素是否存在,确保在删除它时起码有足够的数目能够被删除。
  • 然后保证元素个数大于 0.
  • 最后,如果元素存在则将元素个数减少。如果元素个数减少后小于等于 0,直接删除整个元素。

这里,Bag 还不能干更多的事情,甚至无法访问它的内容。你还不能使用 Dictionary 中存在的高阶方法。

但亡羊补牢为时未晚。我们开始在 Bag 中一一添加这些代码。现在的任务是保持你的代码整洁。

请先等一下!Swift 提供了让 Bag 符合传统集合的所有工具。
你需要先了解一下在 Swift 中,让一个对象变成集合需要做些什么。

自定义集合需要做些什么?

要理解什么是 Swift 集合,首先需要它继承的协议层次:

Sequence 协议表示类型支持排序、以迭代的方式访问其元素。你可以把一个 Sequence 对象视作一个元素的列表,允许你挨个挨个地访问其中的元素。

迭代(Iteration)是一个简单概念,但它能给你的对象提供许多功能。它允许你各种强大的操作比如:

  • map(_:): 用一个闭包将 Sequence 中的每个元素挨个进行转换,并构成另一个数组返回。
  • filter(_:): 用一个闭包过滤所需的元素,将符合闭包谓词所指定条件的元素放到新数组中返回。
  • reduce(_:_:): 用一个闭包将 Sequence 中的所有元素合并成一个值返回。
  • sorted(by:): 根据指定的闭包谓词,将 Sequence 中的元素进行排序并返回排序后的数组。

这只是其中很少的一部分功能。要查看 Sequence 中提供的所有方法,请查看 Sequence 的文档

需要说明的一点是,采用 Sequence 协议的类型强制要求是破坏性的或者是非破坏性的。这意味着,在迭代之后,无法保证下一次迭代会从头开始。

这是一个大问题,如果你的数据准备迭代不止一次的话。要实现非破坏性的迭代,你的对象需要使用 Collection 协议。

Collection 协议继承了 Sequence 和 Indexable 协议。Collection 和 Sequence 的主要区别是,你可以迭代多次,而且可以用索引来访问。

实现 Collection 协议之后,你会获得更多“免费”的方法和属性,例如:

  • isEmpty: 返回一个布尔值,表示集合是否为空。
  • first: 返回集合中的第一个元素。
  • count: 返回集合中的元素个数。

依据集合中的元素类型的不同,你还可能拥有更多的方法和属性。如果你想了解更多,请查看Collection 的文档

在实现这些协议之前,Bag 还有一个地方需要改进。

打印对象

当前,Bag 对象可以用 print(_:) 方法或在 Result Sidebar 视图中暴露出的信息很少。
在 Playground 中加入如下代码:

var shoppingCart = Bag<String>()
shoppingCart.add("Banana")
shoppingCart.add("Orange", occurrences: 2)
shoppingCart.add("Banana")
shoppingCart.remove("Orange")

这里创建了一个 Bag 对象,并加入了几种水果。如果你查看Playground 的调试窗口,你会看到这些对象的类型信息而不是它保存的内容。

你可以用 Swift 标准库中的一个协议来解决这个问题。在 shoppingCart 变量上面的 Bag 类型定义结束的 } 之后添加:

extension Bag: CustomStringConvertible {
  var description: String {
    return String(describing: contents)
  }
}

采用 CustomStringConvertible 协议需要实现一个属性,叫做 description。这个属性返回一个实例对象的文字表示。

在这里,你可以放入任何足以表示你的数据的逻辑。因为字典已经继承了这个协议,你可以简单调用 contents 对象的 description 值。

看一眼 shopingCart 的 debug 信息:

漂亮!现在你已经为 Bag 添加了功能,你可以对它的 contents 进行校验了。

在 Playground 中编写代码时,你可以使用 precondition(_:_:) 来检验返回结果。这会避免你突然破坏之前编写的功能。可以用这个工具作为你的单元测试——将它放到你的日常编码中去做是一个不错的主意!

在最后一次调用 remove(_:occurrences:) 之后加入:

precondition("\(shoppingCart)" == "\(shoppingCart.contents)", "Expected bag description to match its contents description")

如果 shoppingCart 的 description 属性不等于 contents 的 description,则会导致一个错误。

为了创建我们屌爆了的集合类型,接下来的步骤自然就是初始化。

初始化

每次只能加一个元素真的很烦。通常的办法是在初始化的时候用另一个集合来进行初始化。

这正是我们希望 Bag 能够做到的。在 Playground 的最后加入:

let dataArray = ["Banana", "Orange", "Banana"]
let dataDictionary = ["Banana": 2, "Orange": 1]
let dataSet: Set = ["Banana", "Orange", "Banana"]

var arrayBag = Bag(dataArray)
precondition(arrayBag.contents == dataDictionary, "Expected arrayBag contents to match \(dataDictionary)")

var dictionaryBag = Bag(dataDictionary) 
precondition(dictionaryBag.contents == dataDictionary, "Expected dictionaryBag contents to match \(dataDictionary)")

var setBag = Bag(dataSet)
precondition(setBag.contents == ["Banana": 1, "Orange": 1], "Expected setBag contents to match \(["Banana": 1, "Orange": 1])")

无法进行编译,因为还没有定义针对这些类型的初始化方法。不要为每种类型创建一种初始化方法,你可以使用泛型。
在 Bag 定义的 totalCount 后面添加:

// 1
init() { }

// 2
init<S: Sequence>(_ sequence: S) where S.Iterator.Element == Element {
  for element in sequence {
    add(element)
  }
}

// 3
init<S: Sequence>(_ sequence: S) where S.Iterator.Element == (key: Element, value: Int) {
  for (element, count) in sequence {
    add(element, occurrences: count)
  }
}

代码解释如下:

  • 首先,创建一个空的 init 方法。在定义了其他初始化方法之后,你必须添加这个方法,否则编译器会报错。
  • 然后,定义一个初始化方法并接受一个元素为 Sequence 集合的参数。sequence 参数的类型必须和元素类型匹配。例如,Array 和 Set 对象。然后,对 sequence 进行迭代,挨个添加元素。
  • 最后一个方法类似,但元素类型变成元素了(Element, Int)。这种情况最典型的例子就是字典。 这里,你依然对 sequence 中的元素进行迭代并以指定的个数来添加元素。

这些泛型初始化方法为 Bag 对象添加了大量的数据源。但是,它们仍然你初始化另一个 Sequence 对象然后传递给 Bag。

为了避免这个,Swift 标准库提供了两个协议。这两个协议支持以 Sequence 的写法进行初始化。这种写法能让你用更简短的方式定义数据,而不必显式地创建一个对象。

在 Playground 最后加入下列代码,来看看如何使用这种写法:

var arrayLiteralBag: Bag = ["Banana", "Orange", "Banana"]
precondition(arrayLiteralBag.contents == dataDictionary, "Expected arrayLiteralBag contents to match \(dataDictionary)")

var dictionaryLiteralBag: Bag = ["Banana": 2, "Orange": 1]
precondition(dictionaryLiteralBag.contents == dataDictionary, "Expected dictionaryLiteralBag contents to match \(dataDictionary)")

没说的,编译器报错了,我们后面再来解决。这种写法用初始化数组和字典的写法来进行初始化,而不需要创建对象。

在 Bag 的其它扩展之后定义两个扩展:

extension Bag: ExpressibleByArrayLiteral {
  init(arrayLiteral elements: Element...) {
    self.init(elements)
  }
}

extension Bag: ExpressibleByDictionaryLiteral {
  init(dictionaryLiteral elements: (Element, Int)...) {
    // The map converts elements to the "named" tuple the initializer expects.
    self.init(elements.map { (key: $0.0, value: $0.1) })
  }
}

ExpressibleByArrayLiteral 和 ExpressibleByDictionaryLiteral 扩展需要实现一个初始化方法,以处理它们对应的参数的那种写法。由于前面已经定义的初始化方法,它们的实现都非常简单。

现在 Bag 已经非常像原生的集合类型了,我们该来点猛货了。

Sequence

集合类型的最常用的操作是对其元素进行迭代。来看一个例子,在 Playground 最后添加如下代码:

for element in shoppingCart {
  print(element)
}

超级简单。就像数组和字典一样,你可以遍历一个 Bag 对象。因为 Bag 还没有实现 Sequence 协议,编译不能通过。
在 ExpressibleByDictionaryLiteral 扩展后加入另一个扩展:

extension Bag: Sequence {
  // 1
  typealias Iterator = DictionaryIterator<Element, Int>

  // 2
  func makeIterator() -> Iterator {
    // 3
    return contents.makeIterator()
  }
}

不需要实现太多方法。代码解释如下:

  • 定义了一个类型别名,叫做 Iterator,这是 Sequence 中指定的,需要实现 IteratorProtocol 协议。DictionaryIterator 类型是字典用于迭代其元素的。你可以用它,因为 Bag 在底层使用了字典来存储数据的。
  • makeIterator() 方法返回一个 Iterator,它会用来对序列中的每个元素进行迭代。
  • 调用 contents 的 makeIterator() 方法创建一个 Iterator,它已经实现了 Sequence 协议。

这就是 Bag 对 Sequence 协议进行实现的全部。
你现在可以迭代 Bag 对象中的每个元素了,并可以获得每个对象的个数。在之前的 for-in 循环后加入:

for (element, count) in shoppingCart {
  print("Element: \(element), Count: \(count)")
}

打开 Debug 视图,你会看到每个元素都打印出来了:

实现 Sequence 协议之后,你就可以使用许多 Sequence 中有的方法了。

在 Playground 最后加入代码试一试:

// 查找所有数目大于 1 的对象
let moreThanOne = shoppingCart.filter { $0.1 > 1 }
moreThanOne
precondition(moreThanOne.first!.key == "Banana" && moreThanOne.first!.value == 2, "Expected moreThanOne contents to be [(\"Banana\", 2)]")

// 获取所有对象的数组(不需要数量)
let itemList = shoppingCart.map { $0.0 }
itemList
precondition(itemList == ["Orange", "Banana"], "Expected itemList contents to be [\"Orange\", \"Banana\"]")

// 获得所有对象的加总数据
let numberOfItems = shoppingCart.reduce(0) { $0 + $1.1 }
numberOfItems
precondition(numberOfItems == 3, "Expected numberOfItems contents to be 3")

// 所有商品按照数量降序排序
let sorted = shoppingCart.sorted { $0.0 < $1.0 }
sorted
precondition(sorted.first!.key == "Banana" && moreThanOne.first!.value == 2, "Expected sorted contents to be [(\"Banana\", 2), (\"Orange\", 1)]")

所有 Sequence 对象能有的方法都能用——它们完全是免费的。

现在,你可能满足于以这种方式使用 Bag,但还有比这更好玩的吗?当前的 Squence 实现仍然还有改进的余地。

加强版的 Sequence

当前,你依赖于字典为你提供底层支持。这很好,对你来说,这不乏为一种轻松实现功能强大的集合的方式。问题是,它会让 Bag 的用户感到奇怪和困惑。

例如,Bag 会返回一个 DictionaryIterator 类型的 Iterator 好像不妥。你可以创建自己的 Iterator 类型,但这次不是免费的了。

Swift 提供了一个 AnyIterator 类型,将底层的 itertator 隐藏起来。
将 Sequence 的实现修改为:

extension Bag: Sequence {
  // 1
  typealias Iterator = AnyIterator<(element: Element, count: Int)>

  func makeIterator() -> Iterator {
    // 2
    var iterator = contents.makeIterator()

    // 3
    return AnyIterator {
      return iterator.next()
    }
  }
}

Playground 报了一堆错,等会来解决。除了使用了一个 AnyIterator 外,这个实现和之前没有太大区别:

  1. AnyIterator 是一个无类型的 Iterator,它只暴露出底层实际的 Iterator 的 next() 方法给你。这样你就可以将实际使用的 Iterator 类型隐藏起来。
  2. 跟前面一样,通过 contents 创建了一个新的 DictionaryIterator 实例。
  3. 最后,将 Iterator 包装成 AnyIterator 以传递其 next() 方法。

现在来解决先前的报错。你看到的是这两个错误:

前面,你用 DictionaryIterator 的元组命名为 key 和 value。现在你已经将 DictionaryIterator 隐藏起来了,并且将元组的名字修改为 element 和 count。要解决这个错误,将 key 和 value 替换成 element 和 count。

这样你的 precondition 语句的问题就解决了。这就是前置条件的好处了,它能保证某些东西不会被意外修改。

现在,任何人都不知道你在用字典进行所有的工作。
现在的 Bag 让你感觉更好了吧?是时候让它回家了。呃,将你激动的心情收起来吧,它是属于集合的!

Collectdion

闲话少说,接下来还有一道大菜,创建一个集合……即 Collection 协议!再次声明,集合是能够通过索引进行访问并进行多次非破坏性迭代的结合。

为了符合 Collection 协议,你需要提供这些数据:

  1. startIndex 和 endIndex: 指定集合的边界,并说明遍历的起点。
  2. subscript (position:): 允许你通过索引找到集合中的任意元素。这个访问方法的时间复杂度应控制在 0(1) 上。
  3. index(after:): 返回传入的索引的下一个索引。

使用 Cellection 协议时只需要这 4 个数据。在 Sequence 扩展后新增扩展:

extension Bag: Collection {
  // 1
  typealias Index = DictionaryIndex<Element, Int>

  // 2
  var startIndex: Index {
    return contents.startIndex
  }

  var endIndex: Index {
    return contents.endIndex
  }

  // 3
  subscript (position: Index) -> Iterator.Element {
    precondition(indices.contains(position), "out of bounds")
    let dictionaryElement = contents[position]
    return (element: dictionaryElement.key, count: dictionaryElement.value)
  }

  // 4
  func index(after i: Index) -> Index {
    return contents.index(after: i)
  }
}

代码非常简单:

  1. 首先用 DictionaryIndex 来声明一个 Index 类型,DictionaryIndex 在 Collection 协议中已定义。注意,其实编译器会基于你后面的实现来推断这个类型,但为了保持代码的清晰和可维护性,我们显示地指定了类型。
  2. 然后用 contents 来返回第一个索引和最后一个索引。
  3. 用一个前置条件来强制校验索引的有效性。然后,以元组的方式返回 contents 中位于该索引的元素。
  4. 最后,调用 contents.index(after:) 方法并返回结果。

通过添加几个属性和方法,你创建了一个功能完整的集合!在 Playground 最后用几行代码来测试这些功能:

// 读取 Bag 中的第一个对象
let firstItem = shoppingCart.first
precondition(firstItem!.element == "Orange" && firstItem!.count == 1, "Expected first item of shopping cart to be (\"Orange\", 1)")

// 判断 Bag 是否为空
let isEmpty = shoppingCart.isEmpty
precondition(isEmpty == false, "Expected shopping cart to not be empty")

// 获取 Bag 中的商品种类数
let uniqueItems = shoppingCart.count
precondition(uniqueItems == 2, "Expected shoppingCart to have 2 unique items")

// 查找第一个名为 Banana 的元素
let bananaIndex = shoppingCart.indices.first { shoppingCart[$0].element == "Banana" }!
let banana = shoppingCart[bananaIndex]
precondition(banana.element == "Banana" && banana.count == 2, "Expected banana to have value (\"Banana\", 2)")

漂亮!(当你对自己所做的一切感到心满意足时,“等等,你还可以做得更好”这句话又在等着你了……)

是的,你说的没错!你可以做得更好。仍然能够从 Bag 中看出一丝字典的模样。

加强版的 Collection

Bag 暴露了太多底层实现细节。Bag 的用户仍然需要使用 DictionaryIndex 对象去访问集合中的元素。
这个很好搞定。在 Collection 扩展后面增加:

// 1
struct BagIndex<Element: Hashable> {
  // 2
  fileprivate let index: DictionaryIndex<Element, Int>

  // 3
  fileprivate init(_ dictionaryIndex: DictionaryIndex<Element, Int>) {
    self.index = dictionaryIndex
  }
}

没有任何新奇的玩意儿,但我们还是来过一下吧:

  1. 定义了一个泛型类型 BagIndex,和 Bag 一样,为了访问字典对象,它需要一个 Hashable 的泛型参数。
  2. index 类型的真实类型是一个私有的 DictionaryIndex 对象。BagIndex 仅仅是一个封装,隐藏了它真正的 index 类型。
  3. 最后,创建一个私有的初始化方法,接收一个 DictionaryIndex 参数。

Collection 对象需要索引对象实现 Comparable 协议,能够对两个索引进行比较以进行某些操作。因此,BagIndex 必须实现 Comparable 协议。在 BagIndex 扩展之后添加:

extension BagIndex: Comparable {
  static func == (lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index == rhs.index
  }

  static func < (lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index < rhs.index
  }
}

这个逻辑非常简单;方法返回的结果直接调用 DictionaryIndex 已实现的 Comparable 协议的相同方法。

现在将 Bag 修改为使用 BagIndex。将 Collecdtion 扩展替换成:

extension Bag: Collection {
  // 1
  typealias Index = BagIndex<Element>

  var startIndex: Index {
    // 2.1
    return BagIndex(contents.startIndex)
  }

  var endIndex: Index {
    // 2.2
    return BagIndex(contents.endIndex)
  }

  subscript (position: Index) -> Iterator.Element {
    precondition((startIndex ..< endIndex).contains(position), "out of bounds")
    // 3
    let dictionaryElement = contents[position.index]
    return (element: dictionaryElement.key, count: dictionaryElement.value)
  }

  func index(after i: Index) -> Index {
    // 4
    return Index(contents.index(after: i.index))
  }
}

注释中的数字标出了修改之处。分别解释如下:

  1. 首先将 Index 的类型从 DictionaryIndex 修改为 BagIndex。
  2. 然后,是 startIndex 和 endIndex,你换成新的 BagIndex。
  3. 接着,用这个 BagIndex 从 contents 后访问对应元素并返回。
  4. 最后,结合上面几个步骤。从 contents 中获取 DictionaryIndex,然后用它来创建一个 BagIndex。

就这样!用户不会知道你底层是用什么来存储数据的了。你未来还有可能对索引对象的获得更大的控制。

在完成之前,还有一个很重要的地方。除了基于索引来访问元素,你还以通过一段连续索引来访问集合中的值。

要实现这个,你可以看一下集合中是如何进行切片操作的。

切片

切片就是查看集合中多个连续的元素。它允许你在集合元素的子集上进行某些操作,而不用复制这些元素。
切片只会保存原来集合中的元素的引用。它还存储了元素子集的起、始索引。切片的时间复杂度为 0(1),因为它们直接引用了它的原始集合。

切片直接重用原始集合的索引,这使得它们尤其有用。
要测试切片操作,在 Playground 最后添加代码:

// 1
let fruitBasket = Bag(dictionaryLiteral: ("Apple", 5), ("Orange", 2), ("Pear", 3), ("Banana", 7))

// 2
let fruitSlice = fruitBasket.dropFirst() // No pun intended ;]

// 3
if let fruitMinIndex = fruitSlice.indices.min(by: { fruitSlice[$0] > fruitSlice[$1] }) {
  // 4
  let minFruitFromSlice = fruitSlice[fruitMinIndex]
  let minFruitFromBasket = fruitBasket[fruitMinIndex]
}

我们来看一下这些代码做了些什么,以及它们的意思:

  1. 首先,创建一个由 4 种水果构成的水果篮。
  2. 然后拿走第一种水果。这会创建一个水果篮的切片,但第一种水果不见了。
  3. 通过切片找到数量最少的水果的索引。
  4. 尽管上一步的索引是从切片中得到的,你可以把这个索引同时用在原来的集合和后面的切片中来访问对象。

注意:切片对于基于哈希的字典和 Bag 来说,用处不大,因为它们的顺序在任何方向上都是不确定的。而数组则相反,数组是集合中的一个极端例子,在执行顺序操作时,切片扮演很重要的角色。

祝贺你——你现在是一个集合方面的专家了!你可以创建任意内容的 Bag 对象来表示庆祝。

结束

完整的 Playground 代码可以在这里下载。如果你想了解或者实现更完整的 Bag,请从 github 中 checkout 项目。

在这篇文章里,你学习了在 Swift 中,如何通过一个数据结构来创建自己的集合。你使用了 Sequence 、Collection 、CustomStringConvertible、 ExpressibleByArrayLiteral、ExpressibleByDictionaryLiteral 协议以及资第一的索引类型。

这仅仅是对 Swift 提供的用于创建健壮、使用的结合类型的协议的一点尝试。如果你想看一下还有什么其他的协议,你可以参考:

希望你能喜欢这篇教程!创建自定义的集合并不是一个常见的需求,当它能加深你对 Swift 内置集合类型的裂解。

如果有任何疑问或建议,请在下面留言。