Swift 中的字符串子类解决方法

String subclass workaround in Swift

在我的应用程序中,我使用了各种结构,这些结构基本上应该只是 String 的子类,但 String 本身就是一个结构,因此不可能对其进行子类化。例如,我有一个带有单个 address 变量的电子邮件对象,然后是 isValid 等计算变量或 compose() 等函数。关键是,当整个对象实际上应该是地址时,通过 address 属性 访问实际电子邮件地址似乎很荒谬。

那么创建 Email 对象的正确方法是什么?这是唯一的方法吗?或者有没有办法绕过子类化字符串,以便我的电子邮件结构实际上是一个字符串,只包含一些特定于电子邮件的函数?

我会使用这样的东西:

import Foundation

struct EmailAddress {
    let stringValue: String

    static func isValidEmailAddress(_ s: String) -> Bool {
        //implement me
        return true
    }

    init?(fromString s: String) {
        guard EmailAddress.isValidEmailAddress(s) else { return nil }
        self.stringValue = s
    }

    var localPart: String {
        return stringValue.components(separatedBy: "@").first!
    }

    var domain: String {
        return stringValue.components(separatedBy: "@").last!
    }

    // expose other functions that make sense to e-mail addresses.
    // importantly, this doesn't expose *all* String methods,
    // most of which wouldn't make sense in the context of a String
}

我相信 extension 也是您的想法,但是如果您对 Email 对象使用名称 String 感到不自在...那么我只是建议以下内容:

protocol Email {
    func compose() -> Email
    var isValidEmail: Bool
}

extension String: Email {
    func compose() -> Email {
         return something...
    }
    ...
}

var a: Email = "abc"
a.compose()

但是,这当然不能消除您对 StringEmail 功能有联系的疑虑。

如果你的子类没有属性,只有辅助函数,我建议使用 typealias

typealias EmailString = String

extension EmailString {
   func isValid() -> Bool { ... }
}

而且我认为电子邮件不应该具有 compose 功能。

作为上述 Alexander 的替代方案,如果您想在 Codable 符合 API 的模型之一中使用此自定义类型:

struct SomeModel: APIModel, Codable {
  let id: String
  let emailAddress: EmailAddress?
  
  enum CodingKeys: String, CodingKey {
    case id
    case emailAddress = "email_address"
  }
}

您的 EmailAddress 类型可能如下所示:

struct EmailAddress: RawRepresentable, Codable {
  typealias RawValue = String
  
  let rawValue: RawValue

  init?(rawValue: RawValue) {
    guard isValidEmail(rawValue) else { return nil }
    self.rawValue = rawValue
  }
  
  var localPart: String {
    rawValue.components(separatedBy: "@").first!
  }

  var domain: String {
    rawValue.components(separatedBy: "@").last!
  }

  // expose other functions that make sense to e-mail addresses.
  // importantly, this doesn't expose *all* String methods,
  // most of which wouldn't make sense in the context of a String
}

然后可能会有 ff 结果:

1.有效的电子邮件地址

let model = try! SomeModel.decode([
  "id": "xyz",
  "email_address": "user@domain.com"
])

print(model.emailAddress)            // Optional(EmailAddress(rawValue: "user@domain.com"))
print(model.emailAddress?.localPart) // Optional("user")
print(model.emailAddress?.domain)    // Optional("domain.com")

2。无效的电子邮件地址

(不知道为什么服务器会给你一个无效的电子邮件地址。但可能。)

let model = try! SomeModel.decode([
  "id": "xyz",
  
  // Fatal error: "Cannot initialize EmailAddress from invalid String value foo.bar"
  "email_address": "foo.bar"
])

3。完全没有价值

let model = try! SomeModel.decode([
  "id": "xyz"
])
print(model.emailAddress) // nil

CodableRawRepresentable 协议配对的好处是它隐式调用后者的可失败初始化器并抛出 DecodingError.dataCorrupted 万一失败了。