Инициализаторы дженериков


#1

Приветствую. Требуется помощь более опытного зала :wink:

Дано:

  1. Есть модели данных, которые очень между собой похожи, для обработки данных от Firestore.
    Например юзер и задача. Естественно отличаются параметрами:

    class MainUser: SomeProtocol {
        let uid: String
        var email: String
        ...
        var dictionary: [String: Any ] {
             return ["uid": uid, "email": email ...]
        }
    
        init (uid: String, email: String ...) {
            self.uid = uid
            self.email = email
        }
        convenience init ?(dictionary: [String: Any]) {
             var uid: String
             var email: String
    
             if let uidInit = dictionary["uid"] as ? String {uid = uidInit} else {uid = ""}
             if let emailInit = dictionary["email"] as ? String {email = emailInit} else {email = ""}
             
             self.init(uid: uid, email: email)
         }
    
    }
    
    class Task: SomeProtocol {
        let uid: String
        var title: String
        ...
        var dictionary: [String: Any ] {
             return ["uid": uid, "title": title ...]
        }
    
        init (uid: String, title: String ...) {
            self.uid = uid
            self.title = title
            ...
        }
        convenience init ?(dictionary: [String: Any]) {
             var uid: String
             var title: String
             ...
             if let uidInit = dictionary["uid"] as ? String {uid = uidInit} else {uid = ""}
             if let emailInit = dictionary["email"] as ? String {email = emailInit} else {email = ""}
             ...
             self .init(uid: uid, email: email...)
         }
    }
    

Протокол с чем-нибудь )))

protocol SomeProtocol {
   var dictionary: [String: Any ] { get }
}
  1. Есть типовые запросы к БД по чтению и созданию документов, например юзера:

    func getUserDoc(collection: CollectionReference, document: String, completion: @escaping(MainUser?, Error?)->Void) {
         var user: MainUser?
         collection.document(document).getDocument { (snapshot, error) in
             if let snapshot = snapshot {
                 user = MainUser(dictionary: snapshot.data() ?? [:])
             }
             completion(user, error)
         }
    }
    

При таких одинаковых запросах, хотелось бы исопльзовать дженерик вроде:

func getDoc<T: SomeProtocol>(collection: CollectionReference, document: String, completion: @escaping(T?, Error?)->Void) {
        var tempData: T?
        collection.document(document).getDocument { (snapshot, error) in
            if let snapshot = snapshot {
                user = T(dictionary: snapshot.data() ?? [:]) //  Вот здесь проблема
            }
            completion(tempData, error)
        }
}

Но естетсвенно компилятор ругается, тк инициализаторы по словарю разные в моделях (а они всегда разные, тк полный набор параментров разный). Как это можно победить именно в рамках использования дженерика?


#2

Добавьте в протокол init?(dictionary: [String: Any])
А ругается из-за того, что нету гарантии того, что у вашей модели есть такой инициализатор, поэтому нужно явно указать его в протоколе, что бы дженерик был уверен, что он будет.

Вот пример со структурами

protocol SomeProtocol {
   var dictionary: [String: Any ] { get }
   init?(dictionary: [String: Any])
}

struct S1: SomeProtocol {
    var a: String

    var dictionary: [String: Any] {
        return ["a": a]
    }

    init?(dictionary: [String: Any]) {
        a = dictionary["a"] as? String ?? ""
    }
}

struct S2: SomeProtocol {
    var b: Int

    var dictionary: [String: Any] {
        return ["b": b]
    }

    init?(dictionary: [String: Any]) {
        b = dictionary["b"] as? Int ?? 0
    }
}

func getData<T: SomeProtocol>(data: [String: Any]) -> T? {
    var model: T?

    model = T(dictionary: data)

    return model
}

#3

Всё работает - большое спасибо. Не заметил очевидного )))


#4

Я сперва тоже не придал должного значения протоколу. Смотрел именно на дженерик.
Думаю, если бы вы указали ошибку, которую Xcode выдавал, было бы гораздо быстрее. Там как раз и писалось об этом.


#5

Что-то я опять запутался на ровном месте:

вот статичный метод в классе менеджера:

static func getOneDocument<T: DictionaryInit>(collection: CollectionReference, document: String, completion: @escaping(T?, Error?)->Void) {
    var tempData: T?
    collection.document(document).getDocument { (snapshot, error) in
        if let snapshot = snapshot {
            tempData = T(dictionary: snapshot.data() ?? [:])
        }
        completion(tempData, error)
    }
}

Он собирается и ошибок не выдаёт. Входной параметр document - это String.

Когда пытаюсь применить метод, то компилятор почему-то от аргумента этого параметра требует соответсвие протоколу дженерика? Зачем и почему, если я и так явно указываю String? Да мне и надо String.

Argument type 'String' does not conform to expected type 'DictionaryInit'

Подозреваю, что при вызове метода тут надо явно передать в метод какой именно тип будет использоваться в дженерике в этом конкретном вызове метода. Как это правильно сделать (изменить метод)?


#6

В общем понятно, что для дженерика надо передать объект определённого типа в соотвествии с ограничением протокола. Но как передать только тип, если мне не надо передавать объект этого типа, а надо только указать тип, который нужно вернуть?


#7

Нужно явно указать тип у параметров в замыкании:

protocol CollectionReference {}
struct CollectionImpl: CollectionReference {}


protocol DictionaryInit {}
struct Dict1: DictionaryInit {}
struct Dict2: DictionaryInit {}


func getOneDocument<T: DictionaryInit>(collection: CollectionReference, document: String, completion: @escaping (T?, Error?) -> Void) {}
getOneDocument(collection: CollectionImpl(), document: "") { (dict: Dict1?, err: Error?) in
   
}

И в swift 5 появился Result, он как раз для подобных целей:

func getOneDocument<T: DictionaryInit>(collection: CollectionReference, document: String, completion: @escaping (Result<T, Error>) -> Void) {}
getOneDocument(collection: CollectionImpl(), document: "") { (result: Result<Dict1, Error>) in
    switch result {
    case let .success(dict):
        print(dict)
    case let .failure(err):
        print(err)
    }
    // Или можно так
    do {
        let dict = try result.get()
        print(dict)
    } catch {
        print(error)
    }
}


#8

Вот спасибо! Опять в открытые ворота смотрел ))) Я пытался в замыкании указать явно тип при использовании функции, но не дошло, что нужно указать с переменной, куда собственно результат и вернётся ))) бывает :rofl:

Не пойму, что улучшили в конструкции с Result? То, что в итоге можно проще обрабатывать большее количество параметров в замыкании?


#9

T?, Error? - нужно обработать 4 ситуации

  1. nil, nil

  2. value, error

  3. value, nil

  4. nil, error

Result<Dict1, Error> - только две

  1. .success(value)

  2. .failure(error)

Ну и если много Result, их можно засунуть в do catch:

do {
    let val1 = try result1.get()
    let val2 = try result2.get()
    let val3 = try result3.get()
} catch {
    print(error)
}

У Result как и у Optional есть map и flatMap, можно преобразовывать результат в новый Result:

let res = Result<String, Error>.success("1")
let res2 = res.map { Int($0)! }
print(try res2.get())


#10

Всё предельно ясно :slight_smile: