在 Swift 中,我可以使用元组作为字典中的键吗?

Posted

技术标签:

【中文标题】在 Swift 中,我可以使用元组作为字典中的键吗?【英文标题】:In Swift can I use a tuple as the key in a dictionary? 【发布时间】:2014-07-30 15:29:15 【问题描述】:

我想知道是否可以以某种方式使用 x, y 对作为我的字典的键

let activeSquares = Dictionary <(x: Int, y: Int), SKShapeNode>()

但我得到了错误:

Cannot convert the expression's type '<<error type>>' to type '$T1'

和错误:

Type '(x: Int, y: Int)?' does not conform to protocol 'Hashable'

那么..我们怎样才能使它符合?

【问题讨论】:

我确定(x:Int,y:Int) 不是可哈希的东西:) 我认为你应该只创建一个以这种方式建模的结构类型并使其符合Hashable @sjeohp 他们不能,你可以简单地使用结构体创建一个简单的值类型。 元组不能用作键(或者具体来说,不是hashable)的原因是因为它们不是严格不可变的。如果您使用let 定义它,它是不可变的,但如果您使用var 定义它,它不是。在字典中用作键的散列必须根据散列表的定义是不可变的,并且由于任何类型的合理散列将直接依赖于容器内的值,因此不能对可变容器进行散列(同样,不可变容器包含可变对象不能被合理地散列)。 @aruisdante 元组的可变性是无关紧要的——它们是值类型,因此,抛开散列性不谈,它将是存储在字典中的元组的不可变副本。唯一的问题是元组不符合Hashable 并且不能扩展为这样做。元组并不比字符串或Ints 更易变,它们就像字典键一样。 【参考方案1】:

Dictionary 的定义是struct Dictionary&lt;KeyType : Hashable, ValueType&gt; : ...,即密钥的类型必须符合协议Hashable。但是语言指南告诉我们protocols can be adopted by classes, structs and enums,即不是元组。因此,元组不能用作Dictionary 键。

一种解决方法是定义一个包含两个 Int 的可散列结构类型(或任何您想放入元组中的内容)。

【讨论】:

假设我在两个 Int 属性中创建了一个唯一的可散列结构。我应该如何将两个整数的组合映射到唯一的哈希? “x^y”? "x 哇,这似乎完全错过了本可以是一个很棒的语言功能。我的意思是,我没有理由认为他们不能通过组合其组件的哈希值来使元组隐式可哈希。这将是避免二维字典的一种非常快捷的方法! 完全应该是一个特性,如果更高版本的 Swift 没有添加它,我会感到惊讶(尽管 v4 仍然没有它)。在 Python 中,hashable 元组是自动可散列的。 该提案已被接受:github.com/apple/swift-evolution/blob/master/proposals/… @devios1 这也许是 C++ 使用比较优于散列的原因之一。它更容易为元组等数据结构组合可比较的操作。【参考方案2】:

正如上面的答案所述,这是不可能的。但是您可以使用 Hashable 协议将元组包装到通用结构中作为一种解决方法:

struct Two<T:Hashable,U:Hashable> : Hashable 
  let values : (T, U)

  var hashValue : Int 
      get 
          let (a,b) = values
          return a.hashValue &* 31 &+ b.hashValue
      
  


// comparison function for conforming to Equatable protocol
func ==<T:Hashable,U:Hashable>(lhs: Two<T,U>, rhs: Two<T,U>) -> Bool 
  return lhs.values == rhs.values


// usage:
let pair = Two(values:("C","D"))
var pairMap = Dictionary<Two<String,String>,String>()
pairMap[pair] = "A"

【讨论】:

您能解释一下&amp;* 31 &amp;+ 部分的作用吗? &* 和 &+ 与正常操作 * 和 + 类似,但具有溢出错误保护(因此在溢出的情况下不会引发错误) 在 Swift 3 中,== 已移至结构内的静态函数:static func ==&lt;T: Hashable, U: Hashable&gt;(...) -&gt; Bool 很棒的答案。使用对是一种以一般方式处理此问题的好方法。 从 Swift 4.1 开始,现在可以在一个结构或枚举上为您合成哈希实现,其值都是可哈希的 - 只需通过扩展 Hashable 协议来选择加入。见github.com/apple/swift-evolution/blob/master/proposals/…。在 swift 4.2 中还有 hash(into:) 函数来处理组合哈希:github.com/apple/swift-evolution/blob/master/proposals/…。开发文档也已更新,请参阅“符合 Hashable 协议”。【参考方案3】:

不幸的是,从 Swift 4.2 开始,标准库仍然没有为元组提供对 Hashable 的条件一致性,编译器不认为这是有效代码:

extension (T1, T2): Hashable where T1: Hashable, T2: Hashable 
  // potential generic `Hashable` implementation here..

此外,以元组作为字段的结构、类和枚举不会自动合成Hashable

虽然其他答案建议使用数组而不是元组,但这会导致效率低下。元组是一种非常简单的结构,由于元素的数量和类型在编译时是已知的,因此可以很容易地对其进行优化。 Array 实例几乎总是preallocates more contiguous memory 以适应要添加的潜在元素。此外,使用Array 类型会强制您使项目类型相同或使用类型擦除。也就是说,如果你不关心效率低下,(Int, Int) 可以存储在[Int] 中,但(String, Int) 需要类似[Any] 的东西。

我发现的解决方法依赖于 Hashable 确实会自动合成单独存储的字段这一事实,因此即使不手动添加 HashableEquatable 实现,如 Marek Gregor's answer 中那样,此代码也可以工作:

struct Pair<T: Hashable, U: Hashable>: Hashable 
  let first: T
  let second: U

【讨论】:

