在Objective-C中为一个类定义私有方法的最佳方式是什么?

363

我刚开始学习Objective-C编程,由于有Java背景,想知道编写Objective-C程序的人如何处理私有方法。

我理解可能有几种约定和习惯,并将此问题视为聚合使用Objective-C处理私有方法的最佳技术的内容。

发布时,请包含您的方法的论点。为什么它很好?它有哪些缺点(您所知道的)以及您如何处理它们?


至于我目前的发现。
可以使用categories(例如MyClass(Private))在MyClass.m文件中定义私有方法进行分组。
这种方法有2个问题:
1. Xcode(和编译器?)不会检查您是否在相应的@implementation块中定义了所有私有类别中的方法。 2. 您必须在MyClass.m文件的开头放置声明您的私有类别的@interface,否则Xcode会出现“self may not respond to message“privateFoo”的消息。
第一个问题可以通过使用empty category(例如MyClass())来解决。 第二个问题让我很困扰。我想看到私有方法在文件末实现(和定义);我不知道是否可能。

1
大家可能会觉得这个问题很有趣:https://dev59.com/MnI95IYBdhLWcg3wyBCc - bbum
4
为什么不只是省略私有方法的声明? - ma11hew28
12个回答

443

正如其他人已经说过的,Objective-C 中没有所谓的私有方法。但是,从 Objective-C 2.0 开始(即 Mac OS X Leopard、iPhone OS 2.0 及更高版本),您可以创建一个空名称的类别(即 @interface MyClass ())称为 类扩展。类扩展的独特之处在于方法实现必须与公共方法放在同一个 @implementation MyClass 中。因此我将我的类结构化如下:

在 .h 文件中:

@interface MyClass {
    // My Instance Variables
}

- (void)myPublicMethod;

@end

在.m文件中:

@interface MyClass()

- (void)myPrivateMethod;

@end

@implementation MyClass

- (void)myPublicMethod {
    // Implementation goes here
}

- (void)myPrivateMethod {
    // Implementation goes here
}

@end

我认为这种方法最大的优点是,它允许您按功能分组方法的实现,而不是按(有时是任意的)公共/私有区分。


8
它会生成一个“MYClass 可能无法响应‘-myPrivateMethod-’”的警告,而不是异常/错误。 - Özgür
2
这实际上已经开始出现在苹果的样板代码中了。++ - Chris Trahey
79
从LLVM 4编译器开始,您甚至不需要这样做。 您可以在实现内部定义它们,而无需将它们放入类扩展中。 - Abizern
1
如果你得到了@Comptrol提到的警告,那是因为你定义了一个方法在调用它的另一个方法下面(参见Andy的答案)--而你忽略这些警告是会有危险的。我曾经犯过这个错误,编译器一直没问题,直到我嵌套了这样的调用:if (bSizeDifference && [self isSizeDifferenceSignificant:fWidthCombined])... 然后fWidthCombined总是被传递为0。 - Wienke
6
不必担心顺序问题了。最新版本的LLVM即使方法出现在调用它的下方,也能找到该方法。 - Steve Waddicor

55
在Objective-C中,实际上并没有“私有方法”,如果运行时可以确定使用哪个实现,它就会这样做。但这并不意味着没有方法不是文档接口的一部分。对于这些方法,我认为分类是可以的。与其像您所说的第2点那样将@interface放在.m文件的顶部,我会将其放入自己的.h文件中。我遵循的惯例(也看到过其他地方这么做,我认为这是苹果的惯例,因为Xcode现在自动支持它)是将此类文件命名为其类和类别,并用+号分隔,因此@interface GLObject (PrivateMethods)可以在GLObject+PrivateMethods.h中找到。提供头文件的原因是让您可以在单元测试类中导入它 :-)。
顺便说一下,关于在.m文件末尾实现/定义方法的问题,您可以通过在.m文件底部实现类别来实现:
@implementation GLObject(PrivateMethods)
- (void)secretFeature;
@end

或者使用类扩展(你称之为空类别),只需在最后定义这些方法。Objective-C 方法可以在实现中以任何顺序定义和使用,因此没有什么阻止你将“私有”方法放在文件末尾。

即使使用类扩展,我通常也会创建一个单独的头文件(GLObject+Extension.h),以便在需要时使用这些方法,模仿“友元”或“受保护”的可见性。

自从这个答案最初编写以来,clang 编译器已经开始为 Objective-C 方法执行两次操作。这意味着你可以完全避免声明你的“私有”方法,无论它们是在调用站点上方还是下方,编译器都能找到它们。


39

虽然我不是 Objective-C 的专家,但我个人只是在类的实现中定义方法。当然,在调用它的任何方法之前必须定义它(在其上方),但这肯定是最省力的做法。


4
这个解决方案的优势在于它避免了仅仅为了避免编译器警告而添加不必要的程序结构。 - Jan Hettich
1
我也倾向于这样做,但并不是Objective-C专家。对于专家来说,除了方法排序问题之外,有没有任何不这样做的原因? - Eric Smith
2
方法排序似乎是一个小问题,但如果你将其转化为代码可读性,它就会变成一个非常重要的问题,特别是在团队合作中。 - borisdiakur
18
LLVM的最近版本不再关注方法的顺序,因此您可以自行决定方法的顺序,无需先声明。 - Steve Waddicor
请参考@justin的此回答 - lagweezle

24

@implementation块中定义您的私有方法是大多数情况下的理想选择。无论声明顺序如何,Clang都会在@implementation中看到这些方法。没有必要在类扩展(也称为类别)或命名类别中声明它们。

在某些情况下,您需要在类扩展中声明该方法(例如,如果在类扩展和@implementation之间使用选择器)。

static函数非常适合特别敏感或速度关键的私有方法。

使用命名前缀的约定可以帮助您避免意外覆盖私有方法(我发现将类名作为前缀是安全的)。

命名类别(例如@interface MONObject(PrivateStuff))不是一个特别好的选择,因为在加载时可能会出现名称冲突。它们真正有用的只有友元或受保护的方法(这很少是一个好选择)。为确保您收到未完成类别实现的警告,您应该实际实现它:

@implementation MONObject (PrivateStuff)
...HERE...
@end

这是一个小的带注释的备忘单:

MONObject.h

@interface MONObject : NSObject

// public declaration required for clients' visibility/use.
@property (nonatomic, assign, readwrite) bool publicBool;

// public declaration required for clients' visibility/use.
- (void)publicMethod;

@end

MONObject.m

@interface MONObject ()
@property (nonatomic, assign, readwrite) bool privateBool;

// you can use a convention where the class name prefix is reserved
// for private methods this can reduce accidental overriding:
- (void)MONObject_privateMethod;

@end

// The potentially good thing about functions is that they are truly
// inaccessible; They may not be overridden, accidentally used,
// looked up via the objc runtime, and will often be eliminated from
// backtraces. Unlike methods, they can also be inlined. If unused
// (e.g. diagnostic omitted in release) or every use is inlined,
// they may be removed from the binary:
static void PrivateMethod(MONObject * pObject) {
    pObject.privateBool = true;
}

@implementation MONObject
{
    bool anIvar;
}

static void AnotherPrivateMethod(MONObject * pObject) {
    if (0 == pObject) {
        assert(0 && "invalid parameter");
        return;
    }

    // if declared in the @implementation scope, you *could* access the
    // private ivars directly (although you should rarely do this):
    pObject->anIvar = true;
}

- (void)publicMethod
{
    // declared below -- but clang can see its declaration in this
    // translation:
    [self privateMethod];
}

// no declaration required.
- (void)privateMethod
{
}

- (void)MONObject_privateMethod
{
}

@end

另一种可能不太明显的方法是:C++类型既可以非常快速,又可以提供更高程度的控制,同时最小化导出和加载的Objective-C方法数量。

1
在方法名前使用完整的类名作为前缀,这样比仅使用下划线或甚至您自己的TLA更安全。如果私有方法在您另一个项目中使用的库中,并且您忘记了您已经在一两年前使用过该名称,那会怎么样呢? - big_m

14

您可以尝试在您的实现下面或上面定义一个静态函数,它接受一个指向您的实例的指针。它将能够访问任何您的实例变量。

//.h file
@interface MyClass : Object
{
    int test;
}
- (void) someMethod: anArg;

@end


//.m file    
@implementation MyClass

static void somePrivateMethod (MyClass *myClass, id anArg)
{
    fprintf (stderr, "MyClass (%d) was passed %p", myClass->test, anArg);
}


- (void) someMethod: (id) anArg
{
    somePrivateMethod (self, anArg);
}

@end

1
苹果公司保留了以下划线开头的名称供自己使用。 - Georg Schölly
1
如果您不使用苹果的框架怎么办?我经常在没有苹果框架的情况下开发Objective-C代码,事实上我在Linux、Windows和Mac OS X上构建。无论如何,我还是将其删除了,因为大多数编写Objective-C代码的人可能会在Mac OS X上使用它。 - dreamlax
3
我认为这是.m文件中真正的私有方法。其他类别方法实际上并不是私有的,因为你无法在@interface...@end块中将方法标记为私有。 - David.Chu.ca
为什么要这样做?如果你在方法定义的开头添加“-”,你就可以在不将其作为参数传递的情况下访问“self”。 - Guy
1
@Guy:因为这样方法可以被反射检测到,因此根本不是私有的。 - dreamlax

3
每个 Objective-C 对象都符合 NSObject 协议,该协议包含 performSelector: 方法。我之前也在寻找一种创建一些不需要在公共级别上暴露的“助手或私有”方法的方法。如果您想创建一个没有开销且不必在头文件中定义的私有方法,则可以尝试以下方法...
使用与下面代码类似的签名来定义您的方法...
-(void)myHelperMethod: (id) sender{
     // code here...
}

然后当您需要引用该方法时,只需将其作为选择器调用...
[self performSelector:@selector(myHelperMethod:)];

这行代码将调用你创建的方法,并且不会出现在头文件中未定义的烦人警告。


6
这样一来,你就无法传递第三个参数。 - Li Fumin

3
你可以使用块吗?
@implementation MyClass

id (^createTheObject)() = ^(){ return [[NSObject alloc] init];};

NSInteger (^addEm)(NSInteger, NSInteger) =
^(NSInteger a, NSInteger b)
{
    return a + b;
};

//public methods, etc.

- (NSObject) thePublicOne
{
    return createTheObject();
}

@end

我知道这是一个旧问题,但当我正在寻找答案时,这是我发现的第一个问题之一。我没有看到其他地方讨论过这个解决方案,所以如果有什么愚蠢的地方,请告诉我。


1
你在这里所做的是创建了一个全局块类型变量,这并不比函数更好(而且甚至不是真正的私有,因为它没有声明为“static”)。但我一直在尝试将块分配给私有实例变量(从init方法中)-有点像JavaScript风格-这也允许访问私有实例变量,这是从静态函数中不可能实现的。还不确定哪个更好。 - big_m

2
如果你想避免在顶部使用@interface块,你可以将私有声明放在另一个文件MyClassPrivate.h中。虽然不是最理想的方法,但它不会使实现混乱。

MyClass.h

interface MyClass : NSObject {
 @private
  BOOL publicIvar_;
  BOOL privateIvar_;
}

@property (nonatomic, assign) BOOL publicIvar;
//any other public methods. etc
@end

MyClassPrivate.h

@interface MyClass ()

@property (nonatomic, assign) BOOL privateIvar;
//any other private methods etc.
@end

MyClass.m

#import "MyClass.h"
#import "MyClassPrivate.h"
@implementation MyClass

@synthesize privateIvar = privateIvar_;
@synthesize publicIvar = publicIvar_;

@end

2

这里还有一件事情,我没有看到在这里提到 - Xcode支持名称中带有“_private”的.h文件。假设你有一个类MyClass - 你有MyClass.m和MyClass.h,现在你也可以有MyClass_private.h。Xcode会识别它并将其包含在助理编辑器的“Counterparts”列表中。

//MyClass.m
#import "MyClass.h"
#import "MyClass_private.h"

1

在问题#2上没有变通的方法。这就是C编译器(因此也是Objective-C编译器)的工作方式。如果您使用XCode编辑器,函数弹出菜单应该很容易让您浏览文件中的@interface@implementation块。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接