Ist es möglich, die Methode -init in Objective-C privat zu machen?

147

Ich muss die -initMethode meiner Klasse in Objective-C ausblenden (privat machen) .

Wie kann ich das machen?

lajos
quelle
3
Es gibt jetzt eine spezifische, saubere und beschreibende Möglichkeit, um dies zu erreichen, wie in dieser Antwort unten gezeigt . Insbesondere : NS_UNAVAILABLE. Ich möchte Sie generell dringend bitten, diesen Ansatz zu verwenden. Würde das OP erwägen, seine akzeptierte Antwort zu überarbeiten? Die anderen Antworten hier liefern viele nützliche Details, sind jedoch nicht die bevorzugte Methode, um dies zu erreichen.
Benjohn
Wie andere unten erwähnt haben, NS_UNAVAILABLEkann ein Anrufer weiterhin initindirekt über aufrufen new. Durch einfaches Überschreiben initder Rückgabe nilwerden beide Fälle behandelt.
Greg Brown

Antworten:

88

Objective-C hat wie Smalltalk kein Konzept für "private" oder "öffentliche" Methoden. Jede Nachricht kann jederzeit an ein beliebiges Objekt gesendet werden.

Was Sie tun können, ist eine zu werfen NSInternalInconsistencyException wenn Ihre -initMethode aufgerufen wird:

- (id)init {
    [self release];
    @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                   reason:@"-init is not a valid initializer for the class Foo"
                                 userInfo:nil];
    return nil;
}

Die andere Alternative - die in der Praxis wahrscheinlich weitaus besser ist - besteht darin, -initwenn möglich etwas Sinnvolles für Ihre Klasse zu tun.

Wenn Sie dies versuchen, weil Sie sicherstellen möchten, dass ein Singleton-Objekt verwendet wird, stören Sie sich nicht. Genauer gesagt, nicht mit der Mühe „Aufschalten +allocWithZone:, -init, -retain, -release“ Methode der singletons erzeugen. Es ist praktisch immer unnötig und fügt nur Komplikationen hinzu, ohne wirklich einen signifikanten Vorteil zu erzielen.

Schreiben Sie stattdessen einfach Ihren Code so, dass Sie mit Ihrer +sharedWhateverMethode auf einen Singleton zugreifen, und dokumentieren Sie dies, um die Singleton-Instanz in Ihrem Header abzurufen. Das sollte in den allermeisten Fällen alles sein, was Sie brauchen.

Chris Hanson
quelle
2
Ist die Rückgabe hier tatsächlich notwendig?
Philsquared
5
Ja, um den Compiler bei Laune zu halten. Andernfalls kann sich der Compiler beschweren, dass von einer Methode mit nicht ungültiger Rückgabe keine Rückgabe erfolgt.
Chris Hanson
Komisch, das ist nichts für mich. Vielleicht eine andere Compilerversion oder Schalter? (Ich verwende nur die Standard-GCC-Schalter mit XCode 3.1)
Philsquared
3
Es ist keine gute Idee, sich darauf zu verlassen, dass der Entwickler einem Muster folgt. Es ist besser, eine Ausnahme auszulösen, damit Entwickler in einem anderen Team dies nicht wissen. Ich privates Konzept wäre besser.
Nick Turner
4
"Für keinen wirklich signifikanten Vorteil" . Völlig falsch. Der wesentliche Vorteil besteht darin, dass Sie das Singleton-Muster erzwingen möchten . Wenn Sie neue Instanzen erlauben erstellt werden, dann ist ein Entwickler, der nicht vertraut mit der API ist zu verwenden allocund initund ihre Code - Funktion hat falsch , weil sie die richtige Klasse haben, aber die falsche Instanz. Dies ist die Essenz des Prinzips der Einkapselung in OO. Sie verstecken Dinge in Ihren APIs, auf die andere Klassen keinen Zugriff benötigen oder erhalten sollten. Sie halten nicht nur alles öffentlich und erwarten von den Menschen, dass sie alles im Auge behalten.
Nate
345

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

Dies ist die Kurzversion des nicht verfügbaren Attributs. Es erschien zuerst in macOS 10.7 und iOS 5 . Es ist in NSObjCRuntime.h als definiert #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE.

Es gibt eine Version, die die Methode nur für Swift-Clients deaktiviert , nicht für ObjC-Code:

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

Fügen Sie das unavailableAttribut dem Header hinzu, um bei jedem Aufruf von init einen Compilerfehler zu generieren .

-(instancetype) init __attribute__((unavailable("init not available")));  

Kompilierungszeitfehler

Wenn Sie keinen Grund haben, geben Sie einfach ein __attribute__((unavailable))oder sogar __unavailable:

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

Verwenden Sie doesNotRecognizeSelector:diese Option, um eine NSInvalidArgumentException auszulösen. "Das Laufzeitsystem ruft diese Methode immer dann auf, wenn ein Objekt eine aSelector-Nachricht empfängt, auf die es nicht antworten oder die er nicht weiterleiten kann."

- (instancetype) init {
    [self release];
    [super doesNotRecognizeSelector:_cmd];
    return nil;
}

NSAssert

Verwenden Sie NSAssertdiese Option, um NSInternalInconsistencyException auszulösen und eine Nachricht anzuzeigen:

- (instancetype) init {
    [self release];
    NSAssert(false,@"unavailable, use initWithBlah: instead");
    return nil;
}

raise:format:

Verwenden Sie raise:format:diese Option, um Ihre eigene Ausnahme auszulösen:

- (instancetype) init {
    [self release];
    [NSException raise:NSGenericException 
                format:@"Disabled. Use +[[%@ alloc] %@] instead",
                       NSStringFromClass([self class]),
                       NSStringFromSelector(@selector(initWithStateDictionary:))];
    return nil;
}

[self release]wird benötigt, weil das Objekt bereits allocbearbeitet wurde. Wenn Sie ARC verwenden, ruft der Compiler es für Sie auf. Auf jeden Fall kein Grund zur Sorge, wenn Sie die Ausführung absichtlich beenden möchten.

objc_designated_initializer

Falls Sie initdie Verwendung eines bestimmten Initialisierers deaktivieren möchten, gibt es dafür ein Attribut:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

Dies generiert eine Warnung, es sei denn, eine andere Initialisierungsmethode ruft myOwnInitintern auf. Details werden nach der nächsten Xcode-Veröffentlichung in Adopting Modern Objective-C veröffentlicht (denke ich).

Jano
quelle
Dies kann für andere Methoden als gut sein init. Wenn diese Methode ungültig ist, warum initialisieren Sie dann ein Objekt? Wenn Sie eine Ausnahme auslösen, können Sie außerdem eine benutzerdefinierte Nachricht angeben init*, die dem Entwickler die richtige Methode mitteilt , während Sie im Falle von keine solche Option haben doesNotRecognizeSelector.
Aleks N.
Keine Ahnung Aleks, das sollte nicht da sein :) Ich habe die Antwort bearbeitet.
Jano
Das ist komisch, weil es Ihr System zum Absturz gebracht hat. Ich denke, es ist besser, etwas nicht passieren zu lassen, aber ich frage mich, ob es einen besseren Weg gibt. Was ich möchte, ist, dass andere Entwickler es nicht aufrufen können und es vom Compiler beim "Ausführen" oder "
Erstellen
1
Ich habe es versucht und es funktioniert nicht: - (id) init __attribute __ ((nicht verfügbar ("init nicht verfügbar"))) {NSAssert (false, @ "Use initWithType"); return nil; }
Okysabeni
1
@Miraaj Klingt so, als würde es von Ihrem Compiler nicht unterstützt. Es wird in Xcode 6 unterstützt. Wenn ein Initialisierer den angegebenen Aufruf nicht aufruft, sollte "Convenience-Initialisierer einen Selbstaufruf an einen anderen Initialisierer verpassen" angezeigt werden.
Jano
101

Apple hat damit begonnen, Folgendes in seinen Header-Dateien zu verwenden, um den Init-Konstruktor zu deaktivieren:

- (instancetype)init NS_UNAVAILABLE;

Dies wird in Xcode korrekt als Compilerfehler angezeigt. Dies wird insbesondere in mehreren ihrer HealthKit-Headerdateien festgelegt (HKUnit ist eine davon).

lehn0058
quelle
3
Beachten Sie jedoch, dass Sie das Objekt weiterhin mit [MyObject new] instanziieren können.
José
11
Sie können auch + (instancetype) new NS_UNAVAILABLE ausführen.
Sonicfly
@sonicfly Versucht, das zu tun, aber das Projekt kompiliert noch
CyberMew
3

