Wäre es vorteilhaft, instancetype anstelle von id zu verwenden?

226

Clang fügt ein Schlüsselwort hinzu instancetype, das, soweit ich sehen kann, idals Rückgabetyp in -allocund ersetzt init.

Gibt es einen Vorteil instancetypeanstelle von id?

Griotspeak
quelle
10
Nein .. nicht für alloc und init, da sie schon so funktionieren. Der Punkt des Instanztyps ist, dass Sie benutzerdefinierte Methoden zuweisen können, die wie ein Verhalten zugeordnet sind.
Hooleyhoop
@hooleyhoop sie arbeiten nicht so. Sie geben ID zurück. id ist 'ein Obj-C-Objekt'. Das ist alles. Der Compiler weiß nichts anderes über den Rückgabewert.
Griotspeak
5
Sie funktionieren so, die Vertrags- und Typprüfung findet bereits für init statt, nur für Kunden, die Sie benötigen. nshipster.com/instancetype
kocodude
Die Dinge sind ziemlich weit fortgeschritten, ja. Verwandte Ergebnistypen sind relativ neu und andere Änderungen bedeuten, dass dies bald ein viel klareres Problem sein wird.
Griotspeak
1
Ich möchte nur kommentieren, dass jetzt unter iOS 8 viele Methoden, die früher zurückgegeben idwurden, durch ersetzt wurden instancetype, sogar initvon NSObject. Wenn Sie Ihren Code mit Swift kompatibel machen möchten, müssen Sieinstancetype
Hola Soja Edu Feliz Navidad

Antworten:

192

Es gibt definitiv einen Vorteil. Wenn Sie 'id' verwenden, erhalten Sie im Wesentlichen überhaupt keine Typprüfung. Mit instancetype wissen der Compiler und die IDE, welche Art von Dingen zurückgegeben wird, und können Ihren Code besser überprüfen und besser automatisch vervollständigen.

Verwenden Sie es nur dort, wo es natürlich Sinn macht (dh eine Methode, die eine Instanz dieser Klasse zurückgibt). ID ist immer noch nützlich.

Catfish_Man
quelle
8
alloc, initusw. werden instancetypevom Compiler automatisch heraufgestuft. Das heißt nicht, dass es keinen Nutzen gibt; gibt es, aber das ist es nicht.
Steven Fisher
10
Es ist vor allem für Convenience-Konstrukteure nützlich
Catfish_Man
5
Es wurde mit (und zur Unterstützung) ARC mit der Veröffentlichung von Lion im Jahr 2011 eingeführt. Eine Sache, die die weit verbreitete Einführung in Cocoa erschwert, ist, dass alle Kandidatenmethoden überprüft werden müssen, um festzustellen, ob sie [Selbstzuweisung] anstelle von [NameOfClass] durchführen alloc], da es sehr verwirrend wäre, [SomeSubClass ConvenienceConstructor] mit + ConvenienceConstructor auszuführen, der als Instanztyp zurückgegeben wird und keine Instanz von SomeSubClass zurückgibt.
Catfish_Man
2
'id' wird nur dann in den Instanztyp konvertiert, wenn der Compiler auf die Methodenfamilie schließen kann. Dies ist mit Sicherheit nicht für Convenience-Konstruktoren oder andere Methoden zur Rückgabe von IDs der Fall.
Catfish_Man
4
Beachten Sie, dass in iOS 7 viele Methoden in Foundation in den Instanztyp konvertiert wurden. Ich persönlich habe gesehen, dass mindestens 3 Fälle von falschem, bereits vorhandenem Code abgefangen wurden.
Catfish_Man
336

Ja, die Verwendung bietet Vorteile instancetypein allen Fällen, in denen sie gilt. Ich werde es genauer erklären, aber lassen Sie mich mit dieser kühnen Aussage beginnen: Verwenden Sie sie, instancetypewann immer es angebracht ist, dh wann immer eine Klasse eine Instanz derselben Klasse zurückgibt.

In der Tat ist hier, was Apple jetzt zu diesem Thema sagt:

Ersetzen Sie in Ihrem Code Vorkommen idals Rückgabewert instancetypegegebenenfalls durch. Dies ist normalerweise bei initMethoden und Klassenfactory-Methoden der Fall . Obwohl der Compiler automatisch Methoden konvertiert, die mit "alloc", "init" oder "new" beginnen und einen Rückgabetyp von " idreturn" haben instancetype, konvertiert er keine anderen Methoden. Die Objective-C-Konvention besteht darin, instancetypeexplizit für alle Methoden zu schreiben .

Lassen Sie uns weitermachen und erklären, warum dies eine gute Idee ist.

Zunächst einige Definitionen:

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

