Swift: Как сделать код дженериков более чистым

#ios #swift #generics #types #type-alias

Вопрос:

Я ищу способ сделать свой код более чистым и эффективным. У меня есть «BaseInfo», у него есть некоторые свойства, а также он является родителем классов «SomeInfo» и «AnotherInfo», которые имеют собственные дополнительные свойства. Я создал универсальную функцию, которая сохраняет и получает эти объекты из пользовательских действий. И у меня есть ViewController, который сохраняет и загружает информацию с помощью этих функций, учитывая тип информации, которую он должен использовать. Я хочу знать, есть ли какой-либо способ сделать мой код чище и избавиться от приведения типов в моем окне просмотра. Вот мои информационные классы:

 public class  BaseInfo: Codable{
    var name: String?
    
    init(_ dict: [String: Any]){
        
        name = dict["name"] as? String
    }
}

class AnotherInfo: BaseInfo {
    var secondName: String?
    
    override init(_ dict: [String : Any]) {
        super.init(dict)
        secondName = dict["secondName"] as? String
    }
    
    required init(from decoder: Decoder) throws {
        fatalError("init(from:) has not been implemented")
    }
}

class SomeInfo: BaseInfo {
    var middleName: String?
    
    
    override init(_ dict: [String : Any]) {
        super.init(dict)
        middleName = dict["middleName"]
    }
    
    required init(from decoder: Decoder) throws {
        fatalError("init(from:) has not been implemented")
    }
}
 

Вот мой класс, который управляет информацией

 protocol InfoManagerProtocol {
    static func getInfo(with type: InfoType) -> BaseInfo?
    static func saveInfo <T: BaseInfo>(with type: InfoType, info: T)
}

class InfoManager: InfoManagerProtocol {
    
    static func getInfo<T: BaseInfo>(with type: InfoType) -> T? {
        if let data = UserDefaults.standard.object(forKey: type.toString()) as? Data, let info = try? JSONDecoder().decode(type.typeOfInfo() as! T.Type, from: data){
            return info
        }
        return nil
    }
    
    
    static func saveInfo<T>(with type: InfoType, info: T) where T : BaseInfo {
        if let encodedData = try? JSONEncoder().encode(info){
            UserDefaults.standard.setValue(encodedData, forKey: type.toString())
        }
    }
}
 

Перечисление инфотипов:

 enum InfoType: Int{
    case base = 1
    case another = 2
    case some = 3
}

extension InfoType{

    func toString() -> String{
      switch self{
       case .base: 
           return "base"
       case .another: 
           return "another"
       case .some:
           return "some"
      }
    } 
    func typeOfInfo() -> BaseInfo.Type{
        switch self{
        case .base:
            return BaseInfo.self
        case .another:
            return AnotherInfo.self
        case .some:
            return SomeInfo.self
        }
    }
}
 

и какой-то контроллер

 class ViewCV: UIViewController{
    ......
    // some outlets
    var infoType: InfoType? // info 

    // some func that updates UI
    func updateUI(){
       
       let genericInfo = InfoManager.getInfo(info: .infoType)
       self.someNameOutletLabel.text = genericInfo.name
       self.secondNameOutletLabel?.text = (genericInfo as? AnotherInfo).secondName // here I don't like it
       if let someInfo = genericInfo as? SomeInfo{
          self.secondNameOutletLabel?.text = someInfo.thirdName // or like here I also don't like
       }
   }
}
 

С нетерпением ждем других критиков и советов

Ответ №1:

Вы можете упростить свой код, пропустив InfoType тип и вместо этого определив используемый тип по заданному параметру или возвращаемому значению.

Таким образом, протокол становится

 protocol InfoManagerProtocol {
    static func getInfo<T: BaseInfo>() -> T?
    static func saveInfo <T: BaseInfo>(info: T)
}
 

а затем реализация. Обратите внимание, что вместо toString перечисления я теперь использую само имя класса в качестве ключа

 class InfoManager: InfoManagerProtocol {
    static func getInfo<T: BaseInfo>() -> T? {
        if let data = UserDefaults.standard.object(forKey: "(T.self)") as? Data, let info = try? JSONDecoder().decode(T.self, from: data){
            return info
        }
        return nil
    }

    static func saveInfo<T>(info: T) where T : BaseInfo {
        if let encodedData = try? JSONEncoder().encode(info){
            print(encodedData)
            UserDefaults.standard.set(encodedData, forKey: "(T.self)")
        }
    }
}
 

Вот пример того, как его использовать (при условии, что init(from:) он был правильно реализован)

 let dict: [String: String] = ["name": "Joe", "secondName": "Doe", "middleName": "Jr"]
let another = AnotherInfo(dict)
let some = SomeInfo(dict)

//Here the declaration of the argument tells the generic function what T is
InfoManager.saveInfo(info: another)
InfoManager.saveInfo(info: some)

//And here the declaration of the return value tells the generic function what T is
if let stored:AnotherInfo = InfoManager.getInfo() {
    print(stored, type(of: stored))
}
if let stored:SomeInfo = InfoManager.getInfo() {
    print(stored, type(of: stored))
}
 

Комментарии:

1. Не используйте setValue , вы должны использовать func set(_ value: Any?, forKey defaultName: String) . Кстати, у UserDefaults есть определенный метод для получения данных, называемый data(forKey:)

2. Было бы гораздо лучше сделать так, чтобы методы InfoManager выбрасывались, а не выходили из строя молча

3. @JoakimDanielson set вместо setValue определенно не просто улучшение. Не стесняйтесь оставлять все как есть, если хотите. Это даже не указано в документации Apple для установки значения UserDefaults UserDefaults