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


147

我需要-init在Objective-C中隐藏(设为私有)我的类的方法。

我怎样才能做到这一点?


3
现在有一个特定的,干净的和描述性的工具可以实现此目的,如下面的答案所示。具体来说:NS_UNAVAILABLE。我通常会敦促您使用这种方法。OP会考虑修改他们接受的答案吗?这里的其他答案提供了许多有用的细节,但不是实现此目的的首选方法。
约翰

如下面其他人所述,NS_UNAVAILABLE仍然允许调用方通过init间接调用new。只需覆盖init返回即可nil处理这两种情况。
格雷格·布朗

Answers:


88

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

您可以做的是在调用NSInternalInconsistencyException您的-init方法时抛出:

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

另一种选择-在实践中可能要好得多-尽可能让-init您的班级做出明智的选择。

如果由于要“确保”使用单例对象而尝试执行此操作,请不要打扰。具体地,不与打扰“覆盖+allocWithZone:-init-retain-release”创建单身的方法。实际上,这几乎是不必要的,只是增加了复杂性而没有真正的明显优势。

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


2
这里真的需要退货吗?
philsquared

5
是的,让编译器满意。否则,编译器可能会抱怨具有非无效返回的方法没有返回。
克里斯·汉森

有趣,它不适合我。也许是不同的编译器版本或开关?(我只是在XCode 3.1中使用默认的gcc开关)
philsquared

3
指望开发人员遵循模式不是一个好主意。最好抛出一个异常,以便其他团队中的开发人员不知道。我的私人观念会更好。
尼克·特纳

4
“没有真正的明显优势”。完全不真实。显着的优点是您想强制执行单例模式。如果您允许创建新实例,那么不熟悉API的开发人员可能会使用allocinit使其代码功能不正确,因为它们具有正确的类,但是实例错误。这是OO 中封装原理的本质。您在API中隐藏了其他类不需要或无法访问的东西。您不仅要将所有内容都公开,还希望人类能够对其进行跟踪。
2014年

345

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

这是不可用属性的简短版本。它首先出现在macOS 10.7iOS 5中。在NSObjCRuntime.h中将其定义为#define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE

有一个版本仅对Swift客户端禁用此方法,而不对ObjC代码禁用

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

unavailable属性添加到标头,以在对init的任何调用上生成编译器错误

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

编译时错误

如果没有理由,请输入__attribute__((unavailable)),甚至__unavailable

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

使用doesNotRecognizeSelector:提出一个NSInvalidArgumentException。“只要对象收到无法响应或转发的aSelector消息,运行时系统就会调用此方法。”

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

NSAssert

使用NSAssert抛出NSInternalInconsistencyException并显示一条消息:

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

raise:format:

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

- (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强制使用指定的初始化程序,则可以使用以下属性:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

除非myOwnInit内部有其他初始化方法调用,否则将生成警告。下一个Xcode发布后,详细信息将发布在“ 采用现代Objective-C”中(我想)。


这对于除之外的其他方法可能是好的init。因为,如果此方法无效,那么为什么要初始化一个对象?另外,抛出异常时,您可以指定一些自定义消息,将正确的init*方法传达给开发人员,而在情况下则没有这种选择doesNotRecognizeSelector
Aleks N.

不知道Aleks,不应该在那儿:)我编辑了答案。
Jano 2012年

这很奇怪,因为它最终导致系统崩溃。我想最好不要让某些事情发生,但是我想知道是否有更好的方法。我想要的是其他开发人员无法调用它,并使其在“运行”或“构建”时被编译器捕获
okysabeni

1
我试过了,它不起作用:-(id)init __attribute __((unavailable(“ init not available”))){NSAssert(false,@“使用initWithType”); 返回零;}
okysabeni

1
@Miraaj听起来好像您的编译器不支持。Xcode 6中支持它。如果初始化程序未调用指定的初始化程序,则应该得到“缺少对另一个初始化程序的“自我”调用的便捷初始化程序”。
Jano 2014年

101

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

- (instancetype)init NS_UNAVAILABLE;

这在Xcode中正确显示为编译器错误。具体来说,这是在其多个HealthKit头文件中设置的(HKUnit是其中之一)。


