Breaking on "Duplicate keys of type were found in a Dictionary"

I'm getting an error Duplicate keys of type 'SetRecord' were found in a Dictionary that I'm struggling to track down. Once the error is triggered, there’s no more stack, so it's not clear exactly when the detection is occurring; certainly it's not happening on insert. I've tried setting breakpoints on things like swift_reportError, but that's too late.

I do some dictionary ops but afaict it's all iterating over immutable containers and then adding to a new mutable container (i.e. I'm not modifying a container while iterating over it). Worse still, it doesn’t happen every time.

Questions

  • When is this check for duplicate keys performed?
  • Is there a way to break on it where it’s more useful?

Is it this one?

That definitely got invoked, but sadly, it’s deep in SwiftUI and I have no idea what’s actually wrong.

And what's especially weird is that there’s a delay before it happens. The UI gets rendered (correctly afaict), and then after a few seconds, this happens, even without further user interaction.

Well… what about starting with the advice in the function?

  • This usually means either that the type violates Hashable's requirements, or
  • that members of such a dictionary were mutated after insertion

Maybe start by investigating and confirming that the SetRecord type would not violate one of those conditions?

1 Like

Yes, this is printed to the log when the runtime error occurs.

If the key type isn't Hashable, then you get an error from the compiler, so I'm not sure how this could ever be a runtime error.

The alleged mutation is happening on a let container in ForEach, so I'm not sure where the mutation could be occurring:

List
{
	let groupedSets = groupSetsByDay(sets: self.sets)
	let sortedKeys = groupedSets.keys.sorted(by: >)
	
	Text("\(self.sets.count) recorded sets")
	
	ForEach(sortedKeys, id: \.self)
	{ inDate in
		if let daySets = groupedSets[inDate]?.sorted(by: { $0.timestamp < $1.timestamp })
		{
			Section("\(inDate.formatted(date: .abbreviated, time: .omitted))")
			{
				ForEach(daySets, id: \.self)
				{ inSetRecord in
					SetRecordListItem(set: inSetRecord)
				}
			}
		}
	}
}

Having said all that, I realize now that the breakpoint is happening in the right place, just Xcode was collapsing everything and I didn’t notice. Now the problem is that it’s deep in SwiftUI, which is incredibly hard to debug.

The error isn’t saying you have failed to implement the conformance to the Hashable protocol, it is saying your implementation of that conformance might be semantically incorrect.

3 Likes

If you have a custom Hashable implementation, it can be incorrect and lead to issues. If you are writing it or the Equatable conformance manually you should post them so we can check.

2 Likes

I don't actually implement the Hashable conformance myself. But I think I may have found the problem: ForEach(daySets, id: \.self) is using the whole SetRecord (which is a SwiftData @Model), and somehow two are hashing to the same value. Changing this to ForEach(daySets, id: \.id) seems to have made the problem go away.

I say “seems,” because the problem has not been 100% reproducible (although it does repro fairly easily, and now I can’t get it to).

I would have assumed the Hashable implementation by PersistentModel incorporated the id, but I guess I can see why it might not.

1 Like

I think this might also point to the other issue is referenced in the crash:

  • that members of such a dictionary were mutated after insertion

It sounds like what is happening is that your "source of truth" is a Swift.Dictionary and the values of keys are mutable object references. Whether or not the pointer identity of the mutable object reference is consistent across time you could still be mutating the data referenced by that pointer. If your Hashable implementation is dependent on this mutable state then it sounds like this could lead to the condition referenced in the crash.

2 Likes

If only I could inspect the actual objects at the time of detection.

Since it's guaranteed to be visible in stack traces, you should be able to breakpoint KEY_TYPE_OF_DICTIONARY_VIOLATES_HASHABLE_REQUIREMENTS, move up the stack, and examine the key with the issue at some point. Unfortunately I have to leave adding the breaking as an exercise for the reader. :pensive_face:

1 Like

It is not illegal for two unequal values to hash to the same value. The problem is if two equal values hash to different values. Do you manually implement Equatable?

5 Likes