NSRangeMattt Chester Liu 🚩🌱

NSRange 是 Foundation 框架中核心的类型之一。在框架代码中我们经常看到它作为函数的参数或者返回值类型,因此掌握好 NSRange 这个结构会有很多用处。这周的文章将会做你的引路人。


范围这个数据类型用来描述一系列连续整数当中的一个区间。它们最常被用到字符串,数组和具有类似顺序的集合类型上。

对于 Objective-C 程序来说,Foundation 的 NSRange 被用于表示范围。在别的语言中,范围经常被编码成一个有两个元素的数组,其中包含起点和终点的索引。Foundation 中的 NSRange 采用了一种不同的方法,它把范围编码成一个包含着位置和长度的结构体。在 Xcode 里通过在 NSRange 符号上做命令点击(⌘-ʘ),我们可以直接跳转到它在 Foundation/NSRange.h 中的定义:

typedef struct _NSRange {
    NSUInteger location;
    NSUInteger length;
} NSRange;

从实践上看,这种办法减少了常见的“偏移一位”的错误发生的情况。例如下面的 Javascript 和 Objective-C 代码做的同样的事情,通过给定的字符串得到所有字符的范围:

range.js

var string = "hello, world";
var range = [0, string.length - 1];

如果忘记给末尾的索引减 1,Javascript 会遇到越界错误。

range.m

NSString *string = @"hello, world";
NSRange range = NSMakeRange(0, [string length]);

NSRange 的办法相对更加清晰和不容易出错,尤其是在范围值上进行复杂的算术运算时更为明显。

用法

字符串

NSString *string = @"lorem ipsum dolor sit amet";
NSRange range = [string rangeOfString:@"ipsum"];
// {.location=6, .length=5}

NSString *substring = [string substringWithRange:range];
// @"ipsum"

NSString 没有类似 containsString: 的方法。可以使用 rangeOfString: 这个方法检测字符串位置是不是 NSNotFound(译者注:iOS 8 中增加了 containsString: 方法):

NSString *input = ...;
if ([input rangeOfString:@"keyword"].location != NSNotFound) {
    // ...
}

数组

NSArray *array = @[@"a", @"b", @"c", @"d"];
NSArray *subarray = [array subarrayWithRange:NSMakeRange(1, 2)];
// @[@"b", @"c"]

索引集

NSIndexSet 是一个和 NSRange 相似的 Foundation 集合类型。不同的是,它支持非连续的序列。一个 NSIndexSet 可以从 range 中创建,使用 indexSetWithIndexesInRange: 类构造器:

NSRange range = NSMakeRange(0, 10);
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:range];

函数

由于 NSRange 不是一个类,创建和使用它的实例都是通过函数调用来进行的,而不是实例方法。

许多 NSRange 的函数都违反了 Foundation 和 CoreFoundation 中的现代命名惯例,即在两个字母的命名空间以后,直接跟上函数所关联的类型。例如 NSMakeRange 应该被命名成 NSRangeMake,和 CGRectMake 以及 CGSizeMake 等等相仿。同样的,NSEqualRanges 更好的名字应该是 NSRangeEqualToRange,就像 CGPointEqualToPoint

尽管内部的一致性问题似乎不值得我们花费时间精力去替换掉已有的用法,这个 gist 展示了如何创建自己的一套对强迫症更友好的代码基础。

创建 NSRange

  • NSMakeRange: 根据指定的值创建一个新的 NSRange。
NSArray *array = @[@1, @2, @3];
NSRange range = NSMakeRange(0, [array count]);
// {.location=0, .length=3}

查询信息

  • NSEqualRanges: 返回一个指示给出的两个范围是否相等的布尔值。
NSRange range1 = NSMakeRange(0, 6);
NSRange range2 = NSMakeRange(2, 7);
BOOL equal = NSEqualRanges(range1, range2); // NO
  • NSLocationInRange: 返回一个指示给定的位置是否存在于给定的范围的布尔值。
NSRange range = NSMakeRange(3, 4);
BOOL contained = NSLocationInRange(5, range); // YES
  • NSMaxRange: 返回范围的位置和长度的和。
NSRange range = NSMakeRange(3, 4);
NSUInteger max = NSMaxRange(range); // 7

