Swift 5.1 中引入的部分有用的新特性

2020-02-18 15:40:58 浏览数 (1)

Swift 5.1现在已经正式发布,尽管只是次要版本,它包含了大量的更改和改进。从基本的新功能,例如模块稳定性(使SDK供应商可以交付预编译的Swift框架)到所有SwiftUI以及其他功能的新语法功能。

除了具有标题的新功能外,Swift 5.1还包含许多较小的但仍然非常重要的新功能和改进。乍一看,这种变化似乎很小,甚至是不必要的,但可能会对我们编写和构建Swift代码的方式产生重大影响。

Swift 5.1 - 简书

1、函数、闭包单表达式函数的隐式返回

  • 现在,在声明仅包含单个表达式的函数和计算属性时,可以省略return关键字,这使得在声明更简单便捷的API时非常友好:
代码语言:javascript复制
//单行表达式
func increase(number: Int) -> Int {
    number   1
}

//计算属性
struct Message {
    var title: String
    var info: String
    let description: {title   ": "   info}
}

2、具有默认值的成员初始化器----自动合成结构体的构造参数

  • 原来的结构体属性有默认值时,不会生成有可选属性参数的构造函数,现在可以了
代码语言:javascript复制
struct Message {
    var title: String
    var info: "body"
}

在swift 5.1 中,下方初始化方法均正确

代码语言:javascript复制
var message = Message(title: "title")
var message = Message(title: "title", info: "info body")

3、Self 关键字

3.1、静态成员的 Self
  • Swift 5.1之后,可以使用 Self替代类名来访问静态成员
代码语言:javascript复制
class ListViewController: UITableViewController {
    static let cellReuseIdentifier = "list-cell-identifier"

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(
            ListTableViewCell.self,
            forCellReuseIdentifier: Self.cellReuseIdentifier
        )
    }
}
3.2、使用 Self 动态获取引用类型
  • SwiftSelf关键字(或类型)使我们能够在未知具体类型的上下文中动态引用实际上的类型,例如,通过在协议扩展中引用协议的实现类型:
代码语言:javascript复制
extension Numeric {
    func incremented(by value: Self = 1) -> Self {
        return self   value
    }
}
  • 我们给Numeric协议扩展了一个自增的方法,但是我们现在不知道具体自增的类型,使用Self作为返回类型,则可以动态获取对应的类型:
代码语言:javascript复制
let num1 = 5.incremented()           //num1: Int
let num2 = 5.0.incremented()         //
3.3 使用Self引用封闭类型
  • Self的范围现已扩展到还包括具体类型(例如枚举,结构体和类),使我们能够将Self用作一种引用方法或属性的封闭类型的别名,如下所示:
代码语言:javascript复制
struct TextTransform {
    let closure: (String) -> String
}

extension TextTransform {
    static var capitalize: Self {
        return TextTransform { $0.capitalized }
    }

    static var removeLetters: Self {
        return TextTransform { $0.filter { !$0.isLetter } }
    }
}

我们现在可以在上方使用Self而不是完整的TextTransform类型名称看,当然这纯粹是语法糖——但它可以使我们的代码更紧凑,尤其是在处理长类型名称时。我们甚至还可以在方法或属性中使用Self内联,同时使用隐式返回,进一步使上述代码更加紧凑:

代码语言:javascript复制
extension TextTransform {
    static var capitalize: Self {
        Self { $0.capitalized }
    }

    static var removeLetters: Self {
        Self { $0.filter { !$0.isLetter } }
    }
}

给String扩展两个方法:

代码语言:javascript复制
extension String {
    func withTransform(_ textTransform: TextTransform) -> String {
        textTransform.closure(self)
    }
    
    mutating func withTransforms(_ textTransforms: [TextTransform]) -> String {
        textTransforms.forEach{ trans in
            self = self.withTransform(trans)
        }
        return self
    }
}

let singelUse = "i am a string"
    .withTransform(.capitalize)
    .withTransform(.removeLetters)

