你能在Swift中强制执行一个类型别名吗?

4

我将尝试实现一个简单的类型,由Int支持,以便不会与其他Int混淆。

假设您有以下类型别名:

typealias EnemyId = Int
typealias WeaponId = Int

我希望以下代码能够出现编译错误:
var enemy: EnemyId = EnemyId("1")
enemy = WeaponId("1") // this should fail

我希望失败的那行代码能够失败,因为这两种类型(EnemyId 和 WeaponId)是不同的类型。

最好、最清晰的方法是什么?

更新:

在查看答案和评论后,我想使用枚举来解决问题:

enum Enemy {
    case id(Int)
    var id: Int {
        switch self {
        case .id(let i):
            return i
        }
    }
}
let enemy = Enemy.id(1)
print("enemy: \(enemy.id)")

目前Mattt的答案更简短,符合Swift的预期。

更新#2

我还没有使用Swift 4.1的权限,所以我必须执行以下操作:

  struct EnemyId : Hashable {
    private let value: Int

    init(_ value: Int) {
      self.value = value
    }

    init?(_ string:String) {
      guard let value = Int(string) else { return nil }
      self.init(value)
    }

    static func ==(_ lhs: EnemyId, _ rhs: EnemyId) -> Bool {
      return lhs.value == rhs.value
    }

    var hashValue: Int {
      return value.hashValue
    }
  }

然而,结果证明它增加了几百毫秒的解析时间,因此我不得不回到typealias,但它非常接近我想要的效果。


这看起来更适合使用“枚举”。这样,编译器可以防止您进行意外的转换,您永远不必担心无效的敌人或武器类型,并且您可以使用“Weapon.sword”(甚至在大多数情况下是“.sword”)来引用值,而无需声明常量,如“WEAPON_SWORD”。 - NobodyNada
@NobodyNada 我也尝试过使用枚举,但是预定义每个可能的ID太过繁琐。至少我无法得出一个令人满意的解决方案。 - CodeReaper
5个回答

7
请注意,在Swift 4.1中,像Rob和Cristik建议的包装结构体变得非常容易编写,因为当您声明采用Hashable时,您将在幕后获得自动合成实现 hashValue ==
因此,对于您的目的,编写以下内容可能已足够:
struct EnemyId: Hashable {
    let value: Int
}
struct WeaponId: Hashable {
    let value: Int
}

在这两种情况下,您都可以免费获得成员初始化程序init(value:)加上==加上hashValue。请注意保留HTML标记。

1
请注意,成员初始化程序是“internal”的,因此如果结构体在另一个模块中定义,则不可用。 - Cristik
@Cristik 谢谢,我不知道这个(尽管我记得4.1注释中有关于初始化程序和其他模块的内容)。 - matt

4

Swift目前(还)没有newtype的概念——基本上是一个不透明类型,与原始类型存储相同的值。

您可以使用包装原始类型的1字段结构体。1字段结构体没有性能损失,同时为您提供了一个独特的类型,具有更多的语义价值(感谢@RobNapier提供的关于Hashable的绝妙提示):

struct EnemyId: Hashable {
    private let value: Int

    init(_ value: Int) { self.value = value }

    static func ==(_ lhs: EnemyId, _ rhs: EnemyId) -> Bool {
        return lhs.value == rhs.value
    }

    var hashValue: Int {
        return value.hashValue
    }
}

struct WeaponId: Hashable {
    private let value: Int

    init(_ value: Int) { self.value = value }

    static func ==(_ lhs: WeaponId, _ rhs: WeaponId) -> Bool {
        return lhs.value == rhs.value
    }

    var hashValue: Int {
        return value.hashValue
    }
}

像这样的类型可以在许多地方使用,就像Int一样,但是它们是不同的。当然,您可以根据需要添加更多的协议。


在大多数情况下,与其取消引用值,不如直接在结构体上提供功能。在许多情况下,“value”应被视为内部实现细节。(如果您需要很多方法,这可能会变得有点乏味,但您将获得更多的价值。希望最终我们能够获得一种解决方案,可以通过特定协议进行传递。(这涉及到相关类型的一些问题,但我们可以抱有希望。) - Rob Napier
不要让这个答案变得无关紧要(在我看来完全正确),但是我添加了一些代码,以展示如果您有一堆这些东西(就像我经常做的那样)可以更普遍地完成这项任务。 - Rob Napier
实际上,在Swift 4.1中,这变得更加容易了。您只需符合Hashable协议,hashValue==就会自动完全生成!您只需要init,如果您不介意在初始化程序中说value:,则可以省略它。 - matt
@Cristik 我喜欢 newtype 的概念,也许它应该被添加到 Swift 中。这正是我正在寻找的。 :) - CodeReaper

2

在我看来,Cristik是完全正确的。如果你只有几个这样的协议,你可以手动编写协议,但是在完全相同的方式下实现半打的Equatable可能会变得有点乏味。如果这种情况发生在你身上,你可以使用这种类型的协议使这种事情更加自动化:

protocol NumberConvertible: CustomStringConvertible, Comparable {
    init(number: NSNumber)
    var numberValue: NSNumber { get }
}

// CustomStringConvertible
extension NumberConvertible {
    var description: String { return numberValue.description }
}

// Comparable
func == <N: NumberConvertible>(lhs: N, rhs: N) -> Bool {
    return lhs.numberValue == rhs.numberValue
}

func < <N: NumberConvertible>(lhs: N, rhs: N) -> Bool {
    return lhs.numberValue.int64Value < rhs.numberValue.int64Value
}

这只是我手边恰巧有的一部分内容,它使用NSNumber与Core Data更轻松地进行交互。显然,您可以构建一个类似的IntConvertible,它可以在不需要NSNumber的情况下以相同的方式工作。但这种类型的协议/扩展使得单字段结构(“类型提升”)更加易于接受。我在许多地方都使用它们来表示ID,就像您描述的那样。
顺便说一句:在Swift中,这些通常非常便宜。结构体没有存储开销,因此包含Int的结构体的内存使用量仅为Int。随着整个模块的优化,Swift通常也可以内联大部分间接操作。

1
您可以尝试使用一个小的包装结构体:

struct EnemyID {
    let value: Int

    init?(_ value: String) {
        guard let intValue = Int(value) else { return nil }
        self.value = intValue
    }
}

...并且对于WeaponID也是同样的情况。


1

在引入newtype之前,您可以使用类似以下的代码:

struct IntID<T> {
    var value: Int
}
...
typealias EnemyID = IntID<Enemy>
typealias WeaponID = IntID<Weapon>
...

let enemyID = EnemyID(value: 1)
let weaponID = WeaponID(value: 1)

在这种情况下,当调用类似于此示例中的blah函数时,它将不允许您出现订单错误。
func blah(enemyID: EnemyID, weaponID: WeaponID) {...}
...
blah(enemyID: enemyID, weaponID: weaponID) //works
blah(enemyID: weaponID, weaponID: enemyID) //doesn't

除此之外,您可以添加符合 ExpressibleByIntegerLiteral 的要求。
extension IntID: ExpressibleByIntegerLiteral {
    init(integerLiteral value: Int) {
        self.value = value
    }
}

这将允许您执行以下操作:
let enemyId: EnemyID = 2
let weaponID: WeaponID = 4

blah(enemyID: enemyID, weaponID: weaponID)
blah(enemyID: 6, weaponID: 7)
// but this one will not compile
let intValue = 42
blah(enemyID: intValue, weaponID: intValue) 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接