集合操作

  • NSIntersectionRange: 返回给定范围的交集。如果返回的范围长度字段为 0,则两个给定的范围值没有交集。位置字段的值是未定义的。
NSRange range1 = NSMakeRange(0, 6);
NSRange range2 = NSMakeRange(2, 7);
NSRange intersectionRange = NSIntersectionRange(range1, range2);
// {.location=2, .length=4}
  • NSUnionRange: 返回给定范围的并集,即一个包含 range1 和 range2 当中和它们之间的值的 range。如果一个范围被完全包含在另一个之内,返回值是较大的那一个。
NSRange range1 = NSMakeRange(0, 6);
NSRange range2 = NSMakeRange(2, 7);
NSRange unionRange = NSUnionRange(range1, range2);
// {.location=0, .length=9}

NSString * 和 NSRange 互相转换

  • NSStringFromRange: 返回一个范围的字符串表示。
NSRange range = NSMakeRange(3, 4);
NSString *string = NSStringFromRange(range); // @"{3,4}"
  • NSRangeFromString: 返回从文字表示中得到的一个范围.
NSString *string = @"{1,5}";
NSRange range = NSRangeFromString(string);
// {.location=1, .length=5}

如果传入 NSRangeFromString 当中的字符串不能表示一个有效的范围,它会返回一个位置和长度都设为 0 的结果。

NSString *string = @"invalid";
NSRange range = NSRangeFromString(string);
// {.location=0, .length=0}

可能有人会想通过 NSStringFromRange 来对 NSRange 类型进行装箱,使其可以用于 NSArray 当中,正确的方法应该是 NSValue +valueWithRange:

NSRange range = NSMakeRange(0, 3);
NSValue *value = [NSValue valueWithRange:range];

NSRange 是极少数的部分底层实现内联暴露在公共头文件中的类型之一。

Foundation/NSRange.h

NS_INLINE NSRange NSMakeRange(NSUInteger loc, NSUInteger len) {
    NSRange r;
    r.location = loc;
    r.length = len;
    return r;
}

NS_INLINE NSUInteger NSMaxRange(NSRange range) {
    return (range.location + range.length);
}

NS_INLINE BOOL NSLocationInRange(NSUInteger loc, NSRange range) {
    return (!(loc < range.location) && (loc - range.location) < range.length) ? YES : NO;
}

NS_INLINE BOOL NSEqualRanges(NSRange range1, NSRange range2) {
    return (range1.location == range2.location && range1.length == range2.length);
}

NSRangePointer

关于 NSRange 还有一个诡异的部分值得一提,就是 NSRangePointer。“神马?”,你可能会觉得恐慌和困惑。跳转到源码之后,证实了我们最深层的恐惧:

Foundation/NSRange.h

typedef NSRange *NSRangePointer;

所以,在没有确定来源的情况下,我们只能大胆猜测,这个类型是一个心存好意的框架工程师添加的,他注意到了由于 NSRange 是一个结构体而不是类导致的混乱。NSRange *NSRangePointer 是一样的,然而后者可以在 Foundation 框架中的很多地方看到,作为各种方法的输出参数。例如 NSAttributedString 使用 NSRangePointer 来返回在从某个特定索引处开始一个属性的实际有效范围(因为属性有效的范围起点和终点有可能在指定的索引以外):

NSMutableAttributedString *mutableAttributedString = ...;
NSRange range;
if ([mutableAttributedString attribute:NSUnderlineStyleAttributeName
                               atIndex:0
                        effectiveRange:&range])
{
    // Make underlined text blue as well
    [mutableAttributedString addAttribute:NSForegroundColorAttributeName
                                    value:[UIColor blueColor]
                                    range:range];
}

CFRange

最后一个说明:Core Foundation 框架同样定义了一个 CFRange 类型,和 NSRange不同的是,它的成员使用 CFIndex 类型,同时只有一个函数 CFRangeMake

typedef struct {
    CFIndex location;
    CFIndex length;
} CFRange;

当和 CoreText 或者其他底层 C API 交互的时候,更有可能碰到 CFRange 而不是 NSRange


除非另有声明,本文采用知识共享「署名-非商业性使用 3.0 中国大陆」许可协议授权。