【问题标题】:Is there any way to enforce typing on NSArray, NSMutableArray, etc.?有没有办法强制在 NSArray、NSMutableArray 等上输入?
【发布时间】:2010-10-13 13:32:51
【问题描述】:

我可以创建一个NSMutableArray 实例,其中所有元素都是SomeClass 类型吗?

【问题讨论】:

标签: objective-c generics data-structures collections strong-typing


【解决方案1】:

还没有人把这个放在这里,所以我会做的!

现在,Objective-C 正式支持此功能。从 Xcode 7 开始,您可以使用以下语法:

NSArray<MyClass *> *myArray = @[[MyClass new], [MyClass new]];

注意

请务必注意,这些只是编译器警告,从技术上讲,您仍然可以将任何对象插入到数组中。有一些可用的脚本可以将所有警告转化为会阻止构建的错误。

【讨论】:

  • 我在这里偷懒了,但为什么这只在 XCode 7 中可用?我们可以在 XCode 6 中使用nonnull,据我所知,它们是同时引入的。另外,这些概念的使用取决于 XCode 版本还是 iOS 版本?
  • @Guven - 可空性出现在 6 中,你是对的,但是直到 Xcode 7 才引入 ObjC 泛型。
  • 我很确定它仅取决于 Xcode 版本。泛型只是编译器警告,不会在运行时指示。我很确定你可以编译成你想要的任何操作系统。
  • @DeanKelly - 你可以这样做:@property (nonatomic, strong) NSArray&lt;id&lt;SomeProtocol&gt;&gt;* protocolObjects; 看起来有点笨拙,但确实有用!
  • @Logan,不仅有一组脚本可以在检测到任何警告的情况下阻止构建。 Xcode 有一个完善的机制,名为“配置”。看看这个boredzo.org/blog/archives/2009-11-07/warnings
【解决方案2】:

对于从强类型语言(如 C++ 或 Java)过渡到更弱或动态类型的语言(如 Python、Ruby 或 Objective-C)的人们来说,这是一个相对常见的问题。在Objective-C中,大多数对象继承自NSObject(类型id)(其余继承自另一个根类,例如NSProxy,也可以是类型id),任何消息都可以发送到任何目的。当然,向它无法识别的实例发送消息可能会导致运行时错误(并且还会导致带有适当 -W 标志的编译器警告)。只要一个实例响应您发送的消息,您就可能不在乎它属于哪个类。这通常被称为“鸭子打字”,因为“如果它像鸭子一样嘎嘎叫[即响应选择器],它就是鸭子[即它可以处理消息;谁在乎它是什么类]”。

您可以使用-(BOOL)respondsToSelector:(SEL)selector 方法测试实例在运行时是否响应选择器。假设您想在数组中的每个实例上调用一个方法,但不确定所有实例都可以处理该消息(所以您不能只使用NSArray-[NSArray makeObjectsPerformSelector:],这样的事情会起作用:

for(id o in myArray) {
  if([o respondsToSelector:@selector(myMethod)]) {
    [o myMethod];
  }
}

如果您控制实现您希望调用的方法的实例的源代码,更常见的方法是定义一个包含这些方法的@protocol,并声明相关类实现该协议在他们的声明中。在这种用法中,@protocol 类似于 Java 接口或 C++ 抽象基类。然后,您可以测试是否符合整个协议,而不是响应每种方法。在前面的示例中,它不会有太大区别,但是如果您调用多个方法,它可能会简化事情。该示例将是:

for(id o in myArray) {
  if([o conformsToProtocol:@protocol(MyProtocol)]) {
    [o myMethod];
  }
}

假设MyProtocol 声明myMethod。第二种方法受到青睐,因为它比第一种更清楚地说明了代码的意图。

通常,其中一种方法可以让您不必关心数组中的所有对象是否都属于给定类型。如果您仍然关心,标准的动态语言方法是单元测试,单元测试,单元测试。因为此要求中的回归会产生(可能无法恢复的)运行时(而非编译时)错误,所以您需要有测试覆盖来验证行为,这样您就不会将崩溃程序释放到野外。在这种情况下,执行修改数组的操作,然后验证数组中的所有实例是否属于给定类。通过适当的测试覆盖率,您甚至不需要验证实例身份的额外运行时开销。你确实有很好的单元测试覆盖率,不是吗?

【讨论】:

  • 单元测试不能替代一个体面的类型系统。
  • 是的,谁需要类型化数组能够提供的工具。我确信@BarryWark(以及其他接触过他需要使用、阅读、理解和支持的任何代码库的人)具有 100% 的代码覆盖率。但是我敢打赌,除非有必要,否则你不会使用原始的ids,就像 Java 编码人员绕过Objects 一样。为什么不?如果你有单元测试就不需要它吗?因为它存在并且使您的代码更易于维护,就像类型化数组一样。听起来人们在平台上投资并不想承认一点,因此发明了为什么这种遗漏实际上是一种好处的原因。
  • "鸭子打字"??那真好笑!以前从未听说过。
【解决方案3】:

您可以使用-addSomeClass: 方法创建一个类别,以允许编译时静态类型检查(因此,如果您尝试通过该方法添加一个它知道是不同类的对象,编译器会通知您),但是有没有真正的方法来强制数组只包含给定类的对象。

一般来说,Objective-C 似乎不需要这样的约束。我认为我从未听说过有经验的 Cocoa 程序员希望使用该功能。似乎只有其他语言的程序员仍在使用这些语言进行思考。如果您只想要数组中给定类的对象,则只需将该类的对象粘贴在其中。如果您想测试您的代码是否正常运行,请对其进行测试。

【讨论】:

  • 我认为“经验丰富的 Cocoa 程序员”只是不知道他们缺少什么——Java 经验表明类型变量可以提高代码理解能力,并使更多重构成为可能。
  • 嗯,Java 的泛型支持本身就被严重破坏了,因为他们没有从一开始就将其加入...
  • 必须同意@tgdavies。我怀念 C# 的智能感知和重构功能。当我想要动态类型时,我可以在 C# 4.0 中得到它。当我想要强类型的东西时,我也可以拥有它。我发现这两件事都有时间和地点。
  • @charkrit Objective-C 是什么让它“没有必要”?当你使用 C# 时,你觉得有必要吗?我听到很多人说你在 Objective-C 中不需要它,但我认为这些人认为你在任何语言中都不需要它,这使它成为偏好/风格的问题,而不是必要的。跨度>
  • 这不是让你的编译器真正帮助你发现问题。当然你可以说“如果你只想要一个给定类的对象在一个数组中,就只把那个类的对象粘在里面。”但是,如果测试是强制执行的唯一方法,那么您将处于不利地位。离编写代码越远,你发现问题的代价就越大。
【解决方案4】:

你可以继承 NSMutableArray 来强制类型安全。

NSMutableArrayclass cluster,因此子类化并非易事。我最终继承自 NSArray 并将调用转发到该类中的一个数组。结果是一个名为ConcreteMutableArray 的类, 易于子类化。这是我想出的:

更新:查看此blog post from Mike Ash,了解如何对类集群进行子类化。

在您的项目中包含这些文件,然后使用宏生成您希望的任何类型:

MyArrayTypes.h

CUSTOM_ARRAY_INTERFACE(NSString)
CUSTOM_ARRAY_INTERFACE(User)

MyArrayTypes.m

CUSTOM_ARRAY_IMPLEMENTATION(NSString)
CUSTOM_ARRAY_IMPLEMENTATION(User)

用法:

NSStringArray* strings = [NSStringArray array];
[strings add:@"Hello"];
NSString* str = [strings get:0];

[strings add:[User new]];  //compiler error
User* user = [strings get:0];  //compiler error

其他想法

  • 继承自NSArray,支持序列化/反序列化
  • 根据您的喜好,您可能想要覆盖/隐藏通用方法,例如

    - (void) addObject:(id)anObject

【讨论】:

  • 很好,但现在它通过覆盖一些方法而缺乏强类型。目前它只是弱类型。
【解决方案5】:

查看https://github.com/tomersh/Objective-C-Generics,这是一个针对 Objective-C 的编译时(预处理器实现)泛型实现。 This 博客文章有一个很好的概述。基本上你会得到编译时检查(警告或错误),但不会对泛型造成运行时损失。

【讨论】:

  • 我试过了,很好的主意,但遗憾的是有问题,它不检查添加的元素。
【解决方案6】:

This Github Project 正是实现了该功能。

然后您可以使用 &lt;&gt; 括号,就像在 C# 中一样。

从他们的例子中:

NSArray<MyClass>* classArray = [NSArray array];
NSString *name = [classArray lastObject].name; // No cast needed

【讨论】:

    【解决方案7】:

    2020,直截了当的答案。碰巧我需要一个类型为NSString 的可变数组。

    语法:

    Type<ArrayElementType *> *objectName;
    

    例子:

    @property(nonatomic, strong) NSMutableArray<NSString *> *buttonInputCellValues;
    

    【讨论】:

      【解决方案8】:

      一种可能的方法是继承 NSArray,但 Apple 建议不要这样做。对类型化 NSArray 的实际需求三思而后行会更简单。

      【讨论】:

      • 在编译时进行静态类型检查可以节省时间,编辑更好。当您编写 lib 以供长期使用时尤其有用。
      【解决方案9】:

      我创建了一个使用 NSArray 对象作为支持 ivar 的 NSArray 子类,以避免 NSArray 的类集群性质出现问题。接受或拒绝添加对象需要块。

      只允许 NSString 对象,你可以定义一个AddBlock

      ^BOOL(id element) {
          return [element isKindOfClass:[NSString class]];
      }
      

      您可以定义一个FailBlock 来决定在某个元素未通过测试时执行什么操作 - 正常过滤失败,将其添加到另一个数组,或者 - 这是默认设置 - 引发异常。

      VSBlockTestedObjectArray.h

      #import <Foundation/Foundation.h>
      typedef BOOL(^AddBlock)(id element); 
      typedef void(^FailBlock)(id element); 
      
      @interface VSBlockTestedObjectArray : NSMutableArray
      
      @property (nonatomic, copy, readonly) AddBlock testBlock;
      @property (nonatomic, copy, readonly) FailBlock failBlock;
      
      -(id)initWithTestBlock:(AddBlock)testBlock FailBlock:(FailBlock)failBlock Capacity:(NSUInteger)capacity;
      -(id)initWithTestBlock:(AddBlock)testBlock FailBlock:(FailBlock)failBlock;
      -(id)initWithTestBlock:(AddBlock)testBlock;    
      @end
      

      VSBlockTestedObjectArray.m

      #import "VSBlockTestedObjectArray.h"
      
      @interface VSBlockTestedObjectArray ()
      @property (nonatomic, retain) NSMutableArray *realArray;
      -(void)errorWhileInitializing:(SEL)selector;
      @end
      
      @implementation VSBlockTestedObjectArray
      @synthesize testBlock = _testBlock;
      @synthesize failBlock = _failBlock;
      @synthesize realArray = _realArray;
      
      
      -(id)initWithCapacity:(NSUInteger)capacity
      {
          if (self = [super init]) {
              _realArray = [[NSMutableArray alloc] initWithCapacity:capacity];
          }
      
          return self;
      }
      
      -(id)initWithTestBlock:(AddBlock)testBlock 
                   FailBlock:(FailBlock)failBlock 
                    Capacity:(NSUInteger)capacity
      {
          self = [self initWithCapacity:capacity];
          if (self) {
              _testBlock = [testBlock copy];
              _failBlock = [failBlock copy];
          }
      
          return self;
      }
      
      -(id)initWithTestBlock:(AddBlock)testBlock FailBlock:(FailBlock)failBlock
      {
          return [self initWithTestBlock:testBlock FailBlock:failBlock Capacity:0];
      }
      
      -(id)initWithTestBlock:(AddBlock)testBlock
      {
          return [self initWithTestBlock:testBlock FailBlock:^(id element) {
              [NSException raise:@"NotSupportedElement" format:@"%@ faild the test and can't be add to this VSBlockTestedObjectArray", element];
          } Capacity:0];
      }
      
      
      - (void)dealloc {
          [_failBlock release];
          [_testBlock release];
          self.realArray = nil;
          [super dealloc];
      }
      
      
      - (void) insertObject:(id)anObject atIndex:(NSUInteger)index
      {
          if(self.testBlock(anObject))
              [self.realArray insertObject:anObject atIndex:index];
          else
              self.failBlock(anObject);
      }
      
      - (void) removeObjectAtIndex:(NSUInteger)index
      {
          [self.realArray removeObjectAtIndex:index];
      }
      
      -(NSUInteger)count
      {
          return [self.realArray count];
      }
      
      - (id) objectAtIndex:(NSUInteger)index
      {
          return [self.realArray objectAtIndex:index];
      }
      
      
      
      -(void)errorWhileInitializing:(SEL)selector
      {
          [NSException raise:@"NotSupportedInstantiation" format:@"not supported %@", NSStringFromSelector(selector)];
      }
      - (id)initWithArray:(NSArray *)anArray { [self errorWhileInitializing:_cmd]; return nil;}
      - (id)initWithArray:(NSArray *)array copyItems:(BOOL)flag { [self errorWhileInitializing:_cmd]; return nil;}
      - (id)initWithContentsOfFile:(NSString *)aPath{ [self errorWhileInitializing:_cmd]; return nil;}
      - (id)initWithContentsOfURL:(NSURL *)aURL{ [self errorWhileInitializing:_cmd]; return nil;}
      - (id)initWithObjects:(id)firstObj, ... { [self errorWhileInitializing:_cmd]; return nil;}
      - (id)initWithObjects:(const id *)objects count:(NSUInteger)count { [self errorWhileInitializing:_cmd]; return nil;}
      
      @end
      

      像这样使用它:

      VSBlockTestedObjectArray *stringArray = [[VSBlockTestedObjectArray alloc] initWithTestBlock:^BOOL(id element) {
          return [element isKindOfClass:[NSString class]];
      } FailBlock:^(id element) {
          NSLog(@"%@ can't be added, didn't pass the test. It is not an object of class NSString", element);
      }];
      
      
      VSBlockTestedObjectArray *numberArray = [[VSBlockTestedObjectArray alloc] initWithTestBlock:^BOOL(id element) {
          return [element isKindOfClass:[NSNumber class]];
      } FailBlock:^(id element) {
          NSLog(@"%@ can't be added, didn't pass the test. It is not an object of class NSNumber", element);
      }];
      
      
      [stringArray addObject:@"test"];
      [stringArray addObject:@"test1"];
      [stringArray addObject:[NSNumber numberWithInt:9]];
      [stringArray addObject:@"test2"];
      [stringArray addObject:@"test3"];
      
      
      [numberArray addObject:@"test"];
      [numberArray addObject:@"test1"];
      [numberArray addObject:[NSNumber numberWithInt:9]];
      [numberArray addObject:@"test2"];
      [numberArray addObject:@"test3"];
      
      
      NSLog(@"%@", stringArray);
      NSLog(@"%@", numberArray);
      

      这只是一个示例代码,从未在实际应用程序中使用过。为此,它可能需要实现 mor NSArray 方法。

      【讨论】:

        【解决方案10】:

        如果您混合使用 c++ 和 Objective-c(即使用 mm 文件类型),您可以使用 pair 或 tuple 强制键入。例如,在下面的方法中,可以创建一个 std::pair 类型的 C++ 对象,将其转换为 OC wrapper 类型的对象(需要定义的 std::pair 的包装器),然后将其传递给一些其他 OC 方法,您需要将 OC 对象转换回 C++ 对象才能使用它。 OC 方法只接受 OC 包装类型,从而保证类型安全。您甚至可以使用元组、可变参数模板、类型列表来利用更高级的 C++ 特性来促进类型安全。

        - (void) tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) indexPath
        {
         std::pair<UITableView*, NSIndexPath*> tableRow(tableView, indexPath);  
         ObjCTableRowWrapper* oCTableRow = [[[ObjCTableRowWrapper alloc] initWithTableRow:tableRow] autorelease];
         [self performSelector:@selector(selectRow:) withObject:oCTableRow];
        }
        

        【讨论】:

          【解决方案11】:

          我的两分钱有点“干净”:

          使用类型定义:

          typedef NSArray<NSString *> StringArray;
          

          在代码中我们可以做到:

          StringArray * titles = @[@"ID",@"Name", @"TYPE", @"DATE"];
          

          【讨论】:

            猜你喜欢
            • 2022-01-06
            • 1970-01-01
            • 2021-01-01
            • 2014-04-17
            • 1970-01-01
            • 2010-11-04
            • 2012-06-06
            • 1970-01-01
            • 2021-08-07
            相关资源
            最近更新 更多