Why Synthesized Conformance Is a Lie for Identifiable Types in Swift

Erk EkinErk Ekin
5 min read

Compiler-synthesized conformance in Swift is a powerful tool that accelerates development by reducing boilerplate code. For protocols like Equatable and Hashable, the compiler can automatically generate the required implementations, a convenience that is widely utilized. However, this automation, like any abstraction, comes with trade-offs. The default behavior can introduce significant and non-obvious performance bottlenecks, particularly in systems that rely on a clear concept of object identity.

This analysis examines the default synthesis mechanism, identifies its performance characteristics, and proposes a more robust architectural pattern for any Swift project that uses identifiable data models.


The Performance Cost of Synthesized Conformance

When the Swift compiler synthesizes conformance for Equatable and Hashable, it applies a simple, universal rule: every stored property of the type is included in the comparison or hashing logic. For a struct with several value-type properties, the overhead is often negligible. However, for more complex data structures, the cost can become substantial.

Consider a model that includes a collection:

struct Company {
    let id: UUID
    let name: String
    let address: String
    let employees: [User] // A potentially large collection
}

If Company conforms to Equatable, a comparison between two instances (companyA == companyB) will trigger a member-wise comparison of all properties. This includes iterating through the entire employees array, comparing each User object one by one. This operation has an algorithmic complexity of at least O(N), where N is the number of elements in the largest collection.

In performance-sensitive contexts, such as the diffing algorithms used to update SwiftUI views, these O(N) comparisons can become a significant performance hotspot, leading to dropped frames and unresponsive user interfaces. The same complexity issue applies to Hashable conformance, affecting the performance of Set and Dictionary operations.


The Identifiable Protocol: A Semantic Contract

Modern Swift development, particularly with frameworks like SwiftUI and Combine, heavily utilizes the Identifiable protocol.

public protocol Identifiable {
    associatedtype ID: Hashable
    var id: ID { get }
}

This protocol is not merely a utility; it is a semantic contract. It asserts that a type has a single, canonical property that represents its stable identity. If two instances share the same id, they represent the same conceptual entity, even if their other properties differ (e.g., a mutable state that has been updated).

Herein lies a logical disconnect. The compiler's synthesized Equatable implementation checks every property for equality, while the Identifiable protocol establishes that only the id property is necessary to determine identity. The default behavior is therefore not only computationally expensive but also semantically imprecise.


A Universal Pattern for Identity-Based Conformance

To resolve this, we must align the implementation of equality with the semantic contract of identity. While this can be done by manually implementing the required methods on every conforming type, a more scalable and architecturally sound solution is to establish a project-wide design pattern using protocol extensions.

By providing a default implementation for Equatable and Hashable constrained to Identifiable types, we can create a universal rule.

// A foundational pattern for any project using Identifiable models.

extension Equatable where Self: Identifiable {
  public static func == (lhs: Self, rhs: Self) -> Bool {
    return lhs.id == rhs.id
  }
}

extension Hashable where Self: Identifiable {
  public func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
}

This leverages a key feature of Swift's protocol-oriented design. When a type like Company declares conformance to Hashable and Identifiable, the compiler sees that these constrained extensions already satisfy the protocol requirements. It therefore forgoes its own member-wise synthesis in favor of these more specific, O(1) implementations.


Verifying the Compiler's Behavior

This architectural pattern is necessary because of how the Swift compiler's synthesis mechanism is implemented. An analysis of the Swift open-source repository confirms that the derivation logic for Equatable and Hashable is generalized and does not account for the semantic meaning of Identifiable.

The relevant logic resides in the compiler source file DerivedConformanceEquatableHashable.cpp. When synthesizing conformance, the compiler's primary concern is to verify that all stored properties of a type themselves conform to Hashable before generating the combined hash.

// Refuse to synthesize Hashable if type isn't a struct or enum, or if it
// has non-Hashable stored properties/associated values.
auto hashableProto = Context.getProtocol(KnownProtocolKind::Hashable);
if (!canDeriveConformance(getConformanceContext(), Nominal,
                          hashableProto)) {
  // ...error handling...
}

// see: https://github.com/swiftlang/swift/blob/858383c71d264ec3ff0271bacce52cc25b9820af/lib/Sema/DerivedConformance/DerivedConformanceEquatableHashable.cpp#L987

Same is true for the Equatable.

As the source demonstrates, the validation step is mechanical: it checks for protocol conformance on all members. It does not contain any special-casing to detect if the type also conforms to Identifiable and, if so, to use the id property as the sole source for hashing and equality. The synthesized implementation ignores any semantic relationship between the two protocols.

The compiler will therefore always default to its member-wise synthesis unless a more specific implementation—like the one provided by our extensions—is available to satisfy the protocol requirements first.


Conclusion: A Principled Approach to Equatable and Hashable

Developer productivity tools are valuable, but they should not dictate architectural integrity. For any system where models have a distinct identity, the definition of equality must reflect that identity.

The most effective strategy is to incorporate a universal pattern that enforces this logic consistently. Adopting these extensions as a standard part of your project ensures that all Identifiable types are automatically optimized for both performance and semantic correctness.

// Recommended extensions for robust identity and equality handling.

extension Equatable where Self: Identifiable {
  public static func == (lhs: Self, rhs: Self) -> Bool {
    return lhs.id == rhs.id
  }
}

extension Hashable where Self: Identifiable {
  public func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
}

This approach establishes a more performant and logically consistent foundation for your software.

0
Subscribe to my newsletter

Read articles from Erk Ekin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Erk Ekin
Erk Ekin