var str = "i am a string"
let groupUse = str.withTransforms([
    .capitalize,
    .removeLetters
])

4、属性包装类型(Property Wrapper Types)

在 iOS 开发中,经常要用到@IBOutlet@IBAction,在Swift中,越来越多@修饰的关键字出现,比如 @UIApplicationMain,特别是在 SwiftUI 中,会发现有很多类似这样的关键字。

  • swift5.1中新增了一个 @propertyWrapper
  • 用它来修饰一个一个结构体,它修饰的结构体可以变成一个新的修饰符并作用在其他代码上,来改变这些代码的默认行为。
代码语言:javascript复制
@propertyWrapper
struct Trimmed {
    private var value: String = ""
    var wrappedValue: String {
        get { value }
        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
    }
    init(wrappedValue initialValue: String) {
        self.wrappedValue = initialValue
    }
}

struct Message {
    @Trimmed var title: String
    @Trimmed var info: String
}
//任何字符串无论是在初始化期间还是通过后面的属性访问都会自动删除前后面的空格。
var msg = Message(title: "  Swift5.1 Property Wrappers  ", info: "  is a new and important  key words")
let title = msg.title // "Swift5.1 Property Wrappers"
let info = msg.info  // "is a new and important  key words"

5、有序集合的差异

  • 作为Swift 5.1的一部分引入的全新标准库API,有序集合差异(ordered collection diffing)。 随着CombineSwiftUI之类的工具越来越接近声明式编程的世界,能够计算两种状态之间的差异变得越来越重要。
  • 毕竟,声明性UI开发就是关于不断呈现状态的新快照的,而且尽管SwiftUI和新的diffable数据源可能会完成大部分繁重的工作来实现这一点,但能够计算出我们自己在两种状态之间的差异可能是非常有用。
  • 例如,假设我们正在构建一个DatabaseController,它将使我们可以使用一系列内存模型轻松地更新磁盘上的数据库。为了能够确定是应该插入还是删除模型,我们现在可以简单地调用新的差异API来计算旧数组与新数组之间的差异-然后迭代该差异中的更改以执行我们的数据库操作:
代码语言:javascript复制
class DatabaseController<Model: Hashable & Identifiable> {
    private let database: Database
    private(set) var models: [Model] = []
    
    ...

    func update(with newModels: [Model]) {
        let diff = newModels.difference(from: models)

        for change in diff {
            switch change {
            case .insert(_, let model, _):
                database.insert(model)
            case .remove(_, let model, _):
                database.delete(model)
            }
        }

        models = newModels
    }
}
  • 但是,上述实现并未考虑已移动的模型-因为默认情况下,移动将被视为单独的插入和删除。为了解决这个问题,我们在计算diff时也要调用inferringMoves方法,然后查看每个插入是否与移除关联,如果这样,则将其视为移动,如下所示:
代码语言:javascript复制
func update(with newModels: [Model]) {
    let diff = newModels.difference(from: models).inferringMoves()
    
    for change in diff {
        switch change {
        case .insert(let index, let model, let association):
            // 如果 associated index 不为 nil, 则意味着这个inset操作实际上是move
            if association != nil {
                database.move(model, toIndex: index)
            } else {
                database.insert(model)
            }
        case .remove(_, let model, let association):
            // 我们将只处理 association 为 nil的情况,其他情况已经在上方处理
            if association == nil {
                database.delete(model)
            }
        }
    }

    models = newModels
}
  • 现在,将差异内置到标准库(以及UIKit和AppKit中)的事实真是太了不起了——因为编写高效,灵活且健壮的差异算法非常困难。

6、将协议抛出错误功能实现为非抛出

  • 在Swift中,可以使用非抛出函数满足抛出错误函数协议的要求,这在某些情况下非常有用。例如,假设我们为解析器定义了一个协议,该协议使我们可以通过某种方式对字符串进行标记:
代码语言:javascript复制
protocol TokenParser {
    func parseToken(from string: String) throws -> Token
}
  • 尽管上述协议的某些实现需要抛出,但不一定对所有符合条件的类型都适用。例如,下面的KeywordParser抛出,而TextParser没有:
代码语言:javascript复制
struct KeywordParser: TokenParser {
    func parseToken(from string: String) throws -> Token {
        ...
    }
}

struct TextParser: TokenParser {
    //这仍然能够符合我们的协议,虽然它没有 throws
    func parseToken(from string: String) -> Token {
        ...
    }
}
  • 由于我们协议功能的原始声明被标记为throw,因此在确切的确切类型未知时,我们总是需要使用try来调用它——不管基础实现是否实际抛出:
代码语言:javascript复制
let parsers: [TokenParser] = ...

for parser in parsers {
    let token = try parser.parseToken(from: string)
}
  • 但是,当直接处理非抛类型时,我们现在可以省略try关键字——即使原始协议要求已标记为throws:
代码语言:javascript复制
let parser = TextParser()
let text = parser.parseToken(from: string)
  • 这是一个很小的功能,但事实是,我们可以使用非抛出函数来实现抛出函数的要求,这使我们在遵守包含此类函数的协议时具有更高的灵活性。

7、字符串插值新协议ExpressibleByStringInterpolation——使类型可以使用字符串插值

  • 为诸如字符串和整数之类的原始值创建包装器类型,是使我们的代码更具类型安全性和自记录性的好方法,并且还为我们提供了专用的类型,可以在这些类型上实现特定于域的便捷性API。
  • 例如,这种类型代表文件系统路径,可用于执行诸如加载文件内容的操作:
代码语言:javascript复制
struct Path {
    var string: String
}

func loadFile(at path: Path) throws -> File {
    ...
}
  • 但是,虽然拥有专用的Path类型确实给我们带来了很多好处,但它也可能使我们的API使用起来更加麻烦。例如,任何时候我们想要使用字符串文字来指定路径时,我们现在都必须先将其包装起来:
代码语言:javascript复制
try loadFile(at: Path(string: "~/documents/article.md"))
  • 为了解决这个问题,我们可以使Path符合ExpressibleByStringLiteral,这使我们能够直接将字符串文字传递给任何接受Path的API:
代码语言:javascript复制
extension Path: ExpressibleByStringLiteral {
    init(stringLiteral value: String) {
        self.string = value
    }
}
try loadFile(at: "~/documents/article.md")
  • 这样已经非常好了,但是如果我们在字符串中使用任何形式的插值,则上述方法将无法正常工作,例如:
代码语言:javascript复制
try loadFile(at: "/users/(username)/file.txt")

现在,swift5.1引入了新协议ExpressibleByStringInterpolation,只要使Path遵循这个协议,则上面的代码就可以正常运行了,增加如下代码:

代码语言:javascript复制
extension Path: ExpressibleByStringInterpolation {}

8、返回值类型抽象化 —— some关键字

这里的some其实就是和一个称为opaque(不透明)类型有关,在返回类型前面加上一些关键字表示返回类型是不透明的,不透明类型通常被称为反向泛型类型。 比如我定一个一个Animal协议,具有关联类型LikeType

代码语言:javascript复制
protocol Animal {
    associatedtype LikeType
    var name: String { get }
    var like: LikeType { get }
}

struct Dog: Animal {
    var name: String
    var like: Bark
}

struct Pig: Animal {
    var name: String
    var like: Sleep
}

此时我们实现一个方法去识别动物:

代码语言:javascript复制
func identityAnimal() -> Animal

这在swift中是无法编译通过的,因为swift不能把带有关联类型的协议类型作为返回类型,这个时候就轮到some上场了:

代码语言:javascript复制
func identityAnimal() -> some Animal {
    return Pig(name: "pink pig", like: Sleep("every day"))
}

var animal: some Animal = identityAnimal()

参考文档: 5 small but significant improvements in Swift 5.1 Implementing throwing protocol functions as non-throwing Making types expressible by string interpolation

0 人点赞