标红修饰词
open, public, internal, fileprivate, private, forzen
访问控制import
typealias, associatedType
重命名discardableResult
可丢弃结果override, convenience, required, deinit, final
class继承相关dynamicMemberLookup, dynamicCallable
动态成员查找,动态传参propertyWrapper
属性构造器resultBuilder
结果构造器throws, throw, rethrows
异常async, await, actor
autoclosure
自动闭包defer
执行代码块operator, prefix, infix, postfix, precedencegroup, associativity, higherThan, lowerThan, assignment
自定义运算符fallthrough
switchavailable
版本控制#file, #filePath, #fileID, #line, #column, #function, #dsohandle, #sourceLocation
打印相关.Type, .self
元类型相关nonmutating
#keyPath
字符串表达式@objc, @nonobjc, @objcMembers
OC混编@requires_stored_property_inits
访问控制 Access Control
open
public
internal
fileprivate
private
open/public
可以在任何模块中访问使用,即可以使用import
在其他模块中访问,其中open
只能修饰class
类型,public
既可以修饰class
类型,也可以修饰struct/enum
类型internal
默认访问权限,可以在当前模块中访问,不能在其他模块中使用fileprivate
可以在当前文件中被访问,在其他文件中不能被访问private
可以在当前class
以及struct/enum
中被访问,其他类中不能被访问
@forzen
这个比较特殊,用的不多,它针对枚举或者结构体的声明使用该特性,可以显示你对该类型的修改。它只有在编译迭代库时被允许使用。未来版本的库不能通过添加、删除或重新排序枚举的case或者结构的存储实例属性来更改声明。在未冻结的类型上,这些操作都是允许的,但是它们破坏了冻结类型的 ABI
兼容性。
注意: 昂编译器不处于迭代库的模式,所有的结构体和枚举都是隐性冻结,并且该特性会被忽视。
import
关于引入框架,我觉得很多人都在找类似 OC
中 prefix
的文件,但是没找到,其实 Swift
中是存在这种方法的
@_exported import SDWebimage
众所周知,带有_ 的语句为未确定语句
将上面的代码放在 @main
函数执行的文件中进行引入,那么在当前模块中都无线再进行引入,可以直接使用
重命名与关联对象
typealias
typealias
用来对已经存在的类型重新定义新名字,通过命名,可以是代码变得更加清楚简洁。
typealias success = (_ data: String) -> Void
typealias ViewType = UIView
associatedType
associatedType
用来定义一个名字,也可以确定类型,具体使用的值需要用 typealias
来确定,一般在 protocol
中使用。
protocol Network {
associatedType DataType // 没确定类型
associatedType ViewType: UIView // 确定类型,实现时可以使用其子类
}
class Model: Network {
typealias DataType = String
typealias ViewType = UILabel
}
方法返回值
@discardableResult
一般用在有返回值的方法前,含义是,调用当前方法时,可以不使用对象去接收
func getName() -> String {
}
// 当调用该方法时需要使用对象承接
let name = getName()
// 或者
let _ = getName()
而如果不使用对象去接收时,编译器则会报告警告⚠️,因此使用 @discardableResult
将警告消除
@discardableResult func getAge() -> Int {
}
getAge()
关于继承
只有 class
类型可以被继承这个概念不用我多说吧,那么就来说说继承之后的事
override
覆盖父类的方法或属性,如覆盖父类的init
方法
override init() {
// 必须要执行父类的init方法
// 其中子类自定义的属性要放在 super.init() 方法前执行
super.init()
// 子类修改父类的属性要放在 super.init() 方法后执行
}
// 覆盖属性父类属性要使用计算属性覆盖
override var name: String {
get {
return ""
}
set {
}
}
convenience
用于修饰init
方法,为便利构造器,可以override convenience
覆盖父类的便利构造器,方法中必须执行当前子类的构造器
override init() {
}
override convenience init(name: String) {
self.init()
self.name = name
}
required
子类必须重写的init
方法
required init(_ sex: String) {
fatalError("init(_:) has not been implemented")
}
final
使它修饰的变量、方法、类不可继承,使用final
可以提高性能。避免系统的动态派发(Dynamic Dispatch
)
final class Person {}
动态成员查找
@dynamicMemberLookup
是在Swift 4.2 中增加的新特性,即动态成员查找。在使用了 @dynamicMemberLookup
修饰对象后(class
、struct
、enum
、protocol
),并且实现了 subscript(dynamicMember member: String)
方法后就可以方为不存在的属性,而属性就会作为 member
传入这个方法。
@dynamicMemberLookup
struct Person {
subscript(dynamicMember member: String) -> String {
let properties = ["nickname": "Zhang", "city": "BeiJing"]
return properties[member, default: "nil"]
}
}
// 执行
let p = Person()
print(p.city)
print(p.nickname)
print(p.name)
如果没有声明 @dynamicMemberLookup
,执行代码会报错
如果实现了多个 subscript(dynamicMember member: String)
方法时,需要在执行时执行返回类型
@dynamicMemberLookup
struct Person {
subscript(dynamicMember member: String) -> String {
let properties = ["nickname": "Zhang", "city": "BeiJing"]
return properties[member, default: "nil"]
}
subscript(dynamicMember member: String) -> Int {
return 18
}
}
let p = Person()
let age: Int = p.age
print(age) // 18
@dynamicMemberLookup
内部处理
a = someValue.someMember
==>
a = someValue[dynamicMember: "someMember"]
动态传参
@dynamicCallable
,当对象被 @dynamicCallable
标记后,需要实现两个必要方法中的某一个或者两个来进行动态传参。
// 其中数组中的数据类型及返回类型可替换成其他类型
func dynamicallCall(withArguments args: [String]) -> Double
// 其中KeyValuePairs中的数据类型及返回类型可替换成其他类型
func dynamicallCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double
使用方法
@dynamicCallable
struct Random {
// func generate(numberOfZeros: Int) -> Double {
// let max = pow(10, Double(numberOfZeros))
// return Double.random(in: 0...max)
// }
func dynamicallyCall(withArguments: [Int]) -> Double {
return 12.0
}
func dynamicallyCall(withKeywordArguments args: [String: Int]) -> Double {
let numberOfZeros = Double(args.first?.value ?? 0)
let max = pow(10, Double(numberOfZeros))
return Double.random(in: 0...max)
}
}
// 调用
let random = Random()
// let result = random.dynamicallyCall(withKeywordArguments: ["numberOfZeros": 3])
let result = random(number: 3) // 调用 withKeywordArguments 方法
let result = random(3) // 12.0 调用 withArguments 方法
print(result)
@dynamicCallable
使用时需要注意一些重要规则
- 可以将其应用于
class
、struct
、enum
和protocol
- 如果使用 withKeywordArguments: 并且不使用 withArguments: 你的类型仍然可以在没有参数标签的情况下调用 - 只不过会获得空字符串的键(
key
) - 如果
withKeywordArguments: / withArguments:
被标记为throwing
,则调用类型也将throwing
- 不能将
@dynamicCallable
添加到扩展,只能添加到类型本身 - 你仍然可以向类型添加其他方法和属性,并正常使用。
Swift 的野心
Swift 之所以做出 @dynamicMemberLookup
和 @dynamicCallable
,其根本原因在于 Swift
不仅仅将视线放在了 C
与 OC
的互通上,也将目光放在了将来与 Python
、JavaScript
等动态语言一起工作。
属性包装器
@propertyWrapper
属性包装器的目的就是为了能够给属性一个统一的入口和出口,方便我们进行操作。比如当我们想监听属性时,需要在每个属性中做监听,而属性包装器可以将监听做成统一入口。
对于 @propertyWrapper
,有两个要求:
- 必须使用
@propertyWrapper
进行定义 - 必须具有
wrappedValue
属性
@propertyWrapper
struct Random<T> {
var wrappedValue: T
}
// 使用
@Random var r: Int = 0
// 或者
@Random(wrappedValue: 0) var r
// 或者
var r: Randow = Random(wrappedValue: 0)
/// 三种方法均可,通常使用第一种
系统定义好的属性包装器有 @State
, @Binding
, @Published
, @ObservedObject
, @StateObject
, @EnvironmentObject
, @Environment
等。
// 关于在SwiftUI中有重大问题,将单列一篇文章来讲
@State private var height: CGFloat? // 在实际使用中有重大问题
使用限制
- 具有包装器的属性不能再子类中覆盖
- 具有包装器的属性不能有
lazy
,@NSCoping
,@NSManaged
,weak
或unowned
修饰 wrappedValue
,init(wrappedValue:)
和projectedValue
必须具有与包装类型本身相同或高级(并不生效)的访问控制级别- 不能再协议或扩展中声明带有包装器的属性
Where
where
在定义泛型的类中会被经常使用,
// 泛型
struct Stack<Element> {
}
// 如果要求泛型是某一特殊类型时
extension Stack where Element == String {
}
// 如果要求泛型遵循某一协议时
extension Stack where Element: Equatable {
}
// 定义非默认泛型
struct Sildeshow<Data, ID, Content>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
}
// 上面的定义可以写成
struct Sildeshow<Data: RandomAccessCollection, ID: Hashable, Content: View>: View {
}
也可以在 for循环
中使用
for i in 0...3 where i % 2 == 0 {
print(i)
}
// 打印 0 2
结果生成器 resultBuilder
@resultBuilder
是 Swift 5.4
的新特性,之前叫 Function Builders
,它使用 buildBlock
方法可以将 多个内容 构建为 一个结果 ,该特性在 SwiftUI
中广泛使用,如 @ViewBuilder
可以使用 @resultBuilder
自定义Result Builders
@resultBuilder
struct StringBuidler {
// 可以将多个值构建为一个结果
static func buildBlock(_ components: String...) -> String {
// 多个值处理
components.joined(separator: "\n")
}
// if 逻辑分支
static func buildEither(first component: String) -> String {
return "if \(component)"
}
// else 逻辑分支
static func buildEither(second component: String) -> String {
return "else \(component)"
}
// 当只实现或只执行if时,而结果又为false时走该逻辑分支
static func buildOptional(_ component: String?) -> String {
return "啥也不是"
}
}
// 修饰方法
@StringBuidler
func buildString() -> String {
"静夜思"
"李白"
"窗前明月光,疑是地上霜。"
"举头望明月,低头思故乡。"
if Bool.random() {
"一首诗"
} else {
"一首词"
}
}
// 修饰闭包参数
func builderString(@StringBuidler content: () -> String ) -> String {
return content()
}
异常
throws
在 Swift
中,有很多在OC中执行没有问题但在 Swift
中使用可能会抛出异常的函数,这些函数都使用 throws
来标记,当我们执行多个 throws
方法时,可以自定义 throws
函数进行统一抛出异常。
// 例如在使用AVFoundation框架时
struct Record {
var audioRecorder: AVAudioRecorder?
mutating func record() throws {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, mode: .default)
try session.setActive(true)
let audioUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent("audio.m4a")
let settings = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC)]
audioRecorder = try AVAudioRecorder(url: audioUrl, settings: settings)
}
}
// 这时,当我们调用方法时,可以进行统一处理
var record = Record()
do {
try record.record()
} catch {
fatalError("record error \(error.localizedDescription)")
}
throw
throw
用于在函数内部,如果出现可能的异常,可以使用 throw
抛出这个异常。
enum TestError: Error {
case errorOne(Int)
case errorTwo(String)
}
func testThrow(num: Int) throws -> String {
switch num {
case 1:
throw TestError.errorOne(1)
case 2:
throw TestError.errorTwo("2")
default:
return "no error"
}
}
// 执行
do {
let result: String = try testThrow(num: 2)
print(result)
} catch TestError.errorOne(let num) {
print(num)
} catch TestError.errorTwo(let str) {
print(str)
} catch let err {
print(err.localizedDescription)
}
rethrows
rethrows 关键字只起到传递异常的作用,在一个函数或者方法中,调用一个会throw的函数,就可以通过rethrows传递可能的异常
func testRethrow(testThrowCall: (Int) throws -> String, num: Int) rethrows -> String {
try testThrowCall(num)
}
// 执行
do {
let result: String = try testRethrow(testThrowCall: testThrow(num:), num: 2)
print(result)
} catch TestError.errorOne(let num) {
print(num)
} catch TestError.errorTwo(let str) {
print(str)
} catch let err {
print(err.localizedDescription)
}
异步等待 async/await/actor
async/await
async/await
功能是为了是在并发中的 Swift
代码能容易编写和理解,因为在一起一直使用闭包(Block)来处理异步操作。当你的代码又许多异步操作,并且是一个异步操作需要等待另一个异步操作时,最简单的是在一个异步操作的内部执行另一个异步操作,这种方法很快就会导致“回调地狱”。
因此Swift为语言带来了 async/await
函数可以选择成为异步的,允许开发者使用正常的控制流机制来组成涉及异步操作的复杂逻辑。编译器负责将一个异步函数翻译成适当的闭包和状态机集合。
下面的代码显示了如何声明和调用 async/await
struct Resource {}
struct LoadImage {
func loadWebResource(_ path: String) async throws -> Resource {
return Resource()
}
func decodeImage(_ r1: Resource, r2: Resource) async throws -> Image {
return Image(systemName: "")
}
func dewarpAndCleanupImage(_ i: Image) async throws -> Image {
return i
}
func processImageData() async throws -> Image {
let r1 = try await loadWebResource("r1")
let r2 = try await loadWebResource("r2")
let image = try await decodeImage(r1, r2: r2)
let result = try await dewarpAndCleanupImage(image)
return result
}
}
// 调用
let image = LoadImage()
do {
Task {
let i = try await image.processImageData()
}
} catch {
fatalError("load image error \(error.localizedDescription)")
}
注意:
- 在声明方法时,如果需要
async
与throws
同时使用时,async
必须要在throws
的前面。 - 在调用方法时,如果需要
await
与try
同时使用时,try
必须要在await
的前面。
虽然异步函数似乎大大简化了并发管理,但是它们并没有排除死锁或状态损坏的可能性。因此开发者需要注意暂停点异步函数带来的问题。为了避免死锁或数据损坏的风险,异步函数应避免调用可能阻塞其线程的函数。
那么怎么将已经定义好的闭包函数变成async
函数呢?
转换时需要借助两个函数
public func withCheckedContinuation<T>(function: String = #function, _ body: (CheckedContinuation<T, Never>) -> Void) async -> T
public func withCheckedThrowingContinuation<T>(function: String = #function, _ body: (CheckedContinuation<T, Error>) -> Void) async throws -> T
两个函数的区别在于是否需要存在 throws
struct Resource {}
class LoadImage {
func loadWebResource(_ path: String, resource: @escaping (Resource) -> Void) throws {
resource(Resource())
}
func decodeImage(_ r1: Resource, r2: Resource, image: @escaping (Image) -> Void) throws {
image(Image(systemName: "xmark"))
}
func dewarpAndCleanupImage(_ i: Image, image: @escaping (Image) -> Void) throws {
image(i)
}
var r1: Resource!
var r2: Resource!
func imageData(_ complated: @escaping (Image) -> Void) throws {
let semaphore = DispatchSemaphore.init(value: 0)
try loadWebResource("") { [weak self] resource in
self?.r1 = resource
print("r1")
semaphore.signal()
}
semaphore.wait(timeout: DispatchTime.distantFuture)
try loadWebResource("") { [weak self] resource in
self?.r2 = resource
print("r2")
semaphore.signal()
}
semaphore.wait(timeout: DispatchTime.distantFuture)
try decodeImage(self.r1, r2: self.r2, image: { im in
print("decode")
complated(im)
})
}
func processImageData() async throws -> Image {
try await withCheckedThrowingContinuation({ continuation in
let queue = DispatchQueue.init(label: "async.queue")
queue.async {
do {
try self.imageData { image in
continuation.resume(returning: image)
}
} catch {
continuation.resume(throwing: error)
}
}
})
}
}
// 调用
let image = LoadImage()
do {
Task {
let i = try await image.processImageData()
print(i)
}
} catch {
fatalError("load image error \(error.localizedDescription)")
}
注意看上面代码中 imageData
的方法,在方法内部需要使用属性类存储相关回调的方法的值。并且还需要借助 semaphore
来进行值的赋值获取。当前类型也要修改为 class
,确实比较麻烦,但是在调用的时候还是比较方便的。
actor
actor
是建立在 async/await
上的抽象,可以安全的访问可变的状态。简而言之 actor
封装了一些状态,并提供了一组方法来安全的访问它。
与类不同的是,actor
每次只允许一个任务访问其可变状态,这使得多个任务的代码可以安全的与 actor
的同一个实例进行交互
注意:actor
的对象为引用类型
actor WebImage {
let url: String
var image: Image!
private(set) var max: Int
init(url: String, image: Image) {
self.url = url
self.image = image
self.max = url.count
}
}
// 调用
Task(priority: .background) {
let image = await WebImage(url: "", image: Image(systemName: "xmark"))
}
自动闭包 @autoclosure
@autoclosure
是一种自动创建的闭包,用于将参数包装闭包。这种闭包不接受任何参数,当它被调用的时候,会返回传入的值,这种便利语法让你在调用的时候能够省略闭包的花括号。
函数中有一个 () -> Any
类型的参数,用 @autoclosure
修饰时,调用函数的时候可以传入一个确定的值 value
,这个值会被自动包装成 (){ return value }
的闭包,就不需要显式的将闭包表达式写出来
struct Resource {
func debugOutPrint(_ condition: Bool, _ message: @autoclosure () -> String) {
if condition {
print("debug = \(message())")
}
}
func getString() -> String {
return "Application Eror Occured"
}
}
// 调用
let res = Resource()
res.debugOutPrint(true, "Application Error")
res.debugOutPrint(true, res.getString())
defer
defer {}
里的代码会在当前代码块返回的时候执行,无论当前代码块从哪个分支 return
的,即使抛出异常也会执行。
如果多个 defer
语句出现在同一作用域中,则它们出现的顺序与它们执行的顺序相反,也就是先出现的后执行。
func function() {
defer {
print("first defer")
}
defer {
print("second defer")
}
print("end of function")
}
function()
// 打印结果
// end of function
// second defer
// first defer
自定义运算符
Swift
中可以声明和实现自定义运算符。新的运算符要使用 operator
关键字在全局作用域内进行定义,同时还要指定 prefix
、infix
或者 postfix
修饰符
struct Point {
var x: CGFloat
var y: CGFloat
}
// 前缀运算符
prefix operator +++
extension Point {
static prefix func +++ (point: Point) -> Point {
return Point(x: point.x * 2, y: point.y * 2)
}
}
// 执行
let point = Point(x: 2, y: 5)
+++point // x: 4.0, y: 10.0
每个自定义中缀运算符都属于某个优先级组。优先级组指定了这个运算符相对于其他中缀运算符的优先级和结合性。
而没有明确放入某个优先级组的自定义中缀运算符将会被放到一个默认的优先级组内,其优先级高于三元运算符。
下面例子定义了一个新的自定义中缀运算符 +-
,此运算符属于 AdditionPrecedence
优先级组,其他优先级组
infix operator +-: AdditionPrecedence
extension Point {
static func +- (lhs: Point, rhs: Point) -> Point {
return Point(x: lhs.x + rhs.x, y: lhs.y - rhs.y)
}
}
// 调用
let p1 = Point(x: 2, y: 3)
let p2 = Point(x: 4, y: 2)
p1 +- p2 // x: 6.0, y: -1.0
注意: 当定义前缀与后缀运算符的时候,我们并没有指定优先级。然而,如果对同一个值同时使用前缀和后缀运算符的时候,后缀运算符会先参与运算。
precedencegroup
precedencegroup
用于自定义优先级
precedencegroup CustomPrecedence {
// 结合律
associativity: none
/// 赋值性
assignment:
// 优先级设置
// 高于加减法
higherThan: AdditionPrecedence
// 低于乘除法
lowerThan: MultiplicationPrecedence
}
associativity
定义了结合律,即当多个同类的操作付出现的计算顺序。比如常见的加减法都是left
,就是说多个加减法同时出现时,按照从左往右的顺序计算。不允许连续出现的操作符可以写成none
higherThan/lowerThan
用于指定自定义的优先级高于和低于另一个优先级
fallthrough
Swift fallthrough
语句让 case
之后的语句按顺序继续执行,且不论条件是否满足。
Swift 中的 switch
不会从上一个 case
分支落入到下一个case分支。只要第一个匹配到的 case
分支完成了它需要执行的语句,整个 switch
代码块完成了它的执行。
注意:在大多数语言中,switch
语句块中,case
要紧跟 break
,否则 case
之后的语句会顺序运行,而在 Swift
语言中,默认是不会执行下去的,switch
也会终止。如果你想在 Swift
中让 case
之后的语句会按顺序继续运行,则需要使用 fallthrough
语句。
func getIndex(index: Int) {
switch index {
case 100:
print("index value = 100")
fallthrough
case 10, 15:
print("index value = 10 or 15")
fallthrough
case 5:
print("index value = 5")
default:
print("default")
}
}
// 执行
getIndex(index: 10)
// 打印结果
// index value = 10 or 15
// index value = 5
版本控制
available
是用于版本控制的,有 #available
和 @available
两种,前一种是在函数或方法内部的,一般用于 if
或 guard
的判断。
if #available(iOS 15.0, macOS 10.0, watchOS 3.0, *) {
}
guard #available(iOS 14.0, macOS 11.0, watchOS 3.0, *) else {
return
}
而 @available
一般用于整个方法或整个类的版本控制
@available(iOS 16.0, macOS 10.0, watchOS 2.0, *)
func avai() {}
@available(iOS 16.0, macOS 10.0, watchOS 2.0, *)
struct Person {}
如果你只想控制 iOS
的版本其他不想限制的时候,只写 iOS
的控制即可。
打印相关
#file
输出完整的当前文件的名字#filePath
当前文件的绝对路径#fileID
当前文件的相对路径,相对于当前工程#line
#line这个函数所在的行数#column
#column这个函数所在行的列数,即前面有多少个字符#function
当前的方法名#dsohandle
使用动态共享对象处理#sourceLocation
行控制语句,可以改变#file
,#line
等所打印的内容,其中参数file
最好设置绝对路径
func printInfo() {
print("some person info - on column \(#column), line \(#line), function \(#function), file \(#file), fileID \(#fileID), filePath \(#filePath), dsohandle \(#dsohandle), source ")
#sourceLocation(file: "ListDemo/Stack.swift", line: 1)
print("some person info - on column \(#column), line \(#line), function \(#function), file \(#file), fileID \(#fileID), filePath \(#filePath), dsohandle \(#dsohandle), source ")
}
元类型
元类型就是类型的类型。比如说 5 是 Int 类型,此时 5 是 Int 类型的一个值。但是如果问Int类型占用多少内存空间,这个时候与具体某个值无关,而与类型的信息相关。如果要写一个函数,返回一个类型的实例内存空间大小。那么这个时候的参数是一个类型数据,这个类型数据是可以直接说明的,比如是Int 类型,也可以从一个值上取,比如 5 这个值的类型。这里的类型数据,就是一个类型的类型,术语叫元类型:Metatype
。
.Type
和 .self
Swift 中的元类型用 .Type
表示,比如 Int.Type
就是 Int
的元类型。类型与值有着不同的形式,就像 Int
与 5
的关系。元类型也是类似, .Type
是类型,类型的 .self
是元类型的值。
let intType: Int.Type = Int.self
可能平时对元类型使用的比较少,加上这两个形式有些接近,一个元类型只有一个对应的值,所以使用的时候常常写错
types.append(Int.Type)
types.append(Int.self)
如果分清了 Int.Type
是类型的名称而不是值,就不会再弄错了。因为你肯定不会这么写
numbers.append(Int)
type(of: T)
type(of: T)
是通过实例对象获取元类型的方式。
func type<T, Metatype>(of value: T) -> Metatype
因此,也可以通过实例对象来获取元类型
let intType: Int.Type = type(of: 5)
.self
取到的是静态的元类型,声明的时候是什么类型就是什么类型type(of:)
取到的是运行时的元类型,也就是这个实例的类型
当我们访问静态变量的时候,其实也是通过元类型进行访问的,只是编译器帮我们省略了 .self
。下面的两个写法是等价的
Int.max
Int.self.max
AnyClass
AnyClass
就是对 AnyObject
的元类型起的别名
public typealias AnyClass = AnyObject.Type
nonmutating
先来看一段代码
let pointer = UnsafeMutablePointer<Any>.allocate(capacity: 1)
pointer[0] = 1
let array = [Any](repeating: 0, count: 1)
array[0] = 1
同样是 let
修饰,同样用下标访问,同样是 struct
,为什么 UnsafeMutablePointer
不会报错?
二者的区别在于 subscript
方法
// Array
@inlinable public subscript(index: Int) -> Element
// UmsafeMutablePointer
@inlinable public subscript(i: Int) -> Pointee { get nonmutating set }
当用 nonmutating
修饰 setter方法
的时候,编译器会知道该方法不会改变常量变量因此不会报错
使用 nonmutating
有一定的危险性,因为它跳过了编译器的安全检查机制。不过我们可以在合适的情况下使用它,写出更加灵活的代码
比如给 UserDefaults
添加 subscript
方法
protocol UserDefaultsSubscipt {
subscript(key: String) -> Any? { get nonmutating set }
}
extension UserDefaults: UserDefaultsSubscipt {
subscript(key: String) -> Any? {
get { return object(forKey: key) }
set { set(newValue, forKey: key) }
}
}
// 使用
let defaults = UserDefaults.standard
defaults["key"] = "value"
keyPath 字符串表达式
#keyPath
字符串表达式可以访问一个引用 Objective-C
属性的字符串,通常在 key-value 编程
和 key-value 观察
的API中使用。#keyPath
(属性名)
属性名必须
是一个可以在 Objective-C 运行时
使用的属性的引用。在编译期,key-path
字符串表达式会被一个字符串字面量替换。
class SomeClass: NSObject {
@objc var someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
}
}
// 调用
let c = SomeClass.init(someProperty: 12)
let keyPath = #keyPath(SomeClass.someProperty)
if let value = c.value(forKey: keyPath) {
print(value)
}
// 12
当在一个类中使用 key-path
字符串表达式时,可以省略类名,直接使用属性名来访问这个类的某个属性。
extension SomeClass {
func getSomeKeyPath() -> String {
#keyPath(someProperty)
}
}
print(c.getSomeKeyPath())
// someProperty
由于 key-path
字符串表达式在编译期才创建,编译期可以检查属性是否存在,以及属性是否暴露给 Objective-C
运行时。
注意:尽管#keyPath是一个表达式,但它永远不会被求值
@objc, @nonobjc, @objcMembers
OC 混编
@objc
该特性用于修饰任何可以在Objective-C中表示的声明。
该特性用在扩展中,与在没有明确标记为nonobjc特性的扩展中给每个成员添加该特性具有相同效果。
编译器隐式的将objc特性添加到Objective-C中定义的任何类的子类。但是,子类不能是泛型,并且不能继承于任何泛型类。你可以将objc特性显式添加到满足这些条件的子类,来指定其Objective-C的名称。
在以下情况中同样会隐式的添加objc特性:
- 父类有objc特性,且重写为子类的声明。
- 遵循带有objc特性协议的声明。
- 带有IBAction、IBSegueAction、IBOutlet、IBDesignable、IBInspectable、NSManaged或GKInspectable特性的声明。
class ExampleClass: NSObject {
@objc var enabled: Bool {
@objc(isEnabled) get {
// 返回适当的值
}
}
}
@nonobjc
@nonobjc
针对方法、属性、下标或构造器的声明使用该特性将覆盖隐式的 objc
特性。nonobjc
特性告诉编译器该声明不能再Objective-C代码中使用,即便它能在Objective-C中表示。
@nonobjc
用在扩展中,与在没有明确标记为 objc
特性的扩展中给每个成员添加该特性具有相同效果。
可以使用 nonobjc
特性解决标有 objc
的类中桥接方法的循环问题,太特性还允许对标有 objc
的类中的构造器和方法进行重载。
标有 nonobjc
特性的方法不能重写标有 objc
特性的方法,而标有 objc
特性的方法可以重写标有 nonobjc
特性的方法。同样,标有 nonobjc
特性的方法不能满足标有 objc
特性的协议中的方法。
@objc class Point: NSObject {
var x: CGFloat = 1
var y: CGFloat = 1
@nonobjc var center: Point
override init() {
self.center = Point()
super.init()
}
}
@objcMembers
该特性用于类声明,以将 objc
特性应用于该类、扩展、子类以及子类的扩展的所有 Objective-C
兼容成员。
大多数代码应该使用 objc
特性,以暴露所需的声明。如果需要暴露多个声明,可以将其分组到添加 objc
特性的扩展中。objcMembers
特性为大量使用 Objective-C
运行时的内省工具的库提供了便利。添加不必要的 objc
特性会增加二进制体积并影响性能。
@requires_stored_property_inits
该特性用于类声明,以要求类中所有存储属性提供默认值作为其定义的一部分。对于从中继承的任何类都推断出 NSManagedObject
特性。