【参考方案4】:

我在一个应用程序中创建了这段代码:

struct Point2D: Hashable
    var x : CGFloat = 0.0
    var y : CGFloat = 0.0

    var hashValue: Int 
        return "(\(x),\(y))".hashValue
    

    static func == (lhs: Point2D, rhs: Point2D) -> Bool 
        return lhs.x == rhs.x && lhs.y == rhs.y
    


struct Point3D: Hashable
    var x : CGFloat = 0.0
    var y : CGFloat = 0.0
    var z : CGFloat = 0.0

    var hashValue: Int 
        return "(\(x),\(y),\(z))".hashValue
    

    static func == (lhs: Point3D, rhs: Point3D) -> Bool 
        return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z
    



var map : [Point2D : Point3D] = [:]
map.updateValue(Point3D(x: 10.0, y: 20.0,z:0), forKey: Point2D(x: 10.0, 
y: 20.0))
let p = map[Point2D(x: 10.0, y: 20.0)]!

【讨论】:

很好的例子?【参考方案5】:

如果您不介意效率低下,您可以轻松地将元组转换为字符串,然后将其用作字典键...

var dict = Dictionary<String, SKShapeNode>() 

let tup = (3,4)
let key:String = "\(tup)"
dict[key] = ...

【讨论】:

【参考方案6】:

无需特殊代码或幻数即可实现 Hashable

Swift 4.2 中的可散列:

struct PairKey: Hashable 

    let first: UInt
    let second: UInt

    func hash(into hasher: inout Hasher) 
        hasher.combine(self.first)
        hasher.combine(self.second)
    

    static func ==(lhs: PairKey, rhs: PairKey) -> Bool 
        return lhs.first == rhs.first && lhs.second == rhs.second
    

更多信息:https://nshipster.com/hashable/

【讨论】:

【参考方案7】:

我建议实现结构并使用类似于boost::hash_combine 的解决方案。

这是我使用的:

struct Point2: Hashable 

    var x:Double
    var y:Double

    public var hashValue: Int 
        var seed = UInt(0)
        hash_combine(seed: &seed, value: UInt(bitPattern: x.hashValue))
        hash_combine(seed: &seed, value: UInt(bitPattern: y.hashValue))
        return Int(bitPattern: seed)
    

    static func ==(lhs: Point2, rhs: Point2) -> Bool 
        return lhs.x == rhs.x && lhs.y == rhs.y
    


func hash_combine(seed: inout UInt, value: UInt) 
    let tmp = value &+ 0x9e3779b97f4a7c15 &+ (seed << 6) &+ (seed >> 2)
    seed ^= tmp

它比使用字符串作为哈希值要快得多。

如果您想了解更多关于magic number的信息。

【讨论】:

【参考方案8】:

将扩展文件添加到项目(View on gist.github.com):

extension Dictionary where Key == Int64, Value == SKNode 
    func int64key(_ key: (Int32, Int32)) -> Int64 
        return (Int64(key.0) << 32) | Int64(key.1)
    

    subscript(_ key: (Int32, Int32)) -> SKNode? 
        get 
            return self[int64key(key)]
        
        set(newValue) 
            self[int64key(key)] = newValue
        
    

声明:

var dictionary: [Int64 : SKNode] = [:]

用途:

var dictionary: [Int64 : SKNode] = [:]
dictionary[(0,1)] = SKNode()
dictionary[(1,0)] = SKNode()

【讨论】:

【参考方案9】:

或者只是使用数组来代替。我正在尝试执行以下代码:

let parsed:Dictionary<(Duration, Duration), [ValveSpan]> = Dictionary(grouping: cut)  span in (span.begin, span.end) 

这让我看到了这篇文章。看完这些,失望了(因为如果他们可以通过只采用协议而不做任何事情来合成EquatableHashable,他们应该可以对元组做到这一点,不是吗?),我突然意识到,只需使用数组然后。不知道它的效率如何,但这种改变效果很好:

let parsed:Dictionary<[Duration], [ValveSpan]> = Dictionary(grouping: cut)  span in [span.begin, span.end] 

我更一般的问题变成了“那么为什么元组不是像数组这样的一流结构呢?Python 成功了(鸭子然后跑)。”

【讨论】:

【参考方案10】:

你还不能使用Swift 5.3.2,但是你可以使用Array代替元组:

var dictionary: Dictionary<[Int], Any> = [:]

而且用法很简单:

dictionary[[1,2]] = "hi"
dictionary[[2,2]] = "bye"

它还支持任何尺寸:

dictionary[[1,2,3,4,5,6]] = "Interstellar"

【讨论】:

【参考方案11】:
struct Pair<T:Hashable> : Hashable 
    let values : (T, T)
    
    init(_ a: T, _ b: T) 
        values = (a, b)
    
    
    static func == (lhs: Pair<T>, rhs: Pair<T>) -> Bool 
        return lhs.values == rhs.values
    
    
    func hash(into hasher: inout Hasher) 
        let (a, b) = values
        hasher.combine(a)
        hasher.combine(b)
    


let myPair = Pair(3, 4)

let myPairs: Set<Pair<Int>> = set()
myPairs.update(myPair)

【讨论】:

以上是关于在 Swift 中,我可以使用元组作为字典中的键吗?的主要内容,如果未能解决你的问题,请参考以下文章

我可以使用 NSPredicate 作为 NSDictionary 中的键吗

我可以使用对象(类的实例)作为 Python 中的字典键吗?

我可以使用字符串数组作为记录集中的键吗?

Javascript:使用元组作为字典键

我可以使用成员变量作为 hash_set/hash_map 的键吗?

Python:可以将自定义类用作字典的键吗? [重复]