3
注意,尽管您仍然可以使用[MyObject new]实例化该对象;
何塞

11
您也可以+(instancetype)new NS_UNAVAILABLE;
sonicfly

@sonicfly尝试这样做,但项目仍可编译
Cyber​​Mew

3

如果您正在谈论默认的-init方法,那么您就不能这样做。它是从NSObject继承的,每个类都将对其进行响应而不会发出警告。

您可以创建一个新方法,例如-initMyClass,并将其放在Matt所建议的私有类别中。然后定义默认的-init方法以引发异常(如果已调用)或(更好)使用一些默认值调用您的私有-initMyClass。

人们似乎想要隐藏init的主要原因之一是单例对象。如果是这种情况,则无需隐藏-init,只需返回单例对象(或在尚不存在时创建它)。


这似乎是一种更好的方法,而不是仅在您的单例中单独保留“ init”,并依靠文档与用户进行交流,告知他们应该通过“ sharedWhatever”进行访问。人们通常在浪费大量时间试图找出问题之前,才会阅读文档。
格雷格·马莱蒂奇


3

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

因此,您可以将这些行放在@interface下面

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

甚至可以在前缀标头中定义宏

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

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end

2

这取决于您“私有化”的意思。在Objective-C中,在对象上调用方法可能更好地描述为向该对象发送消息。语言中没有什么可以阻止客户端调用对象上的任何给定方法。最好的办法是不要在头文件中声明该方法。但是,如果客户端使用正确的签名调用“私有”方法,它仍将在运行时执行。

也就是说,在Objective-C中创建私有方法的最常见方法是在实现文件中创建Category,然后在其中声明所有“隐藏”方法。请记住,这并不能真正阻止调用的init运行,但是如果有人尝试执行此操作,则编译器会发出警告。

MyClass.m

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

@implementation MyClass

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

@end

有一个像样的线程有关此主题的MacRumors.com。


3
不幸的是,在这种情况下,类别方法并没有真正的帮助。通常,它会向您发出编译时警告,提示您该方法可能未在类中定义。但是,由于MyClass必须从根类之一继承,并且它们定义了init,因此不会发出警告。
巴里·沃克

2

好了,为什么不能将其设为“私有/不可见”的问题是导致init方法发送给id(因为alloc返回一个id)而不是YourClass

请注意,从编译器(检查器)的角度来看,一个id可能会对曾经键入的任何内容做出响应(它无法在运行时检查该id中真正包含的内容),因此您只能在无处可寻的情况下隐藏init(public = in标头)使用一种方法init(比编译会知道的那样),因为id在任何地方都没有init(在您的源代码中,所有的lib等等),所以没有办法响应init。

因此您不能禁止用户通过init并被编译器破坏...但是您可以做的是通过调用init来防止用户获取真实实例

只需通过实现init即可,该init返回nil并具有一个(私有/不可见的)初始化器,该初始化器的其他人将无法获得该名字(例如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;
}

注意:有人可以这样做

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

它实际上会返回一个新实例,但是如果initOnce在项目中的任何地方都不会在头中公开声明,它将生成警告(id可能不响应...),无论如何,使用此方法的人都需要确切知道真正的初始化器是initOnce

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


0

我不得不提到,在子类中放置断言和引发异常以隐藏方法会给善意的人带来麻烦。

我会建议使用__unavailable作为Jano的他的第一个例子来说明

方法可以在子类中覆盖。这意味着,如果超类中的方法使用的方法仅在子类中引发异常,则该方法可能无法按预期工作。换句话说,您已经打破了以前的工作方式。初始化方法也是如此。这是这种相当常见的实现的示例:

- (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会发生什么,如果我在子类中这样做:

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

这意味着您应该倾向于使用私有(隐藏)方法,尤其是在初始化方法中,除非您计划覆盖这些方法。但是,这是另一个主题,因为您并不总是完全控制超类的实现。(这使我质疑将__attribute((objc_designated_initializer))用作不好的做法,尽管我没有深入使用它。)

它还暗示您可以在必须在子类中覆盖的方法中使用断言和异常。(如在Objective-C创建抽象类中的“抽象”方法)

并且,不要忘记+ new class方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.