Wenn Sie über die Standardmethode -init sprechen, können Sie dies nicht. Es wurde von NSObject geerbt und jede Klasse wird ohne Warnungen darauf antworten.

Sie können eine neue Methode erstellen, z. B. -initMyClass, und sie in eine private Kategorie einordnen, wie von Matt vorgeschlagen. Definieren Sie dann die Standardmethode -init, um entweder eine Ausnahme auszulösen, wenn sie aufgerufen wird, oder (besser) Ihre private -initMyClass mit einigen Standardwerten aufzurufen.

Einer der Hauptgründe, warum Leute init verstecken wollen, sind Singleton-Objekte . Wenn dies der Fall ist, müssen Sie -init nicht ausblenden. Geben Sie stattdessen einfach das Singleton-Objekt zurück (oder erstellen Sie es, wenn es noch nicht vorhanden ist).

Nathan Kinsinger
quelle
Dies scheint ein besserer Ansatz zu sein, als nur "init" in Ihrem Singleton allein zu lassen und sich auf die Dokumentation zu verlassen, um dem Benutzer mitzuteilen, auf den er über "sharedWhatever" zugreifen soll. Normalerweise lesen die Leute keine Dokumente, bis sie bereits viele Minuten damit verbracht haben, ein Problem herauszufinden.
Greg Maletic
3

Fügen Sie dies in die Header-Datei ein

- (id)init UNAVAILABLE_ATTRIBUTE;
Jerry Juang
quelle
Dies wird nicht empfohlen. Apples moderne Objective-C-Dokumente besagen, dass init den Instanztyp und nicht die ID zurückgeben sollte. developer.apple.com/library/ios/releasenotes/ObjectiveC/…
lehn0058
5
und es ist: - (instancetype) init NS_UNAVAILABLE;
Bandejapaisa
3

Sie können jede Methode als nicht verfügbar deklarieren NS_UNAVAILABLE.

Sie können diese Zeilen also unter Ihre @ Schnittstelle setzen

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;

Definieren Sie noch besser ein Makro in Ihrem Präfix-Header

#define NO_INIT \
- (instancetype)init NS_UNAVAILABLE; \
+ (instancetype)new NS_UNAVAILABLE;

und

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end
Kaunteya
quelle
2

Das hängt davon ab, was Sie unter "privat machen" verstehen. In Objective-C kann das Aufrufen einer Methode für ein Objekt besser als das Senden einer Nachricht an dieses Objekt beschrieben werden. Es gibt nichts in der Sprache, was einen Client daran hindert, eine bestimmte Methode für ein Objekt aufzurufen. Das Beste, was Sie tun können, ist, die Methode nicht in der Header-Datei zu deklarieren. Wenn ein Client dennoch die "private" Methode mit der richtigen Signatur aufruft, wird sie zur Laufzeit weiterhin ausgeführt.

Die häufigste Methode zum Erstellen einer privaten Methode in Objective-C besteht darin, eine Kategorie in der Implementierungsdatei zu erstellen und alle darin enthaltenen "versteckten" Methoden zu deklarieren. Denken Sie daran, dass dies nicht wirklich verhindert, dass Aufrufe initausgeführt werden, aber der Compiler gibt Warnungen aus, wenn jemand dies versucht.

MyClass.m

@interface MyClass (PrivateMethods)
- (NSString*) init;
@end

@implementation MyClass

- (NSString*) init
{
    // code...
}

@end

Auf MacRumors.com gibt es einen anständigen Thread zu diesem Thema.

Matt Dillard
quelle
3
Leider hilft in diesem Fall der Kategorieansatz nicht wirklich. Normalerweise werden Warnungen zur Kompilierungszeit ausgegeben, dass die Methode möglicherweise nicht für die Klasse definiert ist. Da MyClass jedoch von einer der Root-Klassen erben muss und init definiert, erfolgt keine Warnung.
Barry Wark
2

Nun, das Problem, warum Sie es nicht "privat / unsichtbar" machen können, ist, dass die init-Methode an id gesendet wird (da alloc eine ID zurückgibt) und nicht an YourClass

