如何在Xcode 9.4中使可选类型可哈希化

3
在Xcode 10中,Swift编译器足够智能,可以做到以下几点:
  1. 将包装Hashable值的可选项视为Hashable
  2. Xcode > =9.4也会将包含所有Hashable属性的struct视为Hashable
有趣的是,即使构建语言是Swift 4或者甚至是Swift 3,Xcode 10也会将可选项视为Hashable
以以下代码为例:
struct Foo: Hashable {
    var string1: String?
    var string2: String?
}

这段代码在Xcode 10下编译通过,并且使用Foo对象的hashValue也能按预期工作,即使构建语言是Swift 3!

然而,在Xcode 9.4中,除非将属性设为非可选类型,否则Foo结构体不会自动成为可哈希类型。

在Xcode 9.4/Swift 4.1中,以下代码可以编译并正常工作:

struct Foo: Hashable {
    var string1: String
    var string2: String
}

我该如何在Xcode 9中使可选类型符合Hashable协议呢?我可以自己为我的结构体实现Hashable一致性,但对于多部分结构创建良好的哈希实际上很棘手,我不想担心这个问题。我也想不出一个简单的方法来让可选类型符合Hashable。(再次说明,这只是在Xcode < Xcode 10中存在的问题。我的雇主尚未迁移到Xcode 10,因此我需要适用于Xcode 9.4的解决方案。)

1
你正在混淆Xcode版本和Swift版本。即使你使用Xcode 10,你仍然可以使用Swift 3。我猜你是在问如何在Swift 4.0或Swift 4.1中实现,而不是Swift 4.2?请不要混淆IDE版本和语言/编译器版本,这是两个非常不同的概念。Swift也可以在Xcode之外进行编译(也许你应该尝试一下这个方法来缩小问题范围)。 - Claus Jørgensen
2
我非常了解它们之间的区别。你可能会认为语言版本决定了什么能用,什么不能用,但我进行了专门的测试。结果显示,它似乎是由编译器的版本而不是你在IDE中选择的语言版本所驱动的。在Xcode 10中,即使你选择Swift 3作为你的语言版本,带有可选项的结构体版本也可以编译,而在Xcode 9.4中则无法编译。 - Duncan C
在您升级之前,请使用Sourcery的AutoHashable(https://cdn.rawgit.com/krzysztofzablocki/Sourcery/master/docs/hashable.html)。当您升级后,这很容易被删除并替换为隐式的Hashable。 - Rob Napier
我使用了“Xcode 10”和“Xcode 9.4”,因为我没有关于这些IDE版本中包含的编译器版本信息的准备访问,并且它描述了我发现的情况。如果您有这些信息,那将会很有帮助。(除非我正在使用brew构建项目或在Pi上进行Swift开发,否则我几乎总是在Xcode中使用LLVM Swift编译器。) - Duncan C
尝试在Xcode 9.4中使用包含可选项的结构版本,然后在将语言版本设置为Swift 3的Xcode 10中尝试。你会明白我在说什么。 - Duncan C
显示剩余5条评论
1个回答

3

Swift 4.2Optional.swift中定义了条件Optional: Hashable一致性。

extension Optional: Hashable where Wrapped: Hashable {
  //  ...
  public func hash(into hasher: inout Hasher) {
    switch self {
    case .none:
      hasher.combine(0 as UInt8)
    case .some(let wrapped):
      hasher.combine(1 as UInt8)
      hasher.combine(wrapped)
    }
  }
}

具体来说:

  • .none 的哈希值与整数零的哈希值相同,与类型无关,并且
  • .some(wrapped) 的哈希值是将wrapped的哈希值与整数1的哈希值混合得到的。

Swift 4.1(附带Xcode 9.4)已经具备了实现类似方法所需的必要工具:

  • Conditional conformance, i.e. we can define an extension

    extension Optional: Hashable where Wrapped: Hashable
    
  • Automatic synthesis of Hashable for types if all members are Hashable.

这是一个可能的实现:

extension Optional: Hashable where Wrapped: Hashable {
    struct Combiner: Hashable {
        let left: Int
        let right: Wrapped
    }

    public var hashValue: Int {
        switch self {
        case .none:
            return 0.hashValue
        case .some(let wrapped):
            return Combiner(left: 1, right: wrapped).hashValue
        }
    }
}

例子:

struct Foo: Hashable {
    var string1: String?
    var string2: String?
}

let foo = Foo(string1: "1", string2: "2")
print(foo.hashValue) // 2171695307022640119

为了让此代码也能与Swift 4.2及更高版本一起编译,可以有条件地编译扩展方法(请参考SE-0212 编译器版本指令)。
#if swift(>=4.1.50) || (swift(>=3.4) && !swift(>=4.0))
// Code targeting the Swift 4.2 compiler and above:
// Conditional conformance `Optional: Hashable` defined in the standard library.
#elseif swift(>=4.1) || (swift(>=3.3) && !swift(>=4.0))
// Code targeting the Swift 4.1 compiler and above:

extension Optional: Hashable where Wrapped: Hashable {
    // ...
}

#endif

太棒了(投票)。这正是我在寻找的。不过有一个问题:在 Swift 4.2 版本的 Optional 的 Hashable 扩展中,对于 .some 情况,它会组合 1 的哈希值和包装值的哈希值。(我认为这是为了使 fooOptional<Foo> 具有不同的哈希值。)你的版本没有包括这一步。难道不应该吗? - Duncan C
@DuncanC:已经可以了,使用Combiner(left: 1, right: wrapped).hashValue。哈希组合函数不是公共的,因此我使用了本地结构的“技巧”。您可以检查foo.hashValue(foo as Foo?).hashValue是不同的。- 不用谢,找出来很有趣! - Martin R

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