如何在Swift中使用Sequence和IteratorProtocol实现类属性可迭代并完成实例属性匹配对比
Hey there! Let's work through your problem step by step. You want a Contact class that's iterable (no reflection/mirroring allowed), plus a method to compare two instances and spot matching properties. Let's fix your code, address your questions, and find a clean solution.
First, Fixing Your Iterable Contact Class
Your original code has a key issue: the private var properties = [givenName, familyName, middleName] line stores copies of the initial empty strings, not references to the instance's actual properties. When you update givenName later, the array doesn't reflect those changes. Also, it only holds values, not property names—so you can't tell which property matched during comparison.
Let's build a proper implementation using Sequence and IteratorProtocol, with a helper type to track both property names and values:
// Helper type to hold a property's name and its current value struct ContactProperty { let name: String let value: String } class Contact: Sequence { // Define our iterator and element types for the Sequence protocol typealias Iterator = ContactIterator typealias Element = ContactProperty // Our contact properties var givenName: String = "" var familyName: String = "" var middleName: String = "" // Required method for Sequence: create our custom iterator func makeIterator() -> ContactIterator { return ContactIterator(contact: self) } // Class method to find matching properties between two contacts static func findMatchingProperties(between left: Contact, and right: Contact) -> [String] { var matchingProperties = [String]() // Iterate through left's properties and compare to right's for property in left { // Get the corresponding value from right based on property name let rightValue: String? switch property.name { case "givenName": rightValue = right.givenName case "familyName": rightValue = right.familyName case "middleName": rightValue = right.middleName default: rightValue = nil } // If values match, add the property name to our results if let rightVal = rightValue, rightVal == property.value { matchingProperties.append(property.name) } } return matchingProperties } } // Custom iterator to loop through Contact's properties in order struct ContactIterator: IteratorProtocol { typealias Element = ContactProperty private let contact: Contact private var currentIndex = 0 init(contact: Contact) { self.contact = contact } mutating func next() -> ContactProperty? { defer { currentIndex += 1 } // Return the next property in sequence, or nil when done switch currentIndex { case 0: return ContactProperty(name: "givenName", value: contact.givenName) case 1: return ContactProperty(name: "familyName", value: contact.familyName) case 2: return ContactProperty(name: "middleName", value: contact.middleName) default: return nil } } }
Test this implementation with sample data:
let leftContact = Contact() leftContact.givenName = "John" leftContact.familyName = "Doe" let rightContact = Contact() rightContact.givenName = "John" rightContact.middleName = "Michael" let matches = Contact.findMatchingProperties(between: leftContact, and: rightContact) print("Matching properties: \(matches)") // Output: Matching properties: ["givenName"]
Now you can iterate over a Contact instance directly too:
for prop in leftContact { print("\(prop.name): \(prop.value)") } // Output: // givenName: John // familyName: Doe // middleName:
Answering Your Questions
1. Can we implement this with an enum?
Absolutely! Using an enum with CaseIterable makes the code even cleaner and more type-safe. It eliminates manual index tracking and centralizes our property definitions:
// Enum listing all contact fields, conforming to CaseIterable for easy iteration enum ContactField: CaseIterable { case givenName, familyName, middleName // Human-readable property name var name: String { switch self { case .givenName: return "givenName" case .familyName: return "familyName" case .middleName: return "middleName" } } // Fetch the corresponding value from a Contact instance func value(from contact: Contact) -> String { switch self { case .givenName: return contact.givenName case .familyName: return contact.familyName case .middleName: return contact.middleName } } } class Contact: Sequence { // Reuse the enum's iterator and element type typealias Iterator = ContactField.Iterator typealias Element = ContactField var givenName: String = "" var familyName: String = "" var middleName: String = "" func makeIterator() -> Iterator { // Let the enum handle iteration logic return ContactField.allCases.makeIterator() } static func findMatchingProperties(between left: Contact, and right: Contact) -> [String] { // Filter matching fields and map to their names return ContactField.allCases.filter { field in field.value(from: left) == field.value(from: right) }.map { $0.name } } }
This approach is more maintainable—if you add a new property later, you just add a new case to the enum, and everything else updates automatically.
2. Is there a protocol that lets us iterate properties via hashes/identifiers?
Swift's standard library doesn't have a built-in protocol for this, since property order is a class-specific detail. However, the CaseIterable enum approach we just used is a great alternative: it lets you define the exact order of properties you want to iterate, and uses the enum's cases as type-safe "identifiers" instead of raw hashes or strings.
If you don't want to use an enum, you could manually maintain an array of property names (e.g., ["givenName", "familyName", "middleName"]) and use that to drive iteration—but this is error-prone (typos in property names will break things) and less type-safe than the enum method.
Why Your Original Code Failed
- The
propertiesarray stored copies of initial empty strings, not references to the instance's live properties. Updates togivenNamedidn't affect the array. - Your
NameandNameIteratorstructs weren't connected to theContactclass, so they didn't enable iteration ofContactproperties. - There was no way to link property values back to their names, making meaningful comparison impossible.
内容的提问来源于stack exchange,提问作者daniel