Für eine Klassenfabrik sollten Sie immer verwenden instancetype. Der Compiler konvertiert nicht automatisch idin instancetype. Das idist ein generisches Objekt. Wenn Sie es jedoch zu einem instancetypeObjekt machen, weiß der Compiler, welchen Objekttyp die Methode zurückgibt.

Dies ist kein akademisches Problem. Beispielsweise [[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData]wird unter Mac OS X ( nur ) ein Fehler generiert. Mehrere Methoden mit dem Namen 'writeData:' wurden mit nicht übereinstimmendem Ergebnis, Parametertyp oder Attributen gefunden . Der Grund ist, dass sowohl NSFileHandle als auch NSURLHandle a bereitstellen writeData:. Da der Compiler [NSFileHandle fileHandleWithStandardOutput]ein zurückgibt id, ist er nicht sicher, welche Klasse writeData:aufgerufen wird.

Sie müssen dies umgehen, indem Sie entweder Folgendes verwenden:

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

oder:

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

Die bessere Lösung besteht natürlich darin, fileHandleWithStandardOutputeine Rückgabe zu deklarieren instancetype. Dann ist die Besetzung oder Aufgabe nicht notwendig.

(Beachten Sie, dass dieses Beispiel unter iOS keinen Fehler erzeugt, da nur NSFileHandleein writeData:dort angegeben wird. Es gibt andere Beispiele, z. B. length, die ein CGFloatvon UILayoutSupportaber ein NSUIntegervon zurückgeben NSString.)

Hinweis : Seit ich dies geschrieben habe, wurden die macOS-Header so geändert, dass sie a NSFileHandleanstelle von a zurückgeben id.

Für Initialisierer ist es komplizierter. Wenn Sie dies eingeben:

- (id)initWithBar:(NSInteger)bar

… Der Compiler wird so tun, als hätten Sie dies stattdessen eingegeben:

- (instancetype)initWithBar:(NSInteger)bar

Dies war für ARC notwendig. Dies wird unter Clang Language Extensions Related-Ergebnistypen beschrieben . Dies ist der Grund, warum die Leute Ihnen sagen werden, dass es nicht notwendig ist, es zu verwenden instancetype, obwohl ich der Meinung bin, dass Sie es sollten. Der Rest dieser Antwort befasst sich damit.

Es gibt drei Vorteile:

  1. Explizit. Ihr Code tut, was er sagt, und nicht etwas anderes.
  2. Muster. Sie bauen gute Gewohnheiten für Zeiten auf, in denen es wichtig ist, welche existieren.
  3. Konsistenz. Sie haben eine gewisse Konsistenz für Ihren Code hergestellt, wodurch er besser lesbar ist.

Explizit

Es ist wahr, dass es keinen technischen Vorteil gibt, instancetypevon einem zurückzukehren init. Aber das ist , weil der Compiler automatisch die konvertiert idzu instancetype. Sie verlassen sich auf diese Eigenart; Während Sie schreiben, dass das ein initzurückgibt id, interpretiert der Compiler es so, als ob es ein zurückgibt instancetype.

Diese entsprechen dem Compiler:

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

Diese entsprechen nicht Ihren Augen. Bestenfalls werden Sie lernen, den Unterschied zu ignorieren und ihn zu überfliegen. Dies sollten Sie nicht ignorieren lernen.

Muster

Während es keinen Unterschied zu initund anderen Methoden gibt, gibt es einen Unterschied, sobald Sie eine Klassenfactory definieren.

Diese beiden sind nicht gleichwertig:

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

Sie möchten das zweite Formular. Wenn Sie es gewohnt sind, instancetypeals Rückgabetyp eines Konstruktors zu tippen, werden Sie es jedes Mal richtig machen.

Konsistenz

Stellen Sie sich zum Schluss vor, Sie setzen alles zusammen: Sie wollen eine initFunktion und auch eine Klassenfabrik.

Wenn Sie idfür verwenden init, erhalten Sie Code wie folgt:

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

Aber wenn Sie verwenden instancetype, erhalten Sie Folgendes:

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

Es ist konsistenter und lesbarer. Sie geben dasselbe zurück, und das ist jetzt offensichtlich.

Fazit

Sofern Sie nicht absichtlich Code für alte Compiler schreiben, sollten Sie diesen instancetypegegebenenfalls verwenden.

Sie sollten zögern, bevor Sie eine Nachricht schreiben, die zurückkehrt id. Fragen Sie sich: Gibt dies eine Instanz dieser Klasse zurück? Wenn ja, ist es ein instancetype.

Es gibt sicherlich Fälle, in denen Sie zurückkehren müssen id, aber Sie werden wahrscheinlich instancetypeviel häufiger verwenden.

Steven Fisher
quelle
4
Es ist eine Weile her, seit ich das gefragt habe, und ich habe längst eine Haltung eingenommen, wie Sie sie hier dargelegt haben. Ich lasse es hier los, weil ich denke, dass dies leider als eine Frage des Stils für init angesehen wird und die Informationen in den früheren Antworten die Frage ziemlich klar beantwortet haben.
Griotspeak
6
Um es klar zu sagen: Ich glaube, die Antwort von Catfish_Man ist nur im ersten Satz richtig . Die Antwort von hooleyhoop ist bis auf den ersten Satz richtig . Es ist ein verdammt großes Dilemma. Ich wollte etwas bereitstellen, das sich im Laufe der Zeit als nützlicher und korrekter herausstellen würde als beide. (Bei allem Respekt vor diesen beiden; schließlich ist dies jetzt viel offensichtlicher als damals, als ihre Antworten geschrieben wurden.)
Steven Fisher
1
Mit der Zeit würde ich es hoffen. Ich kann mir keinen Grund vorstellen, dies nicht zu tun, es sei denn, Apple ist der Ansicht, dass es wichtig ist, die Quellkompatibilität aufrechtzuerhalten, um beispielsweise einem NSString ohne Besetzung ein neues NSArray zuzuweisen.
Steven Fisher
4
instancetypevs ist idwirklich keine Stilentscheidung. Die jüngsten Änderungen instancetypemachen wirklich deutlich, dass wir sie instancetypean Orten verwenden sollten, an -initdenen wir "eine Instanz meiner Klasse" meinen
Griotspeak
2
Sie sagten, es gäbe Gründe für die Verwendung von ID. Können Sie das näher erläutern? Die einzigen zwei, an die ich denken kann, sind Kürze und Abwärtskompatibilität; Wenn man bedenkt, dass beide auf Kosten Ihrer Absicht gehen, denke ich, dass dies wirklich schlechte Argumente sind.
Steven Fisher
10

Die obigen Antworten sind mehr als genug, um diese Frage zu erklären. Ich möchte nur ein Beispiel hinzufügen, damit die Leser es in Bezug auf die Codierung verstehen.

Klasse a

@interface ClassA : NSObject

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

@end

Klasse 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
quelle
Dies erzeugt nur dann einen Compilerfehler, wenn Sie #import "ClassB.h" nicht verwenden. Andernfalls geht der Compiler davon aus, dass Sie wissen, was Sie tun. Beispielsweise wird Folgendes kompiliert, stürzt jedoch zur Laufzeit ab: id myNumber = @ (1); id iAssumedThatWasAnArray = myNumber [0]; id iAssumedThatWasADictionary = [myNumber objectForKey: @ "key"];
OlDor
1

Details erhalten Sie auch unter The Designated Initializer

** **.

INSTANZETYP

** Dieses Schlüsselwort kann nur für den Rückgabetyp verwendet werden, der mit dem Rückgabetyp des Empfängers übereinstimmt. Die init-Methode wird immer als Rückgabetyp deklariert. Warum nicht zum Beispiel den Rückgabetyp Party für Party-Instanz festlegen? Das würde ein Problem verursachen, wenn die Parteiklasse jemals untergeordnet würde. Die Unterklasse würde alle Methoden von Party erben, einschließlich des Initialisierers und seines Rückgabetyps. Wenn eine Instanz der Unterklasse diese Initialisierungsnachricht gesendet hätte, wäre dies eine Rückgabe? Kein Zeiger auf eine Party-Instanz, sondern ein Zeiger auf eine Instanz der Unterklasse. Sie könnten denken, dass dies kein Problem ist. Ich werde den Initialisierer in der Unterklasse überschreiben, um den Rückgabetyp zu ändern. In Objective-C können Sie jedoch nicht zwei Methoden mit demselben Selektor und unterschiedlichen Rückgabetypen (oder Argumenten) verwenden. Durch Angabe, dass eine Initialisierungsmethode "

ICH WÜRDE

** Bevor der Instanztyp in Objective-C eingeführt wurde, geben Initialisierer die ID (eye-dee) zurück. Dieser Typ ist als "Zeiger auf ein beliebiges Objekt" definiert. (id ähnelt void * in C.) Zum jetzigen Zeitpunkt verwenden XCode-Klassenvorlagen immer noch id als Rückgabetyp für Initialisierer, die im Boilerplate-Code hinzugefügt wurden. Im Gegensatz zu instancetype kann id nicht nur als Rückgabetyp verwendet werden. Sie können Variablen oder Methodenparameter vom Typ id deklarieren, wenn Sie nicht sicher sind, auf welchen Objekttyp die Variable am Ende verweist. Sie können id verwenden, wenn Sie eine schnelle Aufzählung verwenden, um über ein Array mehrerer oder unbekannter Objekttypen zu iterieren. Beachten Sie, dass Sie beim Deklarieren einer Variablen oder eines Objektparameters dieses Typs kein * einfügen, da id nicht als "Zeiger auf ein Objekt" definiert ist.

Nam N. HUYNH
quelle