是否可以在Objective-C中将-init方法设为私有?

Is it possible to make the -init method private in Objective-C?

我需要把我班的-init方法隐藏在objective-c中。

我该怎么做?


NS_UNAVAILABLE

1
- (instancetype)init NS_UNAVAILABLE;

这是不可用属性的简短版本。它首次出现在MacOS 10.7和iOS 5中。它在nsobjcruntime.h中定义为#define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE

有一个版本仅对Swift客户机禁用该方法,而不是对objc代码禁用该方法:

1
- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

unavailable属性添加到头中,以在调用init时生成编译器错误。

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

compile time error

如果您没有理由,只需键入__attribute__((unavailable)),甚至键入__unavailable

1
-(instancetype) __unavailable init;

doesNotRecognizeSelector:

使用doesNotRecognizeSelector:提出一个nsinvalidargumentexception。"每当对象接收到它无法响应或转发的选择器消息时,运行时系统将调用此方法。"

1
2
3
4
5
- (instancetype) init {
    [self release];
    [super doesNotRecognizeSelector:_cmd];
    return nil;
}

NSAssert

使用NSAssert抛出nsInternalConsistenceyException并显示消息:

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

raise:format:

使用raise:format:抛出自己的异常:

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

需要[self release],因为对象已被alloc激活。当使用ARC时,编译器会为您调用它。在任何情况下,当你打算故意停止执行时,不要担心。

objc_designated_initializer

如果您打算禁用init以强制使用指定的初始值设定项,则有一个属性用于:

1
-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

除非任何其他初始值设定项方法在内部调用myOwnInit,否则将生成警告。在下一个Xcode发布之后,将在采用现代Objective-C中发布详细信息(我猜)。


Apple已开始在其头文件中使用以下内容来禁用init构造函数:

1
- (instancetype)init NS_UNAVAILABLE;

这在Xcode中正确显示为编译器错误。具体来说,这是在他们的几个healthkit头文件中设置的(hkUnit就是其中之一)。


与Smalltalk一样,Objective-C没有"私有"和"公共"方法的概念。任何消息都可以随时发送到任何对象。

如果调用了-init方法,则可以抛出NSInternalInconsistencyException

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

另一种选择——在实践中可能要好得多——是让-init尽可能为你的班级做一些明智的事情。

如果您试图这样做是因为您试图"确保"使用一个单例对象,不要费心。具体来说,不必费心用"override +allocWithZone:-init-retain-release创建单体的方法。它实际上总是不必要的,只是增加了复杂性,没有真正的显著优势。

相反,只需编写代码,使您的+sharedWhatever方法是如何访问单例的,并将其记录为在头中获取单例实例的方法。在大多数情况下,这应该是你所需要的。


把这个放在头文件里

1
- (id)init UNAVAILABLE_ATTRIBUTE;


如果您谈论的是默认的-init方法,那么您就不能这样做了。它是从nsObject继承的,并且每个类都不会对它做出任何警告。

您可以创建一个新的方法,比如-initmyclass,并将其放入Matt建议的私有类别中。然后定义default-init方法,以便在调用它时引发异常,或者(更好)使用一些默认值调用私有的-initmyclass。

人们希望隐藏init的主要原因之一是针对singleton对象。如果是这种情况,那么您不需要隐藏-init,只需返回singleton对象即可(如果尚不存在则创建它)。


那么,为什么不能将其设为"私有/不可见"的问题是因为init方法被发送到id(因为alloc返回一个id)而不是您的类

注意,从编译器(checker)的角度来看,一个ID可能会对任何输入的内容作出响应(它不能在运行时检查真正进入ID的内容),所以只有当没有任何地方(公开=在头中)会使用方法in it时,您才可以隐藏init,因为在任何地方都不是init(在源代码、所有libs等中)

所以不能禁止用户通过init并被编译器破坏…但是,您可以做的是,通过调用init来防止用户获得真正的实例。

只需实现init,它返回nil并具有(private/invisible)初始值设定项,该初始值设定项将命名其他人无法获取的名称(如initonce、initWithSpecial…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}

注意:有人可以这样做

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

它实际上会返回一个新的实例,但是如果initonce在我们的项目中不会公开(在头中)声明,它将生成一个警告(id可能不会响应…)并且无论如何,使用这个的人需要确切地知道真正的初始值设定项是initonce

我们可以进一步阻止这种情况,但没有必要


您可以使用NS_UNAVAILABLE声明任何方法都不可用。

所以您可以将这些行放在@interface下面

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

最好在前缀头中定义一个宏

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

1
2
3
4
5
6
@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end

这取决于你所说的"私人化"。在Objective-C中,对对象调用方法最好描述为向该对象发送消息。语言中没有禁止客户端对对象调用任何给定方法的内容;最好不要在头文件中声明该方法。如果客户机仍然使用正确的签名调用"private"方法,它仍将在运行时执行。

也就是说,在Objective-C中创建私有方法的最常见方法是在实现文件中创建一个类别,并声明其中所有的"隐藏"方法。记住,这不会真正阻止对init的调用运行,但是如果有人试图这样做,编译器会发出警告。

MyClass

1
2
3
4
5
6
7
8
9
10
11
12
@interface MyClass (PrivateMethods)
- (NSString*) init;
@end

@implementation MyClass

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

@end

关于这个话题,在macrumers.com上有一个不错的话题。


我必须提到,在子类中放置断言和引发异常以隐藏方法,对于计划良好的方法有一个令人讨厌的陷阱。

我建议使用__unavailable,正如jano在第一个例子中解释的那样。

方法可以在子类中重写。这意味着,如果超类中的方法使用了一个只在子类中引发异常的方法,那么它可能无法按预期工作。换句话说,你刚刚打破了过去的工作模式。初始化方法也是如此。下面是这样一个非常常见的实现示例:

1
2
3
4
5
6
7
8
9
10
11
- (SuperClass *)initWithParameters:(Type1 *)arg1 optional:(Type2 *)arg2
{
    ...bla bla...
    return self;
}

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

想象一下,如果我在子类中这样做,那么-initwithlessparameters会发生什么:

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

这意味着您应该倾向于使用私有(隐藏)方法,尤其是在初始化方法中,除非您计划重写这些方法。但是,这是另一个主题,因为您并不总是完全控制超类的实现。(这让我怀疑使用uu属性((objc_指定的初始值设定项))作为不好的实践,尽管我没有深入使用它。)

它还意味着您可以在必须在子类中重写的方法中使用断言和异常。(在Objective-C中创建抽象类的"抽象"方法)

而且,不要忘记+新的类方法。