Beachten Sie, dass vom Standpunkt des Compilers (Checker) aus eine ID möglicherweise auf alles reagieren kann, was jemals eingegeben wurde (sie kann nicht überprüfen, was zur Laufzeit wirklich in die ID eingeht), sodass Sie init nur dann ausblenden können, wenn nichts nirgendwo ist (öffentlich = in) Header) Verwenden Sie eine Methode init, als die Kompilierung wissen würde, dass es keine Möglichkeit gibt, dass id auf init reagiert, da es nirgendwo init gibt (in Ihrer Quelle, in allen Bibliotheken usw.)

Sie können dem Benutzer also nicht verbieten, init zu übergeben und vom Compiler zerschlagen zu werden. Sie können jedoch verhindern, dass der Benutzer durch Aufrufen eines init eine echte Instanz erhält

einfach durch die Implementierung von init, das nil zurückgibt und einen (privaten / unsichtbaren) Initialisierer hat, dessen Name jemand anderes nicht erhält (wie initOnce, initWithSpecial ...)

static SomeClass * SInstance = nil;

- (id)init
{
    // possibly throw smth. here
    return nil;
}

- (id)initOnce
{
    self = [super init];
    if (self) {
        return self;
    }
    return nil;
}

+ (SomeClass *) shared 
{
    if (nil == SInstance) {
        SInstance = [[SomeClass alloc] initOnce];
    }
    return SInstance;
}

Hinweis: dass jemand dies tun könnte

SomeClass * c = [[SomeClass alloc] initOnce];

und es würde tatsächlich eine neue Instanz zurückgeben, aber wenn das initOnce nirgends in unserem Projekt öffentlich (im Header) deklariert würde, würde es eine Warnung erzeugen (ID könnte nicht antworten ...) und die Person, die dies verwendet, würde es sowieso brauchen um genau zu wissen, dass der eigentliche Initialisierer der initOnce ist

wir könnten dies noch weiter verhindern, aber es besteht keine Notwendigkeit

Peter Lapisu
quelle
0

Ich muss erwähnen, dass das Platzieren von Behauptungen und das Auslösen von Ausnahmen zum Ausblenden von Methoden in der Unterklasse eine böse Falle für die gut gemeinten Personen darstellt.

Ich würde empfehlen, __unavailablewie Jano für sein erstes Beispiel erklärt hat .

Methoden können in Unterklassen überschrieben werden. Dies bedeutet, dass eine Methode in der Oberklasse, die eine Methode verwendet, die nur eine Ausnahme in der Unterklasse auslöst, wahrscheinlich nicht wie beabsichtigt funktioniert. Mit anderen Worten, Sie haben gerade gebrochen, was früher funktioniert hat. Dies gilt auch für Initialisierungsmethoden. Hier ist ein Beispiel für eine solche ziemlich häufige Implementierung:

- (SuperClass *)initWithParameters:(Type1 *)arg1 optional:(Type2 *)arg2
{
    ...bla bla...
    return self;
}

- (SuperClass *)initWithLessParameters:(Type1 *)arg1
{
    self = [self initWithParameters:arg1 optional:DEFAULT_ARG2];
    return self;
}

Stellen Sie sich vor, was mit -initWithLessParameters passiert, wenn ich dies in der Unterklasse mache:

- (SubClass *)initWithParameters:(Type1 *)arg1 optional:(Type2 *)arg2
{
    [self release];
    [super doesNotRecognizeSelector:_cmd];
    return nil;
}

Dies bedeutet, dass Sie dazu neigen sollten, private (versteckte) Methoden zu verwenden, insbesondere bei Initialisierungsmethoden, es sei denn, Sie planen, die Methoden überschreiben zu lassen. Dies ist jedoch ein anderes Thema, da Sie bei der Implementierung der Oberklasse nicht immer die volle Kontrolle haben. (Dies lässt mich die Verwendung von __attribute ((objc_designated_initializer)) als schlechte Praxis in Frage stellen, obwohl ich es nicht ausführlich verwendet habe.)

Dies bedeutet auch, dass Sie Zusicherungen und Ausnahmen in Methoden verwenden können, die in Unterklassen überschrieben werden müssen. (Die "abstrakten" Methoden wie beim Erstellen einer abstrakten Klasse in Objective-C )

Und vergessen Sie nicht die + neue Klassenmethode.

techniao
quelle