2016-01-18 6 views
7

Próbujemy ustalić, czy jest to błąd w Swift, czy w niewłaściwym używaniu generycznych, opcji, typu wnioskowania i/lub operatora zerowego koalescencji.Wnioskowanie o typ nie powiedzie się, gdy używamy operatora koalesczenia zerowego z dwoma opcjami.

Nasza struktura zawiera kod do analizowania słowników w modelach i mamy problem z opcjonalnymi właściwościami z wartościami domyślnymi.

Mamy protokół SomeProtocol i dwie funkcje standardowych określonych w rozszerzeniem protokołu:

mapped<T>(...) -> T? 
mapped<T : SomeProtocol>(...) -> T? 

Nasze elemencie i klas przywierania do tego protokołu, a następnie zanalizować ich właściwości wewnątrz funkcji init wymaganych przez protokół.

Wewnątrz funkcji init(...) staramy się ustawić wartość właściwości someNumber tak:

someNumber = self.mapped(dictionary, key: "someNumber") ?? someNumber 

Słownik oczywiście zawiera rzeczywistą wartość dla klucza someNumber. Jednak zawsze będzie się to nie udać, a rzeczywista wartość nigdy nie zostanie zwrócona z funkcji mapped().

Komentowanie drugiej funkcji ogólnej lub wymuszenie downcastowania wartości na rhs zadania spowoduje naprawienie tego problemu, ale uważamy, że powinno to działać tak, jak jest obecnie napisane.


Poniżej znajduje się pełny kod urywek wykazując problemu, wraz z dwoma opcjami (tymczasowo) Rozwiązać problem oznaczony OPTION 1 i OPTION 2 w kodzie:

import Foundation 

// Some protocol 

protocol SomeProtocol { 
    init(dictionary: NSDictionary?) 
} 

extension SomeProtocol { 
    func mapped<T>(dictionary: NSDictionary?, key: String) -> T? { 
     guard let dictionary = dictionary else { 
      return nil 
     } 

     let source = dictionary[key] 
     switch source { 

     case is T: 
      return source as? T 

     default: 
      break 
     } 

     return nil 
    } 

    // --- 
    // OPTION 1: Commenting out this makes it work 
    // --- 

    func mapped<T where T:SomeProtocol>(dictionary: NSDictionary?, key: String) -> T? { 
     return nil 
    } 
} 

// Some struct 

struct SomeStruct { 
    var someNumber: Double? = 0.0 
} 

extension SomeStruct: SomeProtocol { 
    init(dictionary: NSDictionary?) { 
     someNumber = self.mapped(dictionary, key: "someNumber") ?? someNumber 

     // OPTION 2: Writing this makes it work 
     // someNumber = self.mapped(dictionary, key: "someNumber") ?? someNumber! 
    } 
} 

// Test code 

let test = SomeStruct(dictionary: NSDictionary(object: 1234.4567, forKey: "someNumber")) 
if test.someNumber == 1234.4567 { 
    print("success \(test.someNumber!)") 
} else { 
    print("failure \(test.someNumber)") 
} 

proszę zauważyć, że jest to przykład, który pomija faktyczne implementacje funkcji mapped, ale wynik jest identyczny i dla zachowania tego pytania kod powinien wystarczyć.


EDIT: I zgłosił ten problem jakiś czas temu, a teraz zostało oznaczone jako stałe, więc mam nadzieję, że to nie powinno się zdarzyć już w Swift 3.
https://bugs.swift.org/browse/SR-574

Odpowiedz

7

Dałaś kompilator ma zbyt wiele opcji i wybiera niewłaściwy (a przynajmniej nie ten, który chciałeś). Problem polega na tym, że każde T może być trywialnie podwyższone do T?, w tym T? (podniesione do T??).

someNumber = self.mapped(dictionary, key: "someNumber") ?? someNumber 

Wow. Takie typy. Opcjonalnie. : D

W jaki sposób Swift zaczyna to rozumieć. Cóż, someNumber jest Double?, więc stara się przekształcić:

Double? = Double?? ?? Double? 

to działa? Poszukajmy typowego mapped, zaczynając od najbardziej konkretnego.

func mapped<T where T:SomeProtocol>(dictionary: NSDictionary?, key: String) -> T? { 

Aby dokonać tej pracy, T musi być Double?. Czy to Double?:SomeProtocol? Nie. Iść dalej.

func mapped<T>(dictionary: NSDictionary?, key: String) -> T? { 

Czy to działa? Pewnie! T może być Double? Powracamy Double?? i wszystko rozwiązuje się.

Dlaczego więc ten działa?

someNumber = self.mapped(dictionary, key: "someNumber") ?? someNumber! 

Ten postanawia:

Double? = Optional(Double? ?? Double) 

a następnie wszystko działa tak, jak uważają oni mają.

Uważaj na tak wiele opcji. Czy someNumber naprawdę musi być opcjonalna? Czy któraś z tych rzeczy powinna być dostępna pod adresem throw? (Nie sugeruję, że throw to ogólne obejście problemu opcjonalnego, ale przynajmniej ten problem daje chwilę, aby zastanowić się, czy to naprawdę jest warunek błędu.)

To prawie zawsze zły pomysł, aby pisać -parametryzować wyłącznie na podstawie wartości zwracanej w Swift, tak jak robi to mapped. To wydaje się być prawdziwym bałaganem w Swift (lub dowolnym języku generycznym, który ma wiele typów wnioskowania, ale w Swift pojawia się bardzo dużo, gdy zaangażowane są opcje). Parametry typu powinny zazwyczaj pojawiać się w argumentach. Zobaczysz problemu jeśli spróbujesz coś takiego:

let x = test.mapped(...) 

to nie będzie w stanie wywnioskować typ x. To nie jest anty-wzór, a czasami kłopot jest tego wart (w uczciwości, problem, który rozwiązujesz może być jednym z tych przypadków), ale unikaj go, jeśli możesz.

Ale to opcje, które cię zabijają.


EDIT: Dominik zadaje bardzo dobre pytanie o to, dlaczego zachowuje się inaczej, gdy ograniczona wersja mapped zostanie usunięty. Nie wiem Oczywiście silnik dopasowujący typy sprawdza poprawne typy w nieco innej kolejności, w zależności od tego, na ile sposobów jest ogólny. Możesz to zobaczyć, dodając print(T.self) do mapped<T>. To może być uznane za błąd w kompilatorze.

+1

To bardzo dobre wytłumaczenie. Rozumiem twój punkt widzenia i mogę zrozumieć, dlaczego "someNumber!" Działa, chociaż nie jestem pewien, czy widzę, dlaczego kod działa zgodnie z oczekiwaniami, kiedy komentujesz funkcję 'mapowaną ...' . Czy mógłbyś powiedzieć coś więcej na ten temat? –

+1

Takie opcje, bardzo bóle głowy. :RE –

Powiązane problemy