Interfejsy PHP 7, podpowiedzi typu zwracanego i self

89

AKTUALIZACJA : PHP 7.4 obsługuje teraz kowariancję i kontrawariancję, co rozwiązuje główny problem poruszony w tym pytaniu.


Napotkałem problem ze stosowaniem podpowiedzi typu zwracanego w PHP 7. Rozumiem, że podpowiedź : selfoznacza, że ​​klasa implementująca ma zwrócić się sama. Dlatego użyłem : selfw moich interfejsach, aby to wskazać, ale kiedy próbowałem faktycznie zaimplementować interfejs, otrzymałem błędy zgodności.

Poniżej znajduje się prosta demonstracja problemu, z którym się spotkałem:

interface iFoo
{
    public function bar (string $baz) : self;
}

class Foo implements iFoo
{

    public function bar (string $baz) : self
    {
        echo $baz . PHP_EOL;
        return $this;
    }
}

(new Foo ()) -> bar ("Fred") 
    -> bar ("Wilma") 
    -> bar ("Barney") 
    -> bar ("Betty");

Oczekiwany wynik to:

Fred Wilma Barney Betty

W rzeczywistości otrzymuję:

Błąd krytyczny PHP: Deklaracja Foo :: bar (int $ baz): Foo musi być kompatybilne z iFoo :: bar (int $ baz): iFoo w test.php w linii 7

Chodzi o to, że Foo jest implementacją iFoo, o ile wiem, implementacja powinna być idealnie kompatybilna z danym interfejsem. Prawdopodobnie mógłbym rozwiązać ten problem, zmieniając interfejs lub klasę implementującą (lub obie) tak, aby zwracały wskazówkę do interfejsu według nazwy zamiast używania self, ale rozumiem, że semantycznie selfoznacza „zwróć wystąpienie klasy, do której właśnie wywołałeś metodę ”. Dlatego zmiana go na interfejs teoretycznie oznaczałaby, że mógłbym zwrócić każdą instancję czegoś, co implementuje interfejs, gdy moja intencja jest zwrócona w wywołanej instancji.

Czy jest to przeoczenie w PHP, czy jest to celowa decyzja projektowa? Jeśli jest to pierwsze, czy jest szansa, że ​​zostanie to naprawione w PHP 7.1? Jeśli nie, to jaki jest właściwy sposób zwrócenia wskazówki, że Twój interfejs oczekuje od Ciebie zwrócenia instancji, do której właśnie wywołałeś metodę w celu utworzenia łańcucha?

Gordon M.
źródło
Myślę, że jest to błąd w podpowiedziach typu powrotu PHP, być może powinieneś zgłosić to jako błąd ; ale jest mało prawdopodobne, aby jakakolwiek poprawka dotarła do PHP 7.1 na tak późnym etapie
Mark Baker
Ponieważ ostatnia wersja beta 7.1 pojawiła się kilka dni temu, jest bardzo mało prawdopodobne, aby jakakolwiek poprawka dotarła do 7.1.
Charlotte Dunois
Z ciekawości, gdzie czytasz swoją interpretację tego, jak selfpowinien działać typ zwrotu?
Adam Cameron
@Adam: Wydaje się, że logiczne jest, selfaby oznaczać „Zwróć instancję, do której to wywołałeś , a nie inną instancję, która implementuje ten sam interfejs”. Wydaje mi się, że Java miała podobny typ zwrotu (chociaż minęło trochę czasu, odkąd programowałem w Javie)
GordonM
1
Cześć Gordon. Cóż, gdyby nie udokumentowane, że gdzieś działa, nie liczyłbym na to, co może być logiczne. TBH w sytuacji, którą opisałbym, byłbym po prostu tak deklaratywny, jak to tylko możliwe i użyłbym iFoo jako typu zwracanego. Czy jest sytuacja, w której to faktycznie nie zadziała? (Zdaję sobie sprawę, że jest to raczej „rada” / opinia niż „odpowiedź”.
Adam Cameron,

Odpowiedzi:

94

selfnie odnosi się do instancji, odnosi się do bieżącej klasy. Nie ma sposobu, aby interfejs określił, że ta sama instancja musi zostać zwrócona - użycie selfw sposób, w jaki próbujesz, wymusiłoby jedynie wymuszenie, aby zwracana instancja była tej samej klasy.

To powiedziawszy, deklaracje typu zwracanego w PHP muszą być niezmienne, podczas gdy to, co próbujesz, jest kowariantne.

Twoje użycie selfjest równoważne z:

interface iFoo
{
    public function bar (string $baz) : iFoo;
}

class Foo implements iFoo
{

    public function bar (string $baz) : Foo  {...}
}

co jest niedozwolone.


Powrót Rodzaj Deklaracje RFC ma do powiedzenia :

Egzekwowanie zadeklarowanego typu zwracanego podczas dziedziczenia jest niezmienne; oznacza to, że jeśli podtyp przesłania metodę nadrzędną, zwracany typ dziecka musi dokładnie odpowiadać typowi nadrzędnemu i nie można go pominąć. Jeśli rodzic nie zadeklaruje typu zwracanego, wówczas dziecko może go zadeklarować.

...

Ten dokument RFC pierwotnie proponował kowariantne typy zwracane, ale został zmieniony na niezmienniczy z powodu kilku problemów. W pewnym momencie w przyszłości można dodać kowariantne typy zwracane.


Na razie przynajmniej najlepsze, co możesz zrobić, to:

interface iFoo
{
    public function bar (string $baz) : iFoo;
}

class Foo implements iFoo
{

    public function bar (string $baz) : iFoo  {...}
}
user3942918
źródło
18
To powiedziawszy, spodziewałbym się, że wskazówka dotycząca typu powrotu staticzadziała, ale nie jest nawet rozpoznawana
Mark Baker
Doszedłem do wniosku, że tak będzie i tak właśnie rozwiążę problem. Wolałbym jednak użyć po prostu: self, jeśli to możliwe, ponieważ jeśli klasa implementuje interfejs, to zwracana wartość self jest również niejawnie wartością zwracaną przez instancję interfejsu.
GordonM
19
Paul, usunięcie komentarzy, które tutaj usunąłeś, jest w rzeczywistości szkodliwe, ponieważ (A) powoduje utratę ważnych informacji, a (B) zakłóca przepływ dyskusji w stosunku do innych komentarzy. Nie widzę powodu, dla którego Twoje komentarze dotyczące Marka i Gordona musiały zostać usunięte. W rzeczywistości robisz to wszędzie i to musi się skończyć. Nie ma absolutnie żadnego powodu, aby wracać do pytania sprzed roku i usuwać wszystkie swoje komentarze, całkowicie niszcząc przepływ dyskusji. W rzeczywistości jest to szkodliwe i uciążliwe.
Cody Gray
Widzimy tutaj ważną przedmowę do części cytatu: „ Kowariantne typy zwracane są uważane za dźwięk typu i są używane w wielu innych językach (C ++ i Java, ale nie C #, jak sądzę). Ten dokument RFC pierwotnie proponował kowariantne typy zwracane, ale został zmieniony na niezmienniczy z powodu kilku problemów. ”. Jestem ciekaw, jakie problemy miał PHP. Ich wybór projektu powoduje kilka ograniczeń, które również powodują dziwne problemy z cechami, co sprawia, że ​​w wielu przypadkach są one nieco bezużyteczne, chyba że poluzujesz ograniczenia typu zwracanego (jak widać w niektórych odpowiedziach poniżej). Bardzo frustrujące.
John Pancoast
1
@MarkBaker zwracany typ staticjest dodawany do PHP 8.
Ricardo Boss
16

Może to być również rozwiązanie, w którym nie definiujesz jawnie typu zwracanego w interfejsie, tylko w PHPDoc, a następnie możesz zdefiniować określony typ zwracania w implementacjach:

interface iFoo
{
    public function bar (string $baz);
}

class Foo implements iFoo
{
    public function bar (string $baz) : Foo  {...}
}
Gabor
źródło
2
Lub zamiast po Fooprostu użyć self.
zamiast
2

W przypadku, gdy chcesz wymusić z interfejsu, ta metoda zwróci obiekt, ale typ obiektu nie będzie typem interfejsu, a samą klasą, możesz to zapisać w ten sposób:

interface iFoo {
    public function bar (string $baz) : object;
}

class Foo implements iFoo {
    public function bar (string $baz) : self  {...}
}

Działa od PHP 7.4.

zamiast
źródło
0

To wygląda na oczekiwane zachowanie.

Po prostu zmień Foo::barmetodę na powrót iFoozamiast selfi skończ z tym.

Wyjaśnienie:

selfużyte w interfejsie oznacza „obiekt typu iFoo”.
selfużyte w implementacji oznacza „obiekt typu Foo”.

Dlatego typy zwracane w interfejsie i implementacja wyraźnie nie są takie same.

Jeden z komentarzy wspomina o Javie i czy miałbyś ten problem. Odpowiedź brzmi: tak, miałbyś ten sam problem, gdyby Java pozwalała ci pisać w ten sposób kod - co nie. Ponieważ Java wymaga użycia nazwy typu zamiast selfskrótu PHP , nigdy tego nie zobaczysz. (Zobacz tutaj, aby zapoznać się z omówieniem podobnego problemu w Javie).

Moshe Katz
źródło
Czy więc deklarowanie jest selfpodobne do deklarowania MyClass::class?
peterchaula
1
@Laser Tak, to prawda.
Moshe Katz
4
Ale jeśli Foo implementuje iFoo, to Foo jest z definicji typu iFoo
GordonM