Czy warto zacząć używać instancetype zamiast id?

226

Clang dodaje słowo kluczowe, instancetypektóre, o ile widzę, zastępuje idjako typ zwracany w -alloci init.

Czy istnieje korzyści stosując instancetypezamiast id?

griotspeak
źródło
10
Nie .. nie do przydzielania i inicjowania, ponieważ już działają w ten sposób. Istota typu instancet polega na tym, że możesz nadać niestandardowym metodom zachowanie / init podobne do init.
hooleyhoop,
@hooleyhoop nie działają w ten sposób. Zwracają identyfikator. id to „obiekt Obj-C”. To wszystko. Kompilator nie wie nic o wartości zwracanej poza tym.
griotspeak
5
działają w ten sposób, sprawdzanie kontraktu i typu już się dzieje dla init, tylko dla konstruktorów tego potrzebujesz. nshipster.com/instancetype
kocodude
Rzeczy nieco się posunęły, tak. Powiązane typy wyników są stosunkowo nowe, a inne zmiany oznaczają, że wkrótce będzie to o wiele wyraźniejszy problem.
griotspeak
1
Chciałbym tylko skomentować, że teraz na iOS 8 wiele metod, które były używane do powrotu id, zostało zastąpionych instancetype, nawet initz NSObject. Jeśli chcesz, aby kod był kompatybilny z szybkim, musisz użyćinstancetype
Hola Soy Edu Feliz Navidad

Odpowiedzi:

192

Na pewno jest korzyść. Kiedy używasz „id”, zasadniczo nie uzyskuje się żadnego sprawdzania typu. Dzięki instancetype kompilator i IDE wiedzą, jaki typ rzeczy jest zwracany, i mogą lepiej sprawdzać kod i lepiej autouzupełnianie.

Używaj go tylko tam, gdzie ma to sens (tj. Metoda, która zwraca instancję tej klasy); identyfikator jest nadal przydatny.

Catfish_Man
źródło
8
alloc, inititp. są automatycznie promowane instancetypeprzez kompilator. Nie oznacza to, że nie ma korzyści; jest, ale to nie to.
Steven Fisher
10
przydaje się głównie konstruktorom wygody
Catfish_Man
5
Został on wprowadzony za pomocą ARC (i w celu wsparcia), wraz z wydaniem Lion w 2011 roku. Jedną z komplikujących powszechną adopcję w Kakao jest to, że wszystkie metody kandydujące muszą zostać poddane audytowi, aby sprawdzić, czy wykonują one [samo przydzielanie], a nie [NameOfClass alokacja], ponieważ byłoby bardzo mylące, aby wykonać [SomeSubClass dogodnośćConstructor], a + dogodnośćConstructor zadeklarowałby zwrócenie typu instancet i nie zwracałby instancji SomeSubClass.
Catfish_Man
2
„id” jest konwertowany na typ instancet tylko wtedy, gdy kompilator może wywnioskować rodzinę metod. Z pewnością nie robi tego w przypadku konstruktorów wygody lub innych metod zwracania identyfikatorów.
Catfish_Man
4
Pamiętaj, że w iOS 7 wiele metod w Foundation zostało przekonwertowanych na typ instancet. Osobiście widziałem ten błąd co najmniej 3 przypadki niepoprawnego wcześniej istniejącego kodu.
Catfish_Man
336

Tak, korzystanie z nich jest korzystne instancetypewe wszystkich przypadkach, w których ma to zastosowanie. Wyjaśnię bardziej szczegółowo, ale zacznę od tego odważnego stwierdzenia: używaj, instancetypegdy jest to właściwe, czyli za każdym razem, gdy klasa zwraca instancję tej samej klasy.

Oto, co Apple teraz mówi na ten temat:

W swoim kodzie zamień wystąpienia idjako wartość zwracaną na instancetypeodpowiednią. Zazwyczaj dzieje się tak w przypadku initmetod i metod fabrycznych klas. Mimo że kompilator automatycznie konwertuje metody zaczynające się od „przydzielania”, „inicjowania” lub „nowego” i ma typ idzwracany do zwrócenia instancetype, nie konwertuje innych metod. Konwencja celu C polega na instancetypejawnym pisaniu dla wszystkich metod.

Aby temu zapobiec, przejdźmy dalej i wyjaśnij, dlaczego to dobry pomysł.

Po pierwsze, niektóre definicje:

 @interface Foo:NSObject
 - (id)initWithBar:(NSInteger)bar; // initializer
 + (id)fooWithBar:(NSInteger)bar;  // class factory
 @end

W fabryce klasowej zawsze powinieneś używać instancetype. Kompilator nie automatycznie konwertować iddo instancetype. To idjest ogólny obiekt. Ale jeśli to zrobiszinstancetype kompilator wie, jaki typ obiektu zwraca metoda.

To nie jest problem akademicki. Na przykład [[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData]wygeneruje błąd w systemie Mac OS X ( tylko ) Znaleziono wiele metod o nazwie „writeData:” z niedopasowanym wynikiem, typem parametru lub atrybutami . Powodem jest to, że zarówno NSFileHandle, jak i NSURLHandle zapewniają writeData:. Ponieważ [NSFileHandle fileHandleWithStandardOutput]zwraca an id, kompilator nie jest pewien, jakiej klasywriteData: jest wywoływana.

Musisz obejść ten problem, używając:

[(NSFileHandle *)[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData];

lub:

NSFileHandle *fileHandle = [NSFileHandle fileHandleWithStandardOutput];
[fileHandle writeData:formattedData];

Oczywiście lepszym rozwiązaniem jest zadeklarowanie fileHandleWithStandardOutputzwrotuinstancetype . Wtedy obsada lub zadanie nie jest konieczne.

(Należy pamiętać, że na iOS, w tym przykładzie nie będzie produkować błąd jak tylko NSFileHandledostarcza writeData:tam. Istnieją również inne przykłady, takie jak length, która zwraca CGFloatz UILayoutSupportale NSUIntegerz NSString).

Uwaga : odkąd to napisałem, nagłówki macOS zostały zmodyfikowane, aby zwracały NSFileHandlezamiastid .

W przypadku inicjatorów jest to bardziej skomplikowane. Po wpisaniu:

- (id)initWithBar:(NSInteger)bar

… Kompilator udaje, że zamiast tego wpisałeś:

- (instancetype)initWithBar:(NSInteger)bar

Było to konieczne dla ARC. Jest to opisane w Powiązane typy rozszerzeń języka Clang . Dlatego ludzie powiedzą ci, że nie musisz używaćinstancetype , chociaż uważam, że powinieneś. Reszta tej odpowiedzi dotyczy tego.

Istnieją trzy zalety:

  1. Wyraźny. Twój kod robi to, co mówi, a nie coś innego.
  2. Wzór. Budujesz dobre nawyki na czasy, które mają znaczenie, które istnieją.
  3. Konsystencja. Ustaliłeś pewną spójność kodu, dzięki czemu jest on bardziej czytelny.

Wyraźny

To prawda, że powrót z nie ma żadnych technicznych korzyści . Ale to dlatego, że kompilator automatycznie konwertuje TO . Polegasz na tym dziwactwie; podczas gdy piszesz, że zwraca an , kompilator interpretuje to tak, jakby zwraca aninstancetypeinitidinstancetypeinitidinstancetype .

Są one równoważne z kompilatorem:

- (id)initWithBar:(NSInteger)bar;
- (instancetype)initWithBar:(NSInteger)bar;

To nie jest równoznaczne z twoimi oczami. W najlepszym przypadku nauczysz się ignorować różnicę i przejrzeć ją. To nie jest coś, co powinieneś nauczyć się ignorować.

Wzór

Chociaż nie ma różnicy z initi innych metod, nie ma różnicy jak najszybciej określić fabrykę klasy.

Te dwa nie są równoważne:

+ (id)fooWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

Chcesz drugiej formy. Jeśli jesteś przyzwyczajony do pisania instancetypejako typ zwracany przez konstruktor, za każdym razem będziesz to robić poprawnie.

Konsystencja

Wreszcie, wyobraź sobie, że połączysz to wszystko: potrzebujesz initfunkcji, a także fabryki klasy.

Jeśli używasz iddo init, możesz skończyć z kodem jak poniżej:

- (id)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

Ale jeśli użyjesz instancetype, otrzymasz to:

- (instancetype)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

Jest bardziej spójny i bardziej czytelny. Zwracają to samo, a teraz to oczywiste.

Wniosek

O ile celowo nie piszesz kodu dla starych kompilatorów, powinieneś go używać w instancetyperazie potrzeby.

Przed napisaniem zwrotnej wiadomości powinieneś się wahać id. Zadaj sobie pytanie: czy to zwraca instancję tej klasy? Jeśli tak, to jest instancetype.

Z pewnością są przypadki, w których musisz wrócić id, ale prawdopodobnie będziesz używać instancetypeznacznie częściej.

Steven Fisher
źródło
4
Minęło trochę czasu, odkąd o to zapytałem i już dawno zająłem stanowisko podobne do tego, które tu przedstawiłeś. Zostawiłem to tutaj, ponieważ uważam, że niestety będzie to uważane za kwestię stylu dla init, a informacje przedstawione we wcześniejszych odpowiedziach dość wyraźnie odpowiedziały na pytanie.
griotspeak
6
Żeby było jasne: uważam, że odpowiedź Catfish_Man jest poprawna tylko w pierwszym zdaniu . Odpowiedź hooleyhoopa jest poprawna, z wyjątkiem pierwszego zdania . To piekło dylematu; Chciałem dostarczyć coś, co z czasem okaże się bardziej przydatne i poprawne niż jedno z nich. (Z całym szacunkiem dla tych dwojga; w końcu jest to teraz o wiele bardziej oczywiste niż wtedy, gdy napisano ich odpowiedzi.)
Steven Fisher
1
Z czasem mam taką nadzieję. Nie mogę wymyślić żadnego powodu, aby tego nie robić, chyba że Apple uważa, że ​​ważne jest utrzymanie zgodności źródła z (na przykład) przypisaniem nowej macierzy NSArtr do NSString bez obsady.
Steven Fisher
4
instancetypevs idnaprawdę nie jest decyzją stylu. Ostatnie zmiany wokół instancetypenaprawdę wyjaśniają, że powinniśmy używać instancetypew miejscach takich jak -init„przykład mojej klasy”
griotspeak
2
Powiedziałeś, że istnieją powody, by używać identyfikatora, czy możesz to rozwinąć? Jedyne dwa, o których mogę myśleć, to zwięzłość i zgodność wsteczna; biorąc pod uwagę, że oba są kosztem wyrażenia twojej intencji, myślę, że to naprawdę kiepskie argumenty.
Steven Fisher
10

Powyższe odpowiedzi są więcej niż wystarczające, aby wyjaśnić to pytanie. Chciałbym tylko dodać przykład, który czytelnicy mogą zrozumieć, jeśli chodzi o kodowanie.

Klasa A

@interface ClassA : NSObject

- (id)methodA;
- (instancetype)methodB;

@end

Klasa B

@interface ClassB : NSObject

- (id)methodX;

@end

TestViewController.m

#import "ClassA.h"
#import "ClassB.h"

- (void)viewDidLoad {

    [[[[ClassA alloc] init] methodA] methodX]; //This will NOT generate a compiler warning or error because the return type for methodA is id. Eventually this will generate exception at runtime

    [[[[ClassA alloc] init] methodB] methodX]; //This will generate a compiler error saying "No visible @interface ClassA declares selector methodX" because the methodB returns instanceType i.e. the type of the receiver
}
Evol Gate
źródło
spowoduje to wygenerowanie błędu kompilatora tylko wtedy, gdy nie użyjesz #import „ClassB.h”. w przeciwnym razie kompilator przyjmie, że wiesz, co robisz. na przykład następujące kompiluje, ale ulega awarii w czasie wykonywania: id myNumber = @ (1); id iAssumedThatWasAnArray = myNumber [0]; id iAssumedThatWasADictionary = [myNumber objectForKey: @ "key"];
OlDor
1

Możesz także uzyskać szczegółowe informacje w The Designated Initializer

**

INSTANCETYPE

** Tego słowa kluczowego można użyć tylko dla typu zwrotu, który jest zgodny z typem zwrotu odbiornika. Metoda init zawsze deklarowała, że ​​zwraca typ instancet. Dlaczego na przykład nie ustawić typu zwrotu Party dla wystąpienia strony? To spowodowałoby problem, gdyby klasa Party kiedykolwiek została podklasowana. Podklasa dziedziczy wszystkie metody z Party, w tym inicjator i jego typ zwracany. Jeśli instancja podklasy zostanie wysłana do tego komunikatu inicjalizującego, to czy zostanie zwrócony? Nie wskaźnik do instancji Party, ale wskaźnik do instancji podklasy. Może ci się wydawać, że to żaden problem, zastąpię inicjalizator w podklasie, aby zmienić typ zwracany. Ale w Objective-C nie można mieć dwóch metod z tym samym selektorem i różnymi typami zwrotu (lub argumentami). Określając, że metoda inicjowania zwraca „

ID

** Przed wprowadzeniem typu instancive w Objective-C inicjalizatory zwracają id (eye-dee). Ten typ jest zdefiniowany jako „wskaźnik do dowolnego obiektu”. (id jest bardzo podobny do void * w C.) W tym piśmie szablony klas XCode nadal używają id jako zwracanego typu inicjalizatorów dodanych w kodzie podstawowym. W przeciwieństwie do instancetype, id może być użyty jako coś więcej niż tylko typ zwrotu. Możesz zadeklarować zmienne lub parametry metody typu id, gdy nie masz pewności, na jaki typ obiektu wskaże zmienna. Możesz użyć id, gdy używasz szybkiego wyliczania do iteracji w tablicy wielu lub nieznanych typów obiektów. Zauważ, że ponieważ id jest niezdefiniowany jako „wskaźnik do dowolnego obiektu”, nie podajesz * przy deklarowaniu zmiennej lub parametru obiektu tego typu.

Nam N. HUYNH
źródło