5
\$\begingroup\$

In Objective-C, many of the NSString or NSMutableString methods for comparing or manipulating strings require a range argument--that is, an argument of data type NSRange (a struct containing a starting position and a length). This applies outside of NSString (NSArray for example), but NSString is where I see it most frequently.

There does exist a convenience function for creating an NSRange struct:

NSRange range = NSMakeRange(position, length);

But the problem for me happens when I'm manipulating a string. I'll sometimes write a method in which I will make several different manipulations on the string within the same method. And the range I need to cover will be the entire length of the string. It's not acceptable to simply declare an NSRange variable, set it, and use this throughout, because if the length of the string changes, the previously set range will be incorrect.

For example, suppose we're working with strings representing file paths. We want to manipulate the string in a few ways to make sure the path looks how we want it to look:

- (NSString *)cleanPath:(NSString *)originalPath inRoot:(NSString *)rootPath {
    NSString *returnPath = [originalPath stringByReplacingOccurrencesOfString:@" " withString:@"" options:NSCaseInsensitiveCompare:NSMakeRange(0,[originalPath length])];

    while ([returnPath rangeOfString:@"./"].location != NSNotFound) {
        returnPath = [returnPath stringByReplacingOccurrencesOfString:@"./" withString:@"/" options:NSCaseInsensitiveCompare:NSMakeRange(0,[returnPath length])];
    }

    if ([returnPath rangeOfString:rootPath options:NSCaseInsensitiveCompare range:NSMakeRange(0,[returnPath length])] != NSNotFound) {
        return returnPath;
    } else {
        return rootPath;
    }
}

As you can see, we have three calls to NSMakeRange just to make a range that encompasses the length of the string we're working on. We want to look at the whole range. Giving a range too short will mean we're not looking at every character, and giving a range too long will result in an out of bounds exception. We also can't know what the range will be.

We know that the first range will be the longest range, because we're only removing characters.

Also, even though the call only shows up in code once in the while loop, it could be called any number of times in actuality, so we can't even preset a range before the loop and use that number every time.

The other annoying thing is that NSMakeRange(0,[myString length]) doesn't offer a ton of autocomplete help.

I also don't want to simply #define a macro that could accidentally have unintended consequences outside of this method.

What are my options to clean this up?


This is tagged C because even though the code here is Objective-C, NSRange is a C-style struct and NSMakeRange is a C-style function. Any C techniques for cleaning up something like this will definitely apply here.

\$\endgroup\$
4
\$\begingroup\$

I see a couple of ways to clean this up.

First of all, you mention not wanting to use #define, but let's not forget about #undef. You could define a macro as the first line of this method and undefine it as the last line of the method.

For example:

- (NSString *)cleanPath:(NSString *)originalPath inRoot:(NSString *)rootPath {
   #define _STRING_RANGE(string) (NSMakeRange(0,[string length]))
   // code
   #undef _STRING_RANGE
}

Now, this macro definition exists only within this method. You don't necessarily save a whole lot of characters, but autocomplete will be a lot nicer, and you don't have to explicitly tell the macro to start at 0 and go to the end...you've predefined that in the macro.


You can also just write your own C-Style function. That's all NSMakeRange is after all.

NSRange stringRange(NSString *string) {
    return NSMakeRange(0,[string length]);
}

And while defining a function is fine and dandy, now it can be seen by at least everything in the file. This is Objective-C, so we can use blocks. We can create a block within the method, and now the block, much like the #define macro that we #undef exists only within the method:

- (NSString *)cleanPath:(NSString *)originalPath inRoot:(NSString *)rootPath {
    NSRange (^stringRange)(NSString *) = (^NSString *string) {
        return NSMakeRange(0,[string length]);
    }
    // code...
}

Here we've define a block called stringRange, that returns a NSRange and takes a single argument of type NSString *. This is exactly the same as the function we wrote in option two, and it's even called in the exact same way too.

stringRange(myString)

There a couple of important differences between a regular C-style function and a block though. Scope and memory.

With this block example, the block's scope is limited only to this method. It also exists in memory on the stack and only until this method returns (or until the end of an @autoreleasepool it was created in, whichever comes first).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.