【问题标题】:Is it possible to make the -init method private in Objective-C?是否可以在 Objective-C 中将 -init 方法设为私有?
【发布时间】:2010-09-16 17:48:26
【问题描述】:

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

我该怎么做?

【问题讨论】:

  • 现在有一个特定的、干净的和描述性的工具来实现这一点,如this answer below 所示。具体来说:NS_UNAVAILABLE。我通常会敦促您使用这种方法。 OP会考虑修改他们接受的答案吗?这里的其他答案提供了很多有用的细节,但不是实现这一点的首选方法。
  • 正如其他人在下面指出的那样,NS_UNAVAILABLE 仍然允许调用者通过new 间接调用init。只需覆盖 init 以返回 nil 即可处理这两种情况。
  • 你当然可以创建 'new' NS_UNAVAILABLE 和 'init' - 这是今天的常见做法。

标签: objective-c


【解决方案1】:

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

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

有一个版本是disables the method only for Swift clients,不适用于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],因为该对象已经allocated。使用 ARC 时,编译器会为您调用它。无论如何,当您要故意停止执行时,不必担心。

objc_designated_initializer

如果您打算禁用 init 以强制使用指定的初始化程序,则有一个属性:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

这会产生一个警告,除非任何其他初始化方法在内部调用 myOwnInit。下一次 Xcode 发布后会在Adopting Modern Objective-C 发布详细信息(我猜)。

【讨论】:

  • 这对于init 以外的方法可能有用。既然,如果这个方法是无效的,你为什么要初始化一个对象呢?另外,当抛出异常时,您将能够指定一些自定义消息,将正确的init* 方法传达给开发人员,而在doesNotRecognizeSelector 的情况下您没有这样的选项。
  • 不知道 Aleks,不应该在那里 :) 我编辑了答案。
  • 这很奇怪,因为它最终导致您的系统崩溃。我想最好不要让事情发生,但我想知道是否有更好的方法。我想要的是让其他开发人员无法调用它并让它在“运行”或“构建”时被编译器赶上
  • 我试过了,但它不起作用: - (id) init __attribute__((unavailable("init not available"))) { NSAssert(false,@"Use initWithType");返回零; }
  • @Miraaj 听起来你的编译器不支持它。它在 Xcode 6 中受支持。如果初始化程序没有调用指定的初始化程序,您应该会得到“Convenience initializer missing a 'self' call to another initializer”。
【解决方案2】:

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

- (instancetype)init NS_UNAVAILABLE;

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

【讨论】:

  • 请注意,您仍然可以使用 [MyObject new]; 实例化对象
  • 你也可以做 + (instancetype)new NS_UNAVAILABLE;
  • @sonicfly 尝试这样做,但项目仍在编译
【解决方案3】:

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

如果你的 -init 方法被调用,你可以做的是抛出一个 NSInternalInconsistencyException

- (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 方法是您访问单例的方式,并将其记录为在标题中获取单例实例的方式。在绝大多数情况下,这应该就是您所需要的。

【讨论】:

  • 这里真的需要退货吗?
  • 是的,让编译器满意。否则编译器可能会抱怨返回非 void 的方法没有返回。
  • 有趣的是,它不适合我。也许不同的编译器版本或开关? (我只是在 XCode 3.1 中使用默认的 gcc 开关)
  • 指望开发人员遵循某种模式并不是一个好主意。最好抛出异常,这样不同团队中的开发人员就知道不要这样做。我的私人概念会更好。
  • “没有真正的显着优势”。完全不真实。显着的优势是您希望强制单例模式。如果您允许创建新实例,那么不熟悉 API 的开发人员可能会使用 allocinit 并使其代码功能不正确,因为它们具有正确的类,但错误的实例。这就是OO中封装原理的精髓。您在 API 中隐藏了其他类不应该需要或无法访问的内容。您不仅要公开所有内容,还希望人类跟踪所有内容。
【解决方案4】:

您可以使用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

【讨论】:

    【解决方案5】:

    如果你在谈论默认的 -init 方法,那么你不能。它是从 NSObject 继承的,每个类都会响应它而不会发出警告。

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

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

    【讨论】:

    • 这似乎是一种更好的方法,而不是仅仅将 'init' 单独留在你的单例中,并依靠文档与用户交流他们应该通过 'sharedWhatever' 访问。人们通常不会阅读文档,直到他们已经浪费了很多时间试图找出问题。
    【解决方案6】:

    把这个放到头文件里

    - (id)init UNAVAILABLE_ATTRIBUTE;
    

    【讨论】:

    【解决方案7】:

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

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

    MyClass.m

    @interface MyClass (PrivateMethods)
    - (NSString*) init;
    @end
    
    @implementation MyClass
    
    - (NSString*) init
    {
        // code...
    }
    
    @end
    

    MacRumors.com 上有一个不错的 thread 关于这个话题。

    【讨论】:

    • 不幸的是,在这种情况下,类别方法并没有真正的帮助。通常,它会在编译时警告您可能未在类上定义该方法。但是,由于 MyClass 必须继承自其中一个根类并且它们定义了 init,因此不会出现警告。
    【解决方案8】:

    为什么你不能让它“私有/不可见”的问题是因为 init 方法被发送到 id(因为 alloc 返回一个 id)而不是 YourClass

    请注意,从编译器(检查器)的角度来看,id 可能会响应任何输入的内容(它无法在运行时检查真正进入 id 的内容),因此只有在无处可隐藏时才可以隐藏 init( public = in header) 使用方法 init,编译器会知道,id 无法响应 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

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

    【讨论】:

      【解决方案9】:

      我不得不提一下,放置断言和引发异常以隐藏子类中的方法对于善意的人来说是一个令人讨厌的陷阱。

      我建议将__unavailable 用作Jano explained for his first example

      方法可以在子类中被覆盖。这意味着如果超类中的方法使用的方法只是在子类中引发异常,它可能不会按预期工作。换句话说,你刚刚打破了过去的工作。初始化方法也是如此。以下是此类相当常见的实现示例:

      - (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)) 作为不好的做法,虽然我没有深入使用它。)

      这也意味着您可以在必须在子类中覆盖的方法中使用断言和异常。 (Creating an abstract class in Objective-C 中的“抽象”方法)

      还有,不要忘记 +new 类方法。

      【讨论】:

        猜你喜欢
        • 2010-10-13
        • 2011-04-09
        • 1970-01-01
        • 2010-10-05
        • 2012-05-06
        • 1970-01-01
        • 2011-01-23
        • 2013-12-08
        • 1970-01-01
        相关资源
        最近更新 更多