skip to Main Content

I’ve just updated to Xcode 16 and my project is littered with warnings due to the change to mark AnyHashable as not-sendable. I was relying on AnyHashable throughout my app, creating a ViewModel layer that returned a datasource of type UITableViewDiffableDataSource<Int, AnyHashable>. Now i’m trying to build something that is Hashable and Sendable, and can be slotted in without much refactoring. Going off other examples online I created this new struct modeled after AnyHashable

public struct AnyHashableSendable: Hashable, Sendable {
    
    public let base: any Hashable & Sendable
    
    public init<H>(_ base: H) where H : Hashable {
        self.base = base
    }
    
    public static func == (lhs: Self, rhs: Self) -> Bool {
        AnyHashable(lhs.base) == AnyHashable(rhs.base)
    }
    
    public func hash(into hasher: inout Hasher) {
        base.hash(into: &hasher)
    }
}

But this current implementation comes with a few annoyances that i’m looking to resolve in a way that matches the apple implementation

  1. If I have a variable foo of type AnyHashable and I want to cast it, I can simply do let newBar = foo as? bar. But with a variable of type AnyHashableSendable I now must call the property base to achieve the same, like so let newBar = foo.base as? bar. How does the inbuilt AnyHashable avoid the need to call its base property?

  2. If I define an array or 2d array of AnyHashable ([AnyHashable], [[AnyHashable]]) I can append any struct that conforms to hashable and it will automatically cast it. E.g.

var array: [AnyHashable] = []
array.append(FooBar(prop1: "test", prop2: "test"))

But with AnyHashableSendable, each item must be initialised as the new object type, e.g.

var array: [AnyHashableSendable] = []
array.append(.init(FooBar(prop1: "test", prop2: "test")))

I’ve tried to solve at least the second one by creating my own custom collection type, with overridden functions that accept AnyHashable and simply call the init and store it. While it works, the differences require a decent bit of refactoring and annoying custom code/types instead of the existing logic were used too. Is there a way i’m unaware of to solve these two issues so that my new type functions like the existing AnyHashable?

2

Answers


  1. It is not possible to create such a type yourself. Both behaviours you mentioned (downcasting directly, and implicit conversion to AnyHashable) are built into the compiler directly.


    From the documentation on dynamic casting in the Swift GitHub repo, AnyHashable behaves like an existential type in the context of casting, even though it is a struct. The cast operator can "reach inside" the existential "box" and get the actual value wrapped in there.

    You can find a special case for AnyHashable in DynamicCast.cpp, there is a call to _swift_anyHashableDownCastConditionalIndirect. This function is where the "reaching in to the box" happens. See its declaration in AnyHashable.swift.


    For the implicit conversion to AnyHashable, the compiler inserts a compiler intrinsic function called _convertToAnyHashable, which is also declared in AnyHashable.swift, and I think this is the line that emits it.

    Login or Signup to reply.
  2. I encountered the same issue, and I came up with this solution:

    struct AnySendableHashable: @unchecked Sendable, Hashable {
        private let wrapped: AnyHashable
        
        init(_ base: some Hashable & Sendable) {
            self.wrapped = .init(base)
        }
    }
    

    So, the assumption is, that when we initialise a AnyHashable with a value conforming to Hashable and Sendable, then the resulting "Box" (AnyHashable) becomes Sendable as well.

    I appreciate comments, whether this is legal, or when there are potential pitfalls with this solution.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search