WTUniquePrimitiveType 1.0.2

WTUniquePrimitiveType 1.0.2

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布上次发布2017年6月
SwiftSwift 版本3.0
SPM支持 SPM

Wagner Truppel 维护。



  • 作者
  • wltrup

WTUniquePrimitiveType

1.0.2 中的新功能

  • 添加了 TypesafeCountType
  • 添加了 TypesafeIntegerIndexType
  • 添加了对当查询 ArraySetDictionary 的大小时返回 TypesafeCountType 实例的支持。
  • 添加了对使用 TypesafeIntegerIndexType 实例进行 Array 下标访问的支持。
  • 任何遵从 UniqueIntegerType 的类型现在都可以使用 Int 初始化自身并检索基础值作为 Int。当操作无法执行时,这两个操作的结果都是 nil。

请期待将来为 UniqueIntegerTypeUniqueFloatingPointType 添加的算术运算符。这些很多,所以需要一些时间,但支持这些运算符的功能 正在 添加。

1.0.1 中的新功能

UniqueBooleanType 现在支持所有标准布尔运算符(!&&||)。

动机

我敢肯定,你一定遇到过代码中的相同原始类型的属性,但它们的语义意义大不相同的情况。例如,假设你有一些值类型(诚然,这是一个奇怪的例子)

    struct User {

        let id: Int
        let name: String
        var isConnected: Bool
        var coreTemperature: Double
        var numberOfPets: Int

        init(id: Int, name: String, coreTemperature: Double, numberOfPets: Int = 0) {
            self.id = id
            self.name = name
            self.isConnected = false
            self.coreTemperature = coreTemperature
            self.numberOfPets = numberOfPets
        }

    }

    struct NuclearReactor {

        let id: Int
        let name: String
        var isConnected: Bool
        var coreTemperature: Double
        var numberOfFalseAlarms: Int

        init(id: Int, name: String, coreTemperature: Double) {
            self.id = id
            self.name = name
            self.isConnected = false
            self.coreTemperature = coreTemperature
            self.numberOfFalseAlarms = 0
        }

    }

显然,用户拥有的宠物数量和他的用户ID没有关系。然而,编译器会高兴地让您将用户ID的值设置为他的宠物数量,就像这样

    let numberOfPets = 3
    let user = User(id: numberOfPets, name: "John Doe", coreTemperature: 36.7)

也许你声称绝不会混淆用户ID与他的宠物数量。好吧,那么用用户ID而不是反应堆ID初始化核反应堆,怎么样,就像这样

    let userId = 10
    // ... sometime later ...
    let id = userId
    // ... sometime later, after you've forgotten that `id` is set to `userId` ...
    let reactorId = id
    let reactor = NuclearReactor(id: reactorId, name: "Homer's Favorite", coreTemperature: 10_000.0)

或者用反应堆的内核温度初始化用户,就像这样

    let coreTemp = reactor.coreTemperature
    // ... sometime later ...
    let user2 = User(id: userId, name: "Jim Doe", coreTemperature: coreTemp)

这些错误太过容易犯,发生得太频繁。是不是很好,当您试图将一种类型的值设置为另一种类型的值时,编译器可以警告您,这两种类型都使用相同的基础原始类型进行存储?

嗯,这正是这个库能让你做的事情。

附加说明:你可能喜欢阅读我的同事所写的这篇博客文章 避免Swift中的原始数据类型偏好。它详细介绍了我朋友所说的应该避免的原始数据类型偏好的原因。这是一篇很好的阅读。

如何使用

这里有四个协议:

  • UniqueBooleanType:用于自定义布尔类型。
  • UniqueIntegerType:用于基于任何整数类型的自定义类型。
  • UniqueFloatingPointType:用于基于任何浮点类型自定义类型。
  • UniqueStringType:用于自定义字符串类型。

以及你需要知道的两个结构体

  • TypesafeIntegerIndexType:用于表示给定元素类型的数组中的索引。
  • TypesafeCountType:用于表示任何给定类型的数量或计数。

要使用这些协议,首先在你项目的某个地方声明以下类型的类型。你需要它们,但你不会经常直接使用它们:

    struct Id<T>: UniqueIntegerType {
        public typealias PrimitiveType = Int
        public let value: Int
        public init(_ value: Int) {
            self.value = value
        }
    }

    struct Name<T>: UniqueStringType {
        public typealias PrimitiveType = String
        public let value: String
        public init(_ value: String) {
            self.value = value
        }
    }

    struct Connected<T>: UniqueBooleanType {
        public typealias PrimitiveType = Bool
        public let value: Bool
        public init(_ value: Bool) {
            self.value = value
        }
    }

    struct CoreTemperature<T>: UniqueFloatingPointType {
        public typealias PrimitiveType = Double
        public let value: Double
        public init(_ value: Double) {
            self.value = value
        }
    }

你也可以创建非泛型类型和不是结构体的类型。例如,如果你的应用程序中只支持密码的类型是User类型,而你想要将密码声明为类而不是结构体,那么你可以声明如下:

    class Password: UniqueStringType {
        public typealias PrimitiveType = String
        public let value: String
        public init(_ value: String) {
            self.value = value
        }
    }

无论如何,下一步是创建实际的数据类型,如下所示:

    typealias UserId = Id<User>
    typealias UserName = Name<User>
    typealias UserConnected = Connected<User>
    typealias UserCoreTemperature = CoreTemperature<User>
    typealias CountOfPets = TypesafeIntCount<Pets> // assuming you have declared a `Pet` type

    struct User {
        let id: UserId
        let name: UserName
        let password: Password
        var isConnected: UserConnected
        var coreTemperature: UserCoreTemperature
        var numberOfPets: CountOfPets
    }

    typealias ReactorId = Id<NuclearReactor>
    typealias ReactorName = Name<NuclearReactor>
    typealias ReactorConnected = Connected<NuclearReactor>
    typealias ReactorCoreTemperature = CoreTemperature<NuclearReactor>
    typealias CountOfFalseAlarms = TypesafeIntCount<FalseAlarm> // assuming you have declared a `FalseAlarm` type

    struct NuclearReactor {
        let id: ReactorId
        let name: ReactorName
        var isConnected: ReactorConnected
        var coreTemperature: ReactorCoreTemperature
        var numberOfFalseAlarms: CountOfFalseAlarms
    }

注意这些类型读起来有多清晰。比如,Name<User>Id<User>读起来非常明确,更是自解释的,同时又是类型安全的。

此外,请注意,你不必为计数声明结构体。相反,你只需要将泛型唯一整数计数类型的特定具体版本异名到TypesafeIntCount。如果你不希望计数由Int实例支持,而是由比如UInt实例支持,那么你可以使用TypesafeUIntCount。更进一步,有一个泛型struct TypesafeCountType<CountType: Integer, TargetType>,你可以用来声明任何类型Integer作为后盾的唯一计数类型。

现在,每当你尝试使用另一个属性中的值设置一个属性值,或者将错误类型的值传递给一个函数时,编译器都会警告你的类型不匹配。例如,以下代码块将不会编译

    let reactor = NuclearReactor(id: 100,
                                 name: "Homer's Favorite",
                                 isConnected: true,
                                 coreTemperature: 10_000.0)

    let coreTemp = reactor.coreTemperature

    let user2 = User(id: 5,
                     name: "Jim Doe",
                     isConnected: false,
                     coreTemperature: coreTemp)

因为你正在尝试将用户的核温度设置为反应堆的核温度。以下是在这种情况我从Xcode中得到的警告截图

sample warning

但是等着!你注意到我没有使用长形式来调用新创建的类型初始化器,就像下面的代码吗?

    let reactor = NuclearReactor(id: ReactorId(100),
                                 name: ReactorName("Homer's Favorite"),
                                 isConnected: ReactorConnected(true),
                                 coreTemperature: ReactorCoreTemperature(10_000.0))

这是因为这些类型在适当的情况下遵守ExpressibleByIntegerLiteral之类的协议,这允许你使用文字值而不是显式的初始化调用。不幸的是,这只适用于文字。有时你可能得写些像下面这样的东西

    let userName = UserName("John Doe [\(userId.description)]")

因为编译器不会接受

    let userName = "John Doe [\(userId.description)]"

因为右侧已不再是文字字符串值。

对数组、集合和字典的支持

等等,还有更多!

如果你有一个用户ID数组,比如,怎么处理隐藏在内的原始值?简单。你使用boxed()unboxed()函数。例如,

    let array1 = [1, 2, 3]
    let userIds: [UserId] = array1.boxed()

将从一个原始类型数组(在这种情况下,是Int)创建一个UserId实例数组。类似的,

    let array2: [Int] = userIds.unboxed()

将返回一个包含所有原始值的数组。请注意,您需要通过声明期望返回的变量类型来帮助编译器推断正确的类型,正如上面为 userIdsarray2 所做的那样。

这种在封装和解封装自定义类型集合之间来回切换的便利性适用于 ArraySetDictionary 的实例。例如,您可以有一个将用户 ID 映射到用户名的字典,如下所示

    var userIdToUserNameMap: [UserId, UserName] = [:]

等等……难道你不担心字典键类型必须符合 Hashable 吗?不,您不必担心,因为原始类型本身就是可哈希的,并且支持这个库工作的底层协议利用了这个事实为您实现了 Hashable。因此,所有您的自定义 WTUniquePrimitiveType 创建都已符合 Hashable。它们还符合 EquatableComparable 丛东!

数组、集合和字典项计数

version 1.0.2 新增了在查询这些类型之一大小时返回唯一计数类型的功能。例如,您有一个用户 ID 的 Array 和一个像这样的核反应的 Set,

    let array: [UserId] = ...
    let reactors: Set<NuclearReactor> = ...

如果您访问这些的 count 只读属性,您将得到标准的 Int 值,但如果您想获取类型安全的计数,则可以访问 typesafeCount,因为它返回的计数专门针对存储在当前集合中的元素的类型。所以,

    let userIdCount = array.typesafeCount
    let reactorCount = reactors.typesafeCount

将具有不同的类型

  • userIdCount 的类型为 TypesafeIntIndexType

reactorCount 的类型为 TypesafeIntIndexType

更普遍地说,数组、集合或字典的 typesafeCount 属性的类型是 TypesafeIntIndexType,其中 ET 是存储在该集合中的实例的类型。

数组索引和下标

version 1.0.2 的新增特性是使用类型安全的索引来索引和下标数组。所以现在,您可以通过声明

    typealias IndexOfUserId = TypesafeIntIndexType<UserId>

来有一个类似于 IndexOfUserId 的类型,

    var userIds: [UserId] = ...
    let userId = userIds[IndexOfUserId(2)] // index 2 returns the 3rd element, as usual
    userIds[IndexOfUserId(4)] = UserId(...) // sets a new userID for the 5th array entry

具有类型安全的索引可以防止将一种数组的索引传递给另一种类型的数组,这是常见的错误。

对基于 Integer 的唯一原始类型的特殊支持

有时您想要通过使用非 IntInteger 属性来节约内存,而是像 Int16Int8 这样的更小的东西。有时您想确保所讨论的整数是无符号的,因此您想要像 UInt 这样的东西。有时您可能希望一个属性同时具备这两种特性,比如 UInt32

在所有这些情况下,您通常仍然希望通过 Int 值来初始化它们(在可能的情况下),并将它们作为普通的 Int 实例来引用(再次,只在可能的情况下)。

现在,在version 1.0.2版本中,您可以为任何使用Integer类型的 uniquely backed 原始数据类型使用Int值进行(尝试)初始化,并且可以使用计算属性valueAsInt(仍然和总是通过计算属性value返回底层值,相应地类型化)来访问底层值。请注意,当底层 Integer 类型的转换到和从 Int 不可能时,初始化程序和valueAsInt都会返回nil

例如,您不能使用大于127的Int8类型的唯一原始数据类型或任何由UnsignedInteger类型支持的唯一原始数据类型的负Int值进行初始化。同样,由UInt64支持的唯一原始数据类型的值范围比Int更广,因此您不一定总能得到底层值的Int表示。

算术运算

这是一件我还未做但是计划在库的version 1.1中添加的事情,所以请继续关注。目前,您必须通过使用getter属性value来访问内部值,如下所示:

    let total = user.numberOfPets.value + 3

很快您将能够编写

    let total = user.numberOfPets + 3

而不是,此时如果您编写

    let total = user.numberOfPets + reactor.numberOfFalseAlarms

好的,现在您也会得到警告,但原因不同,即操作符+不能应用于给定类型的操作数。

测试

库达到了100%的测试覆盖率。

工作原理

存在一个基本的协议

    public protocol WTUniquePrimitiveType: CustomStringConvertible, Equatable, Comparable, Hashable {

        associatedtype PrimitiveType: CustomStringConvertible, Equatable, Comparable, Hashable

        var value: PrimitiveType { get }
        init(_ value: PrimitiveType)

    }

要求任何符合该协议的类型声明适当的存储以及为此存储的初始化器。

然后,有针对单个原始数据类型的高级协议。例如,以下是基于Integer的任何原始数据类型的协议示例:

    public protocol UniqueIntegerType: WTUniquePrimitiveType, ExpressibleByIntegerLiteral {

        associatedtype PrimitiveType: Integer

    }

其余的都是使用协议扩展实现这些类型应具有的共同行为的简单问题。

要求

这个库是用Swift 3.1和Xcode 8.3.2构建和设计的。

安装

WTUniquePrimitiveType通过CocoaPods提供。要安装它,只需在Podfile中添加以下行:

pod "WTUniquePrimitiveType"

如果这不起作用(偶尔会不起作用),请尝试以下方法:

pod 'WTUniquePrimitiveType', :git => 'https://github.com/wltrup/Swift-WTUniquePrimitiveType.git'

作者

wltrup, [email protected]

许可

WTUniquePrimitiveType在MIT许可下可用。有关更多信息,请参阅 LICENSE 文件。