Jak w języku Swift mogę zadeklarować zmienną określonego typu, która jest zgodna z co najmniej jednym protokołem?

96

W Swift mogę jawnie ustawić typ zmiennej, deklarując ją w następujący sposób:

var object: TYPE_NAME

Jeśli chcemy pójść o krok dalej i zadeklarować zmienną, która jest zgodna z wieloma protokołami, możemy użyć protocoldeklaratywnego:

var object: protocol<ProtocolOne,ProtocolTwo>//etc

A co jeśli chciałbym zadeklarować obiekt, który jest zgodny z jednym lub kilkoma protokołami, a także ma określony typ klasy bazowej? Odpowiednik Objective-C wyglądałby następująco:

NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;

W Swift spodziewałbym się, że będzie to wyglądać tak:

var object: TYPE_NAME,ProtocolOne//etc

Daje nam to elastyczność, ponieważ jesteśmy w stanie poradzić sobie z implementacją typu podstawowego, a także dodanym interfejsem zdefiniowanym w protokole.

Czy jest inny bardziej oczywisty sposób, którego mógłbym przegapić?

Przykład

Jako przykład załóżmy, że mam UITableViewCellfabrykę, która jest odpowiedzialna za zwrot komórek zgodnych z protokołem. Możemy łatwo ustawić ogólną funkcję, która zwraca komórki zgodne z protokołem:

class CellFactory {
    class func createCellForItem<T: UITableViewCell where T:MyProtocol >(item: SpecialItem,tableView: UITableView) -> T {
        //etc
    }
}

później chcę usunąć z kolejki te komórki, wykorzystując zarówno typ, jak i protokół

var cell: MyProtocol = CellFactory.createCellForItem(somethingAtIndexPath) as UITableViewCell

Zwraca to błąd, ponieważ komórka widoku tabeli nie jest zgodna z protokołem ...

Chciałbym móc określić, że komórka jest a UITableViewCelli jest zgodna z MyProtocoldeklaracją w zmiennej?

Usprawiedliwienie

Jeśli znasz wzorzec fabryki, miałoby to sens w kontekście możliwości zwracania obiektów określonej klasy, które implementują określony interfejs.

Tak jak w moim przykładzie, czasami lubimy definiować interfejsy, które mają sens, gdy są stosowane do konkretnego obiektu. Mój przykład komórki widoku tabeli jest jednym z takich uzasadnień.

Chociaż dostarczony typ nie jest dokładnie zgodny ze wspomnianym interfejsem, obiekt zwracany przez fabrykę ma, dlatego chciałbym mieć elastyczność w interakcji zarówno z typem klasy bazowej, jak iz deklarowanym interfejsem protokołu

Daniel Galasko
źródło
Przepraszam, ale po co to szybko. Typy już wiedzą, jakie protokoły są zgodne. Czego nie używać tylko typu?
Kirsteins,
1
@Kirsteins Nie, chyba że typ jest zwracany z fabryki, a zatem jest typem ogólnym ze wspólną klasą bazową
Daniel Galasko
Jeśli to możliwe, podaj przykład.
Kirsteins,
NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;. Przedmiot ten wydaje się zupełnie bezużyteczny, ponieważ NSSomethingjuż wie, do czego się dostosowuje. Jeśli nie jest zgodny z jednym z protokołów <>, wystąpią unrecognised selector ...awarie. Nie zapewnia to żadnego bezpieczeństwa typu.
Kirsteins,
@Kirsteins Proszę zobaczyć ponownie mój przykład, jest używany, gdy wiesz, że obiekt, który sprzedaje twoja fabryka, ma określoną klasę bazową zgodną z określonym protokołem
Daniel Galasko

Odpowiedzi:

72

W Swift 4 można teraz zadeklarować zmienną, która jest podklasą typu i implementuje jeden lub więcej protokołów w tym samym czasie.

var myVariable: MyClass & MyProtocol & MySecondProtocol

Aby wykonać opcjonalną zmienną:

var myVariable: (MyClass & MyProtocol & MySecondProtocol)?

lub jako parametr metody:

func shakeEm(controls: [UIControl & Shakeable]) {}

Apple ogłosił to na WWDC 2017 w sesji 402: Co nowego w Swift

Po drugie, chcę porozmawiać o komponowaniu klas i protokołów. Tak więc tutaj wprowadziłem ten zmienny protokół dla elementu interfejsu użytkownika, który może wywołać niewielki efekt potrząśnięcia, aby zwrócić na siebie uwagę. Poszedłem dalej i rozszerzyłem niektóre klasy UIKit, aby faktycznie zapewniały tę funkcję wstrząsania. A teraz chcę napisać coś, co wydaje się proste. Chcę tylko napisać funkcję, która pobiera kilka kontrolek, które są wstrząsalne i potrząsa tymi, które są włączone, aby zwrócić na nie uwagę. Jaki typ mogę tu wpisać w tej tablicy? To naprawdę frustrujące i trudne. Mogłem więc spróbować użyć kontrolki interfejsu użytkownika. Ale nie wszystkie elementy sterujące interfejsu użytkownika są niestabilne w tej grze. Mógłbym spróbować shakable, ale nie wszystkie shakable są kontrolkami interfejsu użytkownika. I właściwie nie ma dobrego sposobu, aby to przedstawić w Swift 3.Swift 4 wprowadza pojęcie tworzenia klasy z dowolną liczbą protokołów.

Philipp Otto
źródło
3
Po prostu dodam
Daniel Galasko
Dziękuję Filip!
Omar Albeik
co jeśli potrzebujesz opcjonalnej zmiennej tego typu?
Vyachaslav Gerchicov
2
@VyachaslavGerchicov: Możesz umieścić wokół niego nawiasy, a następnie znak zapytania w ten sposób: var myVariable: (MyClass & MyProtocol & MySecondProtocol)?
Philipp Otto
30

Nie możesz zadeklarować zmiennej, takiej jak

var object:Base,protocol<ProtocolOne,ProtocolTwo> = ...

ani nie deklaruj typu zwracanego funkcji, jak

func someFunc() -> Base,protocol<MyProtocol,Protocol2> { ... }

Możesz zadeklarować jako parametr funkcji, taki jak ten, ale jest to w zasadzie rzutowanie w górę.

func someFunc<T:Base where T:protocol<MyProtocol1,MyProtocol2>>(val:T) {
    // here, `val` is guaranteed to be `Base` and conforms `MyProtocol` and `MyProtocol2`
}

class SubClass:BaseClass, MyProtocol1, MyProtocol2 {
   //...
}

let val = SubClass()
someFunc(val)

Na razie wszystko, co możesz zrobić, to:

class CellFactory {
    class func createCellForItem(item: SpecialItem) -> UITableViewCell {
        return ... // any UITableViewCell subclass
    }
}

let cell = CellFactory.createCellForItem(special)
if let asProtocol = cell as? protocol<MyProtocol1,MyProtocol2> {
    asProtocol.protocolMethod()
    cell.cellMethod()
}

Dzięki temu technicznie celljest identyczny z asProtocol.

Ale, co do kompilatora, cellma UITableViewCelltylko interfejs , podczas gdy asProtocolma tylko interfejs protokołów. Tak więc, gdy chcesz wywołać UITableViewCellmetody, musisz użyć cellzmiennej. Jeśli chcesz wywołać metodę protokołów, użyj asProtocolzmiennej.

Jeśli masz pewność, że komórka jest zgodna z protokołami, nie musisz ich używać if let ... as? ... {}. lubić:

let cell = CellFactory.createCellForItem(special)
let asProtocol = cell as protocol<MyProtocol1,MyProtocol2>
rintaro
źródło
Ponieważ fabryka określa typy zwrotów, nie muszę technicznie wykonywać opcjonalnego rzutowania? Mogę po prostu polegać na niejawnym typowaniu jerzyków, aby wykonać typowanie, w którym jawnie deklaruję protokoły?
Daniel Galasko,
Nie rozumiem, co masz na myśli, przepraszam za moje złe umiejętności językowe. Jeśli mówisz o -> UITableViewCell<MyProtocol>, to jest nieprawidłowe, ponieważ UITableViewCellnie jest typem ogólnym. Myślę, że to nawet się nie kompiluje.
rintaro
Nie mam na myśli ogólnej implementacji, ale raczej przykładową ilustrację implementacji. gdzie mówisz let asProtocol = ...
Daniel Galasko
lub mógłbym po prostu zrobić: var cell: protocol <ProtocolOne, ProtocolTwo> = someObject as UITableViewCell i uzyskać korzyści z obu w jednej zmiennej
Daniel Galasko
2
Nie sądzę. Nawet gdybyś mógł to zrobić, cellma tylko metody protokołów (dla kompilatora).
rintaro
2

Niestety Swift nie obsługuje zgodności z protokołem na poziomie obiektu. Istnieje jednak nieco niezręczna metoda obejścia, która może służyć Twoim celom.

struct VCWithSomeProtocol {
    let protocol: SomeProtocol
    let viewController: UIViewController

    init<T: UIViewController>(vc: T) where T: SomeProtocol {
        self.protocol = vc
        self.viewController = vc
    }
}

Następnie, gdziekolwiek potrzebujesz zrobić cokolwiek, co ma UIViewController, uzyskasz dostęp do aspektu .viewController struktury i wszystkiego, czego potrzebujesz, aspektu protokołu, odniesiesz się do .protocol.

Na przykład:

class SomeClass {
   let mySpecialViewController: VCWithSomeProtocol

   init<T: UIViewController>(injectedViewController: T) where T: SomeProtocol {
       self.mySpecialViewController = VCWithSomeProtocol(vc: injectedViewController)
   }
}

Teraz za każdym razem, gdy potrzebujesz mySpecialViewController do zrobienia czegokolwiek związanego z UIViewController, wystarczy odwołać się do mySpecialViewController.viewController, a gdy potrzebujesz go do wykonania jakiejś funkcji protokołu, odwołaj się do mySpecialViewController.protocol.

Miejmy nadzieję, że Swift 4 pozwoli nam w przyszłości zadeklarować obiekt z dołączonymi do niego protokołami. Ale na razie to działa.

Mam nadzieję że to pomoże!

Michael Curtis
źródło
1

EDYCJA: Pomyliłem się , ale jeśli ktoś inny przeczyta to nieporozumienie, tak jak ja, zostawiam tę odpowiedź. PO zapytał o sprawdzenie zgodności z protokołem obiektu danej podklasy, a to już inna historia, jak pokazuje przyjęta odpowiedź. Ta odpowiedź dotyczy zgodności protokołu dla klasy bazowej.

Może się mylę, ale czy nie mówisz o dodaniu zgodności protokołu do UITableCellViewklasy? W tym przypadku protokół jest rozszerzany na klasę bazową, a nie obiekt. Zapoznaj się z dokumentacją Apple dotyczącą deklarowania przyjęcia protokołu z rozszerzeniem, które w Twoim przypadku wyglądałoby mniej więcej tak:

extension UITableCellView : ProtocolOne {}

// Or alternatively if you need to add a method, protocolMethod()
extension UITableCellView : ProcotolTwo {
   func protocolTwoMethod() -> String {
     return "Compliant method"
   }
}

Oprócz wspomnianej już dokumentacji języka Swift, zobacz także artykuł Nate Cooks Funkcje ogólne dla niezgodnych typów z dalszymi przykładami.

Daje nam to elastyczność, ponieważ jesteśmy w stanie poradzić sobie z implementacją typu podstawowego, a także dodanym interfejsem zdefiniowanym w protokole.

Czy jest inny bardziej oczywisty sposób, którego mógłbym przegapić?

Protokół Adoption zrobi to, sprawi, że obiekt będzie zgodny z podanym protokołem. Należy jednak pamiętać o niekorzystnej stronie, że zmienna danego typu protokołu nie wie nic poza protokołem. Ale można to obejść, definiując protokół, który ma wszystkie potrzebne metody / zmienne / ...

Chociaż dostarczony typ nie jest dokładnie zgodny ze wspomnianym interfejsem, obiekt zwracany przez fabrykę ma, dlatego chciałbym mieć elastyczność w interakcji zarówno z typem klasy bazowej, jak iz deklarowanym interfejsem protokołu

Jeśli chcesz, aby metoda ogólna, zmienna była zgodna zarówno z protokołem, jak i typami klas bazowych, możesz mieć pecha. Ale wygląda na to, że musisz zdefiniować protokół wystarczająco szeroki, aby mieć wymagane metody zgodności, a jednocześnie wystarczająco wąski, aby mieć możliwość zaadaptowania go do klas bazowych bez zbytniego nakładu pracy (tj. Po prostu deklarując, że klasa jest zgodna z protokół).

Holroy
źródło
1
Wcale nie o tym mówię, ale dzięki :) Chciałem mieć możliwość komunikacji z obiektem zarówno poprzez jego klasę, jak i określony protokół. Podobnie jak w obj-c i can do NSObject <MyProtocol> obj = ... Nie trzeba dodawać, że nie można tego zrobić szybko, musisz rzutować obiekt na jego protokół
Daniel Galasko
0

Kiedyś miałem podobną sytuację, próbując połączyć moje ogólne połączenia między czynnikami w Storyboardach (IB nie pozwoli ci łączyć gniazd z protokołami, tylko instancjami obiektów), którą obejrzałem, po prostu maskując publiczną ivar klasy bazowej za pomocą prywatnego obliczonego własność. Chociaż nie uniemożliwia to komuś dokonywania nielegalnych przydziałów jako takich, zapewnia wygodny sposób bezpiecznego zapobiegania wszelkim niepożądanym interakcjom z niezgodnym wystąpieniem w czasie wykonywania. (tj. zapobiegaj wywoływaniu metod delegatów do obiektów, które nie są zgodne z protokołem).

Przykład:

@objc protocol SomeInteractorInputProtocol {
    func getSomeString()
}

@objc protocol SomeInteractorOutputProtocol {
    optional func receiveSomeString(value:String)
}

@objc class SomeInteractor: NSObject, SomeInteractorInputProtocol {

    @IBOutlet var outputReceiver : AnyObject? = nil

    private var protocolOutputReceiver : SomeInteractorOutputProtocol? {
        get { return self.outputReceiver as? SomeInteractorOutputProtocol }
    }

    func getSomeString() {
        let aString = "This is some string."
        self.protocolOutputReceiver?.receiveSomeString?(aString)
    }
}

„OutputReceiver” jest deklarowany jako opcjonalny, podobnie jak prywatny „protocolOutputReceiver”. Uzyskując zawsze dostęp do outputReceiver (czyli delegata) za pośrednictwem tego ostatniego (właściwość computed), skutecznie odfiltrowuję wszelkie obiekty, które nie są zgodne z protokołem. Teraz mogę po prostu użyć opcjonalnego łączenia łańcuchowego, aby bezpiecznie wywołać obiekt delegata, niezależnie od tego, czy implementuje protokół, czy nawet istnieje.

Aby zastosować to do swojej sytuacji, możesz ustawić publiczny ivar typu „YourBaseClass?” (w przeciwieństwie do AnyObject) i użyj prywatnej właściwości obliczonej, aby wymusić zgodność protokołu. FWIW.

quickthyme
źródło