15. Numbers, Strings, and Collections

This chapter describes how to work with some of the basic objects provided in the Foundation framework. These include numbers; strings; and collections, which refers to the capability to work with groups of objects in the form of arrays, dictionaries, and sets.

The Foundation framework contains a plethora of classes, methods, and functions for you to use. Approximately 100 header files are available on the Mac, and 70 are available as part of GNUStep that you can import into your program. As a convenience, you can simply use the following import:

#import <Foundation/Foundation.h>

Then you don't have to worry about whether you are importing the correct header file. This header file is automatically imported into your program by Project Builder when you create a Foundation Tool, as described previously in Chapter 14, “Introduction to the Foundation Framework.”

Because the Foundation.h file imports virtually all the other Foundation header files, using this statement can add a significant amount of time to your compiles. You can, however, avoid this extra time by using precompiled headers. These are files that have been preprocessed by the compiler. By default, all Project Builder projects benefit from precompiled headers. In this chapter, you'll use the specific header files for each object you use. This will help you become familiar with what's contained in each header file.

Number Objects

All the numeric data types we've dealt with up to now, such as integers, floats, and longs, are basic data types in the Objective-C language; that is, they are not objects. For example, you can't send messages to them. Sometimes, though, you need to work with these values as objects. For example, the Foundation object NSArray enables you to set up an array in which you can store values. These values have to be objects, so you can't directly store any of your basic data types in these arrays. Instead, to store any of the basic numeric data types (including the char data type), you can use the NSNumber class to create objects from these data types. This is shown in Program 15.1.

Program 15.1.


// Working with Numbers

#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSValue.h>

#import <Foundation/NSString.h>

int main (int argc, char *argv[])
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSNumber          *myNumber, *floatNumber, *intNumber;
    int               i;

    // integer value

    intNumber = [NSNumber numberWithInt: 100];
    printf ("%i ", [intNumber intValue]);

    // long value

    myNumber = [NSNumber numberWithLong: 0xabcdef];
    printf ("%lx ", [myNumber longValue]);

    // char value

    myNumber = [NSNumber numberWithChar: 'X'];
    printf ("%c ", [myNumber charValue]);


    // float value

    floatNumber = [NSNumber numberWithFloat: 100.00];
    printf ("%g ", [floatNumber floatValue]);

    // double

    myNumber = [NSNumber numberWithDouble: 12345e+15];
    printf ("%Lg ", [myNumber doubleValue]);

    // Wrong access here

    printf ("%i ", [myNumber intValue]);

    // Test two Numbers for equality

  if ([intNumber isEqualToNumber: floatNumber] == YES)
        printf ("Numbers are equal ");
  else
        printf ("Numbers are not equal ");


  // Test if one Number is <, ==, or > second Number

  if ([intNumber compare: myNumber] == NSOrderedAscending)
        printf ("First number is less than second ");

  [pool release];
  return 0;
}


Program 15.1. Output


100
abcdef
X
100
1.2345e+19
2147483647
Numbers are equal
First number is less than second


The interface file <Foundation/NSValue.h> is needed to work with objects from the NSNumber class.

A Quick Look at the Autorelease Pool

The line in Program 15.1 that reads

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

reserves space in memory for an autorelease pool that you assign to pool. Put this line at the beginning of main in all your Foundation programs—if you're using Project Builder (and following the steps outlined in Chapter 14), it has already been put there for you. If not, you'll also need to include the header file <Foundation/NSAutoreleasePool.h> in your program.

At the end of main, before you exit your program, you should release the allocated memory pool with a line such as follows:

[pool release];

Again, Project Builder automatically inserts this line into your program for you.

In a nutshell, the autorelease pool provides for the automatic release of memory used by objects that are added to this pool. An object is added to the pool when it is sent an autorelease message. When the pool is released, so are all the objects that were added to it. Therefore, all such objects are destroyed unless they have been specified to exist beyond the scope of the autorelease pool (as indicated by their reference counts).

In general, you don't need to worry about releasing an object returned by a Foundation method. Sometimes the object is owned by the method that returns it. Other times, the object is newly created and added to the autorelease pool by the method. As described in detail in Part I, “The Objective-C Language,” you do still need to release any objects (including Foundation objects) that you explicitly create using the alloc method when you're done using them.1

Reference counts and the autorelease pool are described in full detail in Chapter 17, “Memory Management.”

Let's return to Program 15.1. The NSNumber class contains many methods that allow you to create NSNumber objects with initial values. For example, the line

intNumber = [NSNumber numberWithInt: 100];

creates an object from an integer whose value is 100.

The value retrieved from an NSNumber object must be consistent with the type of value that was stored in it. So, in the printf statement that follows in the program, the message expression

[intNumber intValue]

retrieves the integer value stored inside intNumber and displays it using the correct printf formatting characters.

For each basic value, a class method allocates an NSNumber object and sets it to a specified value. These methods begin with numberWith followed by the type, as in numberWithLong:, numberWithFloat:, and so on. In addition, instance methods can be used to set a previously allocated NSNumber object to a specified value. These all begin with initWith—as in initWithLong: and initWithFloat:.

Table 15.1 lists the class and instance methods for setting values for NSNumber objects and the corresponding instance methods for retrieving their values.

Table 15.1. NSNumber Creation and Retrieval Methods

image

Returning to Program 15.1, the program next uses the class methods to create long, char, float, and double NSNumber objects. Notice what happens after you create a double object with the line

myNumber = [NSNumber numberWithDouble: 12345e+15];

and then try to (incorrectly) retrieve and display its value with the following line:

printf ("%i ", [myNumber intValue]);

You get this output:

2147483647

Also, you get no error message from the system. In general, it's up to you to ensure that, if you store a value in an NSNumber object, you retrieve it in a consistent manner.

Inside the if statement, the message expression

[intNumber isEqualToNumber: floatNumber]

uses the isEqualToNumber: method to numerically compare two NSNumber objects. The Boolean value returned is tested by the program to see whether the two values are equal.

The compare: method can be used to test whether one numeric value is numerically less than, equal to, or greater than another. The message expression

[intNumber compare: myNumber]

returns the value NSOrderedAscending if the numeric value stored in intNumber is less than the numeric value contained in myNumber, returns the value NSOrderedSame if the two numbers are equal, and returns the value NSOrderedDescending if the first number is greater than the second. The values returned are defined in the header file NSObject.h for you.

You should note that you can't reinitialize the value of a previously created NSNumber object. For example, you can't set the value of an integer stored in the NSNumber object myNumber with a statement such as follows:

[myNumber initWithInt: 1000];

This statement generates an error when the program is executed. All number objects must be newly created, meaning you must invoke either one of the methods listed in the first column of Table 15.1 on the NSNumber class or one of the methods listed in column two with the result from the alloc method, like so:

myNumber = [[NSNumber alloc] initWithInt: 1000];

Of course, based on previous discussions, if you create myNumber this way, you are responsible for subsequently releasing it when you're done using it with a statement such as follows:

[myNumber release];

You'll encounter NSNumber objects again in programs throughout the remainder of this chapter.

String Objects

You've encountered character strings in your programs before. Whenever you enclosed a sequence of character strings inside a pair of double quotes, as in

"Programming is fun"

you created a character string in Objective-C. Each time you used printf you also specified the format of the output with a character string. These character strings are often referred to as C-strings or C-style strings because they are a basic data type from the underlying C language.

Like the basic data types in Objective-C, C-strings are not objects. The Foundation framework supports a class called NSString for working with character string objects. Whereas C-strings are composed of char characters, NSString objects are composed of unichar characters. A unichar character is a multibyte character according to the Unicode standard. This enables you to work with character sets that can contain literally millions of characters. Luckily, you don't have to worry about the internal representation of the characters in your strings because it's all handled for you automatically by the NSString class.2 By using the methods from this class, you can more easily develop applications that can be localized—that is, made to work in different languages all over the world.

To create a constant character string object in Objective-C, you put the @ character in front of the string. So, the expression

@"Programming is fun"

creates a constant character string object. In particular, it is a constant character string belonging to the class NSConstantString.3 Make sure you understand the distinction between writing a character string with and without the @ character in front of it.

NSConstantString is a subclass of the string object class NSString. To use string objects in your program, include the following line:

#import <Foundation/NSString.h>

The NSLog Function

A function in the Foundation library called NSLog is similar to printf. The first argument to NSLog is the format string; however, in this case it expects a string object. Program 15.2 illustrates the use of NSLog to duplicate in functionality what you did in your first program.

Program 15.2.


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  NSLog (@"Programming is fun ");

  [pool release];
  return 0;
}


Program 15.2. Output


2003-07-10 13:31:53.664 a.out[3621] Programming is fun


Note that the output from NSLog includes the date and time as well as the name of the program that generated the log message.

Program 15.3, which follows, shows how to define an NSString object and assign an initial constant character string to it. You can use NSLog to display a string object using the format characters %@. In the case of printf, it doesn't know about string objects, so an NSString object has to be converted back to a C-string, where it can be displayed with the %s format characters.4 You should realize that C strings do not store Unicode characters, so you might lose data when you convert an NSString object to a C string.

Program 15.3.


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSString *str = @"Programming is fun";

  NSLog (@"%@ ", str);
  printf ("%s ", [str cString]);
  [pool release];
  return 0;
}


Program 15.3. Output


2003-07-10 13:32:53.894 a.out[3629] Programming is fun
Programming is fun


In the line

NSString *str = @"Programming is fun";

the constant string object Programming is fun is assigned to the NSString variable str. Its value is then displayed using NSLog and printf. In the latter case, as mentioned, it must first be converted to a C string using the cString method from the NSString class.

The NSLog format characters %@ can be used not just to display NSString objects, but to display any object as well. For example, given the following:

NSNumber *intNumber = [NSNumber numberWithInt: 100];

the NSLog call

NSLog (@"%@ ", intNumber);

produces the following output:

2003-07-27 21:02:49.054 nslogn[1921] 100

The %@ format characters can even be used to display the entire contents of arrays, dictionaries, and sets. In fact, they can be used to display your own class objects as well, provided you override the description method inherited by your class. If you don't override the method, NSLog simply displays the name of the class and the address of your object in memory (that's the default implementation for the description method that is inherited from the NSObject class).

Mutable Versus Immutable Objects

When you create a string object by writing an expression such as

@"Programming is fun"

you create an object whose contents cannot be changed. This is referred to as an immutable object. The NSString class deals with immutable strings. Frequently, you'll want to deal with strings and change characters within the string. For example, you might want to delete some characters from a string or perform a search-and-replace operation on a string. These types of strings are handled through the NSMutableString class.

Program 15.4 shows basic ways to work with immutable character strings in your programs.

Program 15.4.


// Basic String Operations

#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSString *str1 = @"This is string A";
  NSString *str2 = @"This is string B";
  NSString *res;
  NSComparisonResult compareResult;

  // Count the number of characters

  printf ("Length of str1: %i ", [str1 length]);

  // Copy one string to another

  res = [NSString stringWithString: str1];
  printf ("Copy: %s ", [res cString]);

  // Copy one string to the end of another

  str2 = [str1 stringByAppendingString: str2];
  printf ("Concatentation: %s ", [str2 cString]);

  // Test if 2 strings are equal

  if ([str1 isEqualToString: res] == YES)
        printf ("str1 == res ");
  else
        printf ("str1 != res ");

  // Test if one string is <, == or > than another

  compareResult = [str1 compare: str2];

  if (compareResult == NSOrderedAscending)
        printf ("str1 < str2 ");
  else if (compareResult == NSOrderedSame)
        printf ("str1 == str2 ");
  else // must be NSOrderedDescending
        printf ("str1 > str2 ");

  // Convert a string to uppercase

  res = [str1 uppercaseString];
  printf ("Uppercase conversion: %s ", [res cString]);

  // Convert a string to lowercase

  res = [str1 lowercaseString];
  printf ("Lowercase conversion: %s ", [res cString]);

  printf ("Original string: %s ", [str1 cString]);

  [pool release];
  return 0;
}


Program 15.4. Output


Length of str1: 16
Copy: This is string A
Concatentation: This is string AThis is string B
str1 == res
str1 < str2
Uppercase conversion: THIS IS STRING A
Lowercase conversion: this is string a
Original string: This is string A


Program 15.4 first declares three immutable NSString objects: str1, str2, and res. The first two are initialized to constant character string objects. The declaration

NSComparisonResult compareResult;

declares compareResult to hold the result of the string comparison that will be performed later in the program.

The length method can be used to count the number of characters in a string. The output verifies that the string

@"This is string A"

contains 16 characters. The statement

res = [NSString stringWithString: str1];

shows how to create a new character string with the contents of another. The resulting NSString object is assigned to res and is then displayed to verify the results. An actual copy of the string contents is made here, not just another reference to the same string in memory. That means that str1 and res refer to two different string objects, which is different from simply performing a simple assignment, as follows:

res = str1;

This simply creates another reference to the same object in memory.

The stringByAppendingString: method can be used to join two character strings. So, the expression

[str1 stringByAppendingString: str2]

creates a new string object that consists of the characters str1 followed by str2, returning the result. The original string objects, str1 and str2, are not affected by this operation (they can't be because they're both immutable string objects).

The isEqualToString: method is used next to test to see whether two character strings are equal—that is, contain the same characters. The compare: method can be used instead if you need to determine the ordering of two character strings—for example, if you wanted to sort an array of them. Similar to the compare: method you used earlier for comparing two NSNumber objects, the result of the comparison is NSOrderedAscending if the first string is lexically less than the second string, NSOrderedSame if the two strings are equal, and NSOrderedDescending if the first string is lexically greater than the second. If you don't want to perform a case-sensitive comparison, use the caseInsensitiveCompare: method instead of compare: to compare two strings. In such a case, the two string objects @"Gregory" and @"gregory" would compare as equal with caseInsensitiveCompare:.

The uppercaseString and lowercaseString are the last two NSString methods used in Program 15.4 to convert strings to uppercase and lowercase, respectively. Once again, the conversion does not affect the original strings, as verified by the last line of output.

Program 15.5 illustrates additional methods for dealing with strings. These methods enable you to extract substrings from a string as well as search one string for the occurrence of another.

Some methods require that you identify a substring by specifying a range. A range consists of a starting index number plus a character count. Index numbers begin with zero, so the first three characters in a string would be specified by the pair of numbers {0, 3} . The special data type NSRange is used by some methods from the NSString class (and other Foundation classes as well) to create a range specification. It is defined in <Foundation/NSRange.h> (which is included for you from inside <Foundation/NSString.h>) and is actually a typedef definition for a structure that has two members, called location and length.5 This data type is used in Program 15.5.

Program 15.5.


// Basic String Operations - Continued

#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSString *str1 = @"This is string A";
  NSString *str2 = @"This is string B";
  NSString *res;
  NSRange  subRange;

  // Extract first 4 chars from string

  res = [str1 substringToIndex: 3];
  printf ("First 3 chars of str1: %s ", [res cString]);

  // Extract chars to end of string starting at index 5

  res = [str1 substringFromIndex: 5];
  printf ("Chars from index 5 of str1: %s ", [res cString]);

  // Extract chars from index 8 through 13 (6 chars)

  res = [[str1 substringFromIndex: 8] substringToIndex: 6];
  printf ("Chars from index 8 through 13: %s ", [res cString]);

  // An easier way to do the same thing

  res = [str1 substringWithRange: NSMakeRange (8, 6)];
  printf ("Chars from index 8 through 13: %s ", [res cString]);

  // Locate one string inside another

  subRange = [str1 rangeOfString: @"string A"];
  printf ("String is at index %i, length is %i ",
                subRange.location, subRange.length);

  subRange = [str1 rangeOfString: @"string B"];

  if (subRange.location == NSNotFound)
        printf ("String not found ");
  else
        printf ("String is at index %i, length is %i ",
               subRange.location, subRange.length);

  [pool release];
  return 0;
}


Program 15.5. Output


First 3 chars of str1: Thi
Chars from index 5 of str1: is string A
Chars from index 8 through 13: string
Chars from index 8 through 13: string
String is at index 8, length is 8
String not found


The substringToIndex: method creates a substring from the leading characters in a string up to but not including the specified index number. Because indexing begins at zero, the argument of 3 extracts characters 0, 1, and 2 from the string and returns the resulting string object. For any of the string methods that take an index number as one of their arguments, you'll get a Range or index out of bounds error message if you provide an invalid index number in the string.

The substringFromIndex: method returns a substring from the receiver beginning with the character at the specified index and up through the end of the string.

The expression

res = [[str1 substringFromIndex: 8] substringToIndex: 6];

shows how the two methods can be combined to extract a substring of characters from inside a string. The substringFromIndex: method is first used to extract characters from index number 8 through the end of the string; then substringToIndex: is applied to the result to get the first six characters. The net result is a substring representing the range of characters {8, 6} from the original string.

The substringWithRange: method does in one step what we just did in two: It takes a range and returns a character in the specified range. The special function

NSMakeRange (8, 6)

creates a range from its argument and returns the result. This is given as the argument to the substringWithRange: method.

To locate one string inside another, you can use the rangeOfString: method. If the specified string is found inside the receiver, the returned range specifies precisely where in the string it was found. If, however, the string is not found, the range that is returned has its location member set to NSNotFound.

So, the statement

subRange = [str1 rangeOfString: @"string A"];

assigns the NSRange structure returned by the method to the NSRange variable subRange. Be sure to note that subRange is not an object variable, but a structure variable (the declaration for subRange in the program also does not contain an asterisk). Its members can be retrieved by using the structure member operator dot (.). So, the expression subRange.location gives the value of the location member of the structure and subRange.length gives the length member. These values are passed to the printf function to be displayed.

Mutable Strings

The NSMutableString class can be used to create string objects whose characters can be changed. Because this class is a subclass of NSString, all NSString's methods can be used as well.

When we speak of mutable versus immutable string objects, we talk about changing the actual characters within the string. Either a mutable or an immutable string object can always be set to a completely different string object during execution of the program. For example, consider the following:

str1 = @"This is a string";
   ...
str1 = [str1 stringFromIndex: 5];

In this case, str1 is first set to a constant character string object. Later in the program, it is set to a substring. In such a case, str1 can be declared as either a mutable or an immutable string object. Be sure you understand this point.

Program 15.6 shows some ways to work with mutable strings in your programs.

Program 15.6.


// Basic String Operations - Mutable Strings

#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
  NSString            *str1 = @"This is string A";
  NSString            *search, *replace;
  NSMutableString     *mstr;
  NSRange             substr;

  // Create mutable string from immutable

  mstr = [NSMutableString stringWithString: str1];
  printf ("%s ", [mstr cString]);

  // Insert characters starting at a specific index

  [mstr insertString: @" mutable" atIndex: 7];
  printf ("%s ", [mstr cString]);

  // Effective concatentation if insert at end

  [mstr insertString: @" and string B" atIndex: [mstr length]];
  printf ("%s ", [mstr cString]);

  // Or can use appendString directly

  [mstr appendString: @" and string C"];
  printf ("%s ", [mstr cString]);

    // Delete substring based on range

    [mstr deleteCharactersInRange: NSMakeRange (16, 13)];
    printf ("%s ", [mstr cString]);

    // Find range first and then use it for deletion

    substr = [mstr rangeOfString: @"string B and "];

    if (substr.location != NSNotFound) {
         [mstr deleteCharactersInRange: substr];
         printf ("%s ", [mstr cString]);
    }

    // Set the mutable string directly

    [mstr setString: @"This is string A"];
    printf ("%s ", [mstr cString]);

    // Now let's replace a range of chars with another

    [mstr replaceCharactersInRange: NSMakeRange(8, 8)
          withString: @"a mutable string"];
    printf ("%s ", [mstr cString]);

    // Search and replace

    search = @"This is";
    replace = @"An example of";

    substr = [mstr rangeOfString: search];

    if (substr.location != NSNotFound) {
          [mstr replaceCharactersInRange: substr
                withString: replace];
           printf ("%s ", [mstr cString]);
    }

    // Search and replace all occurrences

    search = @"a";
    replace = @"X";

    substr = [mstr rangeOfString: search];

    while (substr.location != NSNotFound) {
          [mstr replaceCharactersInRange: substr
                withString: replace];
           substr = [mstr rangeOfString: search];
    }

    printf ("%s ", [mstr cString]);

    [pool release];
    return 0;
}


Program 15.6. Output


This is string A
This is mutable string A
This is mutable string A and string B
This is mutable string A and string B and string C
This is mutable string B and string C
This is mutable string C
This is string A
This is a mutable string
An example of a mutable string
An exXmple of X mutXble string


The declaration

NSMutableString *mstr;

declares mstr to be a variable that will hold a character string object whose contents might change during execution of the program. The line

mstr = [NSMutableString stringWithString: str1];

sets mstr to the string object whose contents are a copy of the characters in str1, or "This is string A". When the stringWithString: method is sent to the NSMutableString class, a mutable string object is returned. When it's sent to the NSString class, as you did in Program 15.5, you get an immutable string object instead.

The insertString:atIndex: method inserts the specified character string into the receiver beginning at the specified index number. In this case, you insert the string @" mutable" into the string beginning at index number 7, or in front of the eighth character in the string. Unlike the immutable string object methods, no value is returned here because the receiver is modified—you can do that because it's a mutable string object.

The second insertString:atIndex: invocation uses the length method to insert one character string at the end of another. The appendString: method makes this task a little simpler.

By using the deleteCharactersInRange: method, you can remove a specified number of characters from a string. The range {16, 13} , when applied to the string

This is mutable string A and string B and string C

deletes the 13 characters "string A and " beginning with index number 16 (or the 17th character in the string). This is depicted in Figure 15.1.

This is mutable string A and string B and string C

Figure 15.1. Indexing into a string.

image

The rangeOfString: method is used in the lines that follow in Program 15.6 to show how a string can first be located and then deleted. After first verifying that the string @"string B and" does in fact exist in mstr, the deleteCharactersInRange: method is then used to delete the characters using the range returned from the rangeOfString: method as its argument.

The setString: method can be used to set the contents of a mutable string object directly. After using this method to set mstr to the string @"This is string A", the replaceCharactersInRange: method replaces some of the characters in the string with another string. The size of the strings do not have to be the same; you can replace one string with another of equal or unequal sizes. So, in the statement

[mstr replaceCharactersInRange: NSMakeRange(8, 8)
                    withString: @"a mutable string"];

the 8 characters "string A" are replaced with the 16 characters "a mutable string".

The remaining lines in the program example show how to perform a search and replace. First, you locate the string @“This is” inside the string mstr, which contains @“This is a mutable string”. If the search is successful (which it is), you replace the matched string with the replacement string, which in this case is @“An example of”.

The program next sets up a loop to illustrate how to implement a search-and-replace-all operation. The search string is set to @“a” and the replacement string is set to @“X”. You want to replace all occurrences of the letter a with the letter X. The while loop continues executing as long as you continue to find the search string inside mstr. The output verifies that all as were in fact replaced by Xs.

If the replacement string also contains the search string (for example, consider replacing the string “a” with the string “aX”), you would end up with an infinite loop.

Second, if the replacement string is empty (that is, contains no characters), you will effectively delete all occurrences of the search string. An empty constant character string object is specified by an adjacent pair of quotation marks, with no intervening spaces, like so:

replace = @"";

Of course, if you just wanted to delete an occurrence of a string, you could use the deleteCharactersInRange: method instead, as you've already seen.

Finally, the NSString class also contains a method called replaceOccurrencesOfString:withString:options:range: that can be used to do a search-and-replace-all on a string. In fact, the while loop from Program 15.6 could have been replaced with this single statement:

[mstr replaceOccurrencesOfString: search
                      withString: replace
                         options: nil
                           range: NSMakeRange (0, [mstr length])];

This would achieve the same result and avert the potential of setting up an infinite loop because the method prevents such a thing from happening.

Where Are All Those Objects Going?

Programs 15.5 and 15.6 deal with many string objects that are created and returned by various NSString and NSMutableString methods. As discussed at the beginning of this chapter, you are not responsible for releasing the memory used by these objects; the objects' creators are. Presumably, all these objects have been added to the autorelease pool by their creators and will be freed when the pool is released. However, you should be aware that if you are developing a program that creates a lot of temporary objects, the memory used by these objects can accumulate. In such cases, you might need to adopt different strategies that allow for memory to be released during your program's execution, and not just at the end. This concept is described in Chapter 17. For now, just realize that these objects do take up memory that can expand as your program executes.

The NSString class contains more than 100 methods that can be used to work with immutable string objects. Table 15.2 summarizes some of the more commonly used ones, and Table 15.3 lists some of the additional methods provided by the NSMutableString class. Some other NSString methods (such as working with pathnames and reading the contents of a file into a string) will be introduced to you throughout the remainder of this book.

In Tables 15.2 and 15.3, cstring refers to a C-style character string, url is an NSURL object, path is an NSString object specifying the path to a file, nsstring is an NSString object, i is an integer representing a valid character number in a string, size and opts are unsigned integers, and range is an NSRange object indicating a valid range of characters within a string.

Table 15.2. Common NSString Methods

image

image

The following either create or modify NSMutableString objects.

Table 15.3. Common NSMutableString Methods

image

NSString objects are used extensively throughout the remainder of this text. If you need to parse strings into tokens, you can take a look at Foundation's NSScanner class.

Array Objects

A Foundation array is an ordered collection of objects. Most often, elements in an array are of one particular type, but that's not required. Just as there are mutable and immutable strings, so too are there mutable and immutable arrays. Immutable arrays are handled by the NSArray class, whereas mutable ones are handled by NSMutableArray. The latter is a subclass of the former, which means it inherits its methods. To work with array objects in your programs, include the following line:

#import <Foundation/NSArray.h>

Program 15.7 sets up an array to store the names of the months of the year and then prints them.

Program 15.7.


#import <Foundation/NSObject.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSString.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{

  NSAutoreleasePool  *pool = [[NSAutoreleasePool alloc] init];

  // Create an array to contain the month names

  NSArray *monthNames = [NSArray arrayWithObjects:
    @"January", @"February", @"March", @"April",
    @"May", @"June", @"July", @"August", @"September",
    @"October", @"November", @"December", nil ];

  int   i;

  // Now list all the elements in the array

  printf ("Month  Name =====  ==== ");

  for (i = 0; i < 12; ++i)
         printf (" %2i   %s ", i + 1,
                [[monthNames objectAtIndex: i] cString]);

  [pool release];
  return 0;
}


Program 15.7. Output


Month   Name
=====   ====
  1     January
  2     February
  3     March
  4     April
  5     May
  6     June
  7     July
  8     August
  9     September
10     October
11     November
12     December


The class method arrayWithObjects: can be used to create an array with a list of objects as its elements. In such a case, the objects are listed in order and are separated by commas. This is a special syntax used by methods that can take a variable number of arguments. To mark the end of the list, nil must be specified as the last value in the list—it isn't actually stored inside the array.

In Program 15.7 monthNames is set to the 12 string values specified by the arguments to arrayWithObjects:.

Elements are identified in an array by their index numbers. Similar to NSString objects, indexing begins with zero. So, an array containing 12 elements would have valid index numbers 0–11. To retrieve an element of an array using its index number, you use the objectAtIndex: method.

The program simply executes a for loop to extract each element from the array using the objectAtIndex: method. Each retrieved element is converted to a C string and then displayed with printf.

Program 15.8 generates a table of prime numbers. Because you are going to be adding prime numbers to your array as they are generated, a mutable array is required. The NSMutableArray primes is allocated using the arrayWithCapacity: method. The argument of 20 that you give specifies the initial capacity of the array; a mutable array's capacity automatically is increased as necessary while the program is running.

Even though prime numbers are integers, you can't directly store int values inside your array. Your array can hold only objects. So, you need to store NSNumber integer objects inside your primes array.

Program 15.8.


// Generate a table of prime numbers
#import <Foundation/NSObject.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSString.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSValue.h>

#define kMaxPrime  50

int main (int argc, char *argv[])
{
  int   i, p, n, prevPrime;
  BOOL  isPrime;
  NSAutoreleasePool  *pool = [[NSAutoreleasePool alloc] init];

  // Create an array to store the prime numbers

  NSMutableArray *primes =
        [NSMutableArray arrayWithCapacity: 20];

  // Store the first two primes (2 and 3) into the array

  [primes addObject: [NSNumber numberWithInt: 2]];
  [primes addObject: [NSNumber numberWithInt: 3]];

  // Calculate the remaining primes

  for (p = 5; p <= kMaxPrime; p += 2) {
  // we're testing to see if p is prime

         isPrime = YES;

         i = 1;

         do {
           prevPrime = [[primes objectAtIndex: i] intValue];

           if (p % prevPrime == 0)
         isPrime = NO;

           ++i;
         } while ( isPrime == YES && p / prevPrime >= prevPrime);

         if (isPrime)
               [primes addObject: [NSNumber numberWithInt: p]];
  }


  // Display the results
  n = [primes count];

  for (i = 0; i < n; ++i)
       printf ("%i ", [[primes objectAtIndex: i] intValue]);

  printf (" ");

  [pool release];
  return 0;
}


Program 15.8. Output


2 3 5 7 11 13 17 19 23 29 31 37 41 43 47


You define kMaxPrime to the maximum prime number you want the program to calculate, which in this case is 50.

After allocating your primes array, you set the first two elements of the array using these statements:

[primes addObject: [NSNumber numberWithInt: 2]];
[primes addObject: [NSNumber numberWithInt: 3]];

The addObject: method adds an object to the end of an array. Here you add the NSNumber objects created from the integer values 2 and 3, respectively.

The program then enters a for loop to find prime numbers starting with 5, going up to kMaxPrime and skipping the even numbers in between (p += 2).

For each possible prime candidate p, you want to see whether it is evenly divisible by the previously discovered primes. If it is, it's not prime. As an added optimization, you test the candidate for even division only by earlier primes up to its square root. That's because, if a number is not prime, it must be divisible by a prime number that is less than or equal to its square root (ahh, back to high school math again!). So, the expression

p / prevPrime >= prevPrime

remains true only as long as prevPrime is less than the square root of p.

If the do-while loop exits with the flag isPrime still equal to YES, you have found another prime number. In that case, the candidate p is added to the primes array and execution continues.

Just a comment about program efficiency here. The Foundation classes for working with arrays provide many conveniences. However, in the case of manipulating large arrays of numbers with complex algorithms, learning how to perform such a task using the lower-level array constructs provided by the language might be more efficient, both in terms of memory usage and execution speed. Refer to the section titled “Arrays” in Chapter 13 for more information.

Making an Address Book

Let's take a look at an example that starts to combine a lot of what you've learned to this point by creating an address book.6 Your address book will contain address cards. For the sake of simplicity, your address cards will contain only a person's name and email address. It is straightforward to extend this concept to other information, such as address and phone number, but that's left as an exercise for you at the end of this chapter.

Creating an Address Card

You're going to start by defining a new class called AddressCard. You'll want the ability to create a new address card, set its name and email fields, retrieve those fields, and print the card. In a graphics environment, you could use some nice routines such as those provided by the Application Kit framework to draw your card onscreen. But here you're going to stick to a simple terminal interface to display your address cards.

Program 15.9 shows the interface file for your new AddressCard class.

Program 15.9. Interface File AddressCard.h


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>

@interface AddressCard: NSObject
{
  NSString  *name;
  NSString  *email;
}


-(void) setName: (NSString *) theName;
-(void) setEmail: (NSString *) theEmail;

-(NSString *) name;
-(NSString *) email;

-(void) print;

@end


This is straightforward, as is the implementation file in Program 15.9.

Program 15.9. Implementation File AddressCard.m


#import "AddressCard.h"

@implementation AddressCard;

-(void) setName: (NSString *) theName
{
   name = [[NSString alloc] initWithString: theName];
}

-(void) setEmail: (NSString *) theEmail
{
   email = [[NSString alloc] initWithString: theEmail];
}


-(NSString *) name
{
  return name;
}


-(NSString *) email
{
  return email;
}

-(void) print
{
    printf ("==================================== ");
    printf ("|                                  | ");
    printf ("|  %-31s | ", [name cString]);
    printf ("|  %-31s | ", [email cString]);
    printf ("|                                  | ");
    printf ("|                                  | ");
    printf ("|                                  | ");
    printf ("|       O                  O       | ");
    printf ("==================================== ");

}
@end


You could have the setName: and setEmail: methods store the objects directly in their respective instance variables with method definitions like these:

-(void) setName: (NSString *) theName
{
  name = theName;
}

-(void) setEmail: (NSString *) theEmail
{
  email = theEmail;
}

But the AddressCard object would not own its member objects. We talked about the motivation for an object to take ownership with respect to the Rectangle class owning its origin object in Chapter 8, “Inheritance.”

Defining the two methods this way:

-(void) setName: (NSString *) theName
{
  name = [NSString stringWithString: theName];
}

-(void) setEmail: (NSString *) theEmail
{
  email = [NSString stringWithString: theEmail];
}

would also be the incorrect approach because the AddressCard methods would still not own their name and email objects—NSString would own them.

Returning to Program 15.9, the print method tries to present a nice display of an address card to the user in a format resembling a Rolodex card (remember those?). The %-31s characters to printf indicate to print a C-string within a field width of 31 characters, left-justified. That ensures the right edges of your address card line up in the output.

With your AddressCard class in hand, you can write a test program to create an address card, set its values, and display it (see Program 15.9).

Program 15.9. Test Program


#import "AddressCard.h"
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSString   *aName = @"Julia Kochan";
  NSString   *aEmail = @"[email protected]";
  AddressCard  *card1 = [[AddressCard alloc] init];

  [card1 setName: aName];
  [card1 setEmail: aEmail];

  [card1 print];

  [card1 release];
  [pool release];
return 0;
}


Program 15.9. Output


====================================
|                                  |
| Julia Kochan                     |
| [email protected]                |
|                                  |
|                                  |
|                                  |
|       O                O         |
====================================


Releasing Objects: release and dealloc

The line

[card1 release];

is used in Program 15.9 to release the memory used by your address card. You should realize from previous discussions that releasing an AddressCard object this way does not also release the memory you allocated for its name and email members. To make the AddressCard leak-free, you need to override a method called dealloc to release these members whenever the memory for an AddressCard object is released.

Whereas in Part I you saw how to override the free method to release the space used by objects contained in an object being freed, with NSObject you override dealloc instead. The dealloc method is called whenever the memory used by an object is to be returned to the system, or deallocated. It differs from the release method in a subtle but important way: The release method does not necessarily deallocate the memory used by an object, whereas dealloc does. In fact, when release is ready to free an object's memory, it does so with dealloc. This will become clear in Chapter 17 when we talk about retaining objects. Here is the dealloc method for your AddressCard class:

-(void) dealloc
{
  [name release];
  [email release];
  [super dealloc];
}

The dealloc method must release its own instance variables before using super to destroy the object itself. That's because an object is no longer valid after it has been deallocated.

To make your AddressCard leak-free, you must also modify your setName: and setEmail: methods to release the memory used by the objects stored in their respective instance variables. If someone changes the name on a card, you need to release the memory taken up by the old name before replacing it with the new one. Similarly for the email address, you need to release the memory used by the old email address before replacing it with the new one.

Here are the new setName: and setEmail: methods that will ensure we have a class that handles memory management properly:

-(void) setName: (NSString *) theName
{
  [name release];
  name = [[NSString alloc] initWithString: theName];
}

-(void) setEmail: (NSString *) theEmail
{
  [email release];
  email = [[NSString alloc] initWithString: theEmail];
}

You can send a message to a nil object; therefore, the message expressions

[name release];

and

[email release];

are okay even if name or email have not been previously set.

Let's add another method to your AddressCard class. You might want to set both the name and email fields of your card with one call. To do so, add a new method, setName:andEmail:.7 Here's what the new method looks like:

-(void) setName: (NSString *) theName andEmail: (NSString *) theEmail
{
  [self setName: theName];
  [self setEmail: theEmail];
}

By relying on the setName: and setEmail: methods to set the appropriate instance variables (instead of setting them directly inside the method yourself), you add a level of abstraction and therefore make the program slightly more independent of its internal data structures.

Program 15.10 tests your new method.

Program 15.10. Test Program


#import<Foundations/NSAutoreleasePool.h>
#import "AddressCard.h"

int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  NSString  *aName = @"Julia Kochan";
  NSString  *aEmail = @"[email protected]";
  NSString  *bName = @"Tony Iannino";
  NSString  *bEmail = @"[email protected]";

  AddressCard   *card1 = [[AddressCard alloc] init];
  AddressCard   *card2 = [[AddressCard alloc] init];

  [card1 setName: aName andEmail: aEmail];
  [card2 setName: bName andEmail: bEmail];

  [card1 print];
  [card2 print];
  [card1 release];
  [card2 release];
  [pool release];
  return 0;
}


Program 15.10. Output


====================================
|                                  |
| Julia Kochan                     |
| [email protected]                |
|                                  |
|                                  |
|                                  |
|       O           O              |
====================================
====================================
|                                  |
| Tony Iannino                     |
| [email protected]     |
|                                  |
|                                  |
|                                  |
|       O           O              |
====================================


Your AddressCard class seems to be working okay. What if you wanted to work with a lot of AddressCards? It would make sense to collect them together, which is exactly what you'll do by defining a new class called AddressBook. The AddressBook class will store the name of an address book and a collection of AddressCards, which you'll store in an array object. To start with, you'll want the ability to create a new address book, add new address cards to it, find out how many entries are in it, and list its contents. Later, you'll want to be able to search the address book, remove entries, possibly edit existing entries, sort it, or even make a copy of its contents.

Let's get started with a simple interface file (see Program 15.11).

Program 15.11. Addressbook.h Interface File


#import <Foundation/NSArray.h>
#import "AddressCard.h"

@interface AddressBook: NSObject
{
  NSString        *bookName;
  NSMutableArray  *book;
}

-(AddressBook *) initWithName: (NSString *) name;
-(void) addCard: (AddressCard *) theCard;
-(int) entries;
-(void) list;
-(void) dealloc;

@end


The initWithName: method sets up the initial array to hold the address cards and store the name of the book, whereas the addCard: method adds an AddressCard to the book. The entries method reports the number of address cards in your book, and the list method gives a concise listing of its entire contents. The implementation file for your AddressBook class is shown in Program 15.11.

Program 15.11. Addressbook.m Implementation File


#import "AddressBook.h"

@implementation AddressBook;

// set up the AddressBook's name and an empty book

-(id) initWithName: (NSString *) name
{
  self = [super init];

  if (self) {
    bookName = [[NSString alloc] initWithString: name];
    book = [[NSMutableArray alloc] init];
    }

  return self;
}

-(void) addCard: (AddressCard *) theCard
{
  [book addObject: theCard];
}

-(int) entries
{
  return [book count];
}

-(void) list
{
  int     i, elements;
  AddressCard *theCard;

  printf (" ======== Contents of: %s ========= ",
           [bookName cString]);
  elements = [book count];

  for ( i = 0; i < elements; ++i ) {
      theCard = [book objectAtIndex: i];
      printf ("%-20s  %-32s ", [[theCard name] cString],
                    [[theCard email] cString]);
  }

  printf("========================================
          ============ ");
}

-(void) dealloc
{
  [bookName release];
  [book release];
  [super dealloc];
}
@end


The initWithName: method first calls the init method for the superclass to perform its initialization. Next, it creates a string object (using alloc so it owns it) and sets it to the name of the address book passed in as name. This is followed by the allocation and initialization of an empty mutable array that is stored in the instance variable book.

You defined initWithName: to return an id object, instead of an AddressBook one. If AddressBook is subclassed, the argument to initWithName: isn't an AddressBook object; its type is that of the subclass. For that reason, you define the return type as a generic object type.

Notice also that in initWithName:, you take ownership of the bookName and book instance variables by using alloc. For example, if you created the array for book using NSMutableArray's array method, as in

book = [NSMutableArray array];

you would still not be the owner of the book array; NSMutableArray would own it. Thus, you wouldn't be able to release its memory when you freed up the memory for an AddressBook object.

The addCard: method takes the AddessCard object given as its argument and adds it to the address book.

The count method gives the number of elements in an array. This is used by the entries method to return the number of address cards stored in the address book.

Finally, the list method goes through each entry in the address book and displays the name and email fields at the terminal.

Following is a test program for your new AddressBook class.

Program 15.11. Test Program


#import "AddressBook.h"
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  NSString  *aName = @"Julia Kochan";
  NSString  *aEmail = @"[email protected]";
  NSString  *bName = @"Tony Iannino";
  NSString  *bEmail = @"[email protected]";
  NSString  *cName = @"Stephen Kochan";
  NSString  *cEmail = @"[email protected]";
  NSString  *dName = @"Jamie Baker";
  NSString  *dEmail = @"[email protected]";

  AddressCard *card1 = [[AddressCard alloc] init];
  AddressCard *card2 = [[AddressCard alloc] init];
  AddressCard *card3 = [[AddressCard alloc] init];
  AddressCard *card4 = [[AddressCard alloc] init];

  AddressBook  *myBook = [AddressBook alloc];

  // First set up four address cards

  [card1 setName: aName andEmail: aEmail];
  [card2 setName: bName andEmail: bEmail];
  [card3 setName: cName andEmail: cEmail];
  [card4 setName: dName andEmail: dEmail];

  // Now initialize the address book

  myBook = [myBook initWithName: @"Linda's Address Book"];

  printf ("Entries in address book after creation: %i ",
             [myBook entries]);

  // Add some cards to the address book

  [myBook addCard: card1];
  [myBook addCard: card2];
  [myBook addCard: card3];
  [myBook addCard: card4];

  printf ("Entries in address book after adding cards: %i ",
             [myBook entries]);

  // List all the entries in the book now

  [myBook list];

  [card1 release];
  [card2 release];
  [card3 release];
  [card4 release];
  [myBook release];
  [pool release];
  return 0;
}


Program 15.11. Output


Entries in address book after creation: 0
Entries in address book after adding cards: 4

======== Contents of: Linda's Address Book =========
Julia Kochan      [email protected]
Tony Iannino      [email protected]
Stephen Kochan    [email protected]
Jamie Baker       [email protected]
====================================================


The program sets up four address cards and then creates a new address book called “Linda's Address Book.” The four cards are then added to the address book using the addCard: method, and the list method is used to list the contents of the address book and verify its contents.

Looking Up Someone in the Address Book

When you have a large address book, you won't want to list its complete contents each time you want to look up someone. It therefore makes sense to add a method to do that for you. Let's call the method lookup: and have it take as its argument the name to locate. The method will search the address book for a match (ignoring case) and return the matching entry if found. If the name does not appear in the phone book, you'll have it return nil.

Here's the new lookup: method:

// lookup address card by name -- assumes an exact match

-(AddressCard *) lookup: (NSString *) theName
{
    AddressCard *nextCard;

    int   i, elements;
    elements = [book count];

    for ( i = 0; i < elements; ++i) {
            nextCard = [book objectAtIndex: i];

        if ( [[nextCard name] caseInsensitiveCompare: theName]
               == NSOrderedSame )
                    return nextCard;
    }

    return nil;
}

If you put the declaration for this method in your interface file and the definition in the implementation file, you can write a test program to try your new method. Program 15.12 shows such a program, followed immediately by its output.

Program 15.12. Test Program


#import "AddressBook.h"
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  NSString  *aName = @"Julia Kochan";
  NSString  *aEmail = @"[email protected]";
  NSString  *bName = @"Tony Iannino";
  NSString  *bEmail = @"[email protected]";
  NSString  *cName = @"Stephen Kochan";
  NSString  *cEmail = @"[email protected]";
  NSString  *dName = @"Jamie Baker";
  NSString  *dEmail = @"[email protected]";
  AddressCard   *card1 = [[AddressCard alloc] init];
  AddressCard   *card2 = [[AddressCard alloc] init];
  AddressCard   *card3 = [[AddressCard alloc] init];
  AddressCard   *card4 = [[AddressCard alloc] init];

  AddressBook  *myBook = [AddressBook alloc];
  AddressCard  *myCard;

  // First set up four address cards

  [card1 setName: aName andEmail: aEmail];
  [card2 setName: bName andEmail: bEmail];
  [card3 setName: cName andEmail: cEmail];
  [card4 setName: dName andEmail: dEmail];

  myBook = [myBook initWithName: @"Linda's Address Book"];

  // Add some cards to the address book

  [myBook addCard: card1];
  [myBook addCard: card2];
  [myBook addCard: card3];
  [myBook addCard: card4];

  // Look up a person by name

  printf ("Lookup: Stephen Kochan ");
  myCard = [myBook lookup: @"stephen kochan"];

  if (myCard != nil)
        [myCard print];
  else
        printf ("Not found! ");

  // Try another lookup

  printf (" Lookup: Wes Rosenberg ");
  myCard = [myBook lookup: @"Wes Rosenberg"];

  if (myCard != nil)
        [myCard print];
  else
        printf ("Not found! ");

  [card1 release];
  [card2 release];
  [card3 release];
  [card4 release];
  [myBook release];

  [pool release];
  return 0;
}


Program 15.12. Output


Lookup: Stephen Kochan
====================================
|                                  |
| Stephen Kochan                   |
| [email protected]            |
|                                  |
|                                  |
|                                  |
|       O           O              |
====================================

Lookup: Wes Rosenberg
Not found!


When Stephen Kochan was located in the address book (taking advantage of the fact that a case-insensitive match was made) by the lookup: method, the resulting address card that was returned was given to the AddressCard's print method for display. In the case of the second lookup, the name Wes Rosenberg was not found, thus explaining the resulting message.

This lookup message is very primitive because it needs to find an exact match of the entire name. A better method would perform partial matches and be able to handle multiple matches as well. For example, the message expression

[myBook lookup: @"steve"]

could match entries for “Steve Kochan”, “Fred Stevens”, and “steven levy”. Because multiple matches would exist, a good approach might be to create an array containing all the matches and return the array to the method caller (see exercise 2 at the end of this chapter), like so:

matches = [myBook lookup: @"steve"];

Removing Someone from the Address Book

No address book manager that enables you to add an entry would be complete without the capability to also remove one. You can make a removeCard: method to remove a particular AddressCard from the address book. Another possibility would be to create a remove: method that removes someone based on her name (see exercise 6 at the end of this chapter).

Because you've made a couple of changes to your interface file, Program 15.13 shows it again with the new removeCard: method. It's followed by your new removeCard: method.

Program 15.13. Addressbook.h Interface File


#import <Foundation/NSArray.h>
#import "AddressCard.h"

@interface AddressBook: NSObject
{
  NSString        *bookName;
  NSMutableArray  *book;
}

-(AddressBook *) initWithName: (NSString *) name;

-(void) addCard: (AddressCard *) theCard;
-(void) removeCard: (AddressCard *) theCard;

-(AddressCard *) lookup: (NSString *) theName;
-(int) entries;
-(void) list;

@end


Here's the new removeCard method:

-(void) removeCard: (AddressCard *) theCard
{
  [book removeObjectIdenticalTo: theCard];
}

For purposes of what's considered an identical object, we are using the idea of the same location in memory. So, two address cards that contain the same information, but which are located in different places in memory (which might happen if you made a copy of an AddressCard, for example), would not be considered identical by the removeObjectIdenticalTo: method.

Incidentally, the removeObjectIdenticalTo: method removes all objects identical to its argument. However, that's only an issue if you have multiple occurrences of the same object in your arrays.

You can get more sophisticated with your approach to equal objects by using the removeObject: method and then writing your own isEqual: method for testing whether two objects are equal. If you use removeObject:, the system automatically invokes the isEqual: method for each element in the array, giving it the two elements to compare. In this case, because your address book contains AddressCard objects as its elements, you would have to add an isEqual: method to that class (you would be overriding the method that the class inherits from NSObject). The method could then decide for itself how to determine equality. It would make sense to compare the two corresponding names and emails, and if they were both equal, you could return YES from the method. Otherwise, you could return NO. Your method might look like this:

-(BOOL) isEqual (AddressCard *) theCard
{
  if ([name isEqualToString: [theCard name]] == YES &&
         [email isEqualToString: [theCard email]] == YES)
    return YES;
  else
    return NO;
}

You should note that other NSArray methods, such as containsObject: and indexOfObject:, also rely on this isEqual: strategy for determining whether two objects are considered equal.

Program 15.14 tests the new removeCard: method.

Program 15.14.


#import "AddressBook.h"
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  NSString  *aName = @"Julia Kochan";
  NSString  *aEmail = @"[email protected]";
  NSString  *bName = @"Tony Iannino";
  NSString  *bEmail = @"[email protected]";
  NSString  *cName = @"Stephen Kochan";
  NSString  *cEmail = @"[email protected]";
  NSString  *dName = @"Jamie Baker";
  NSString  *dEmail = @"[email protected]";

  AddressCard *card1 = [[AddressCard alloc] init];
  AddressCard *card2 = [[AddressCard alloc] init];
  AddressCard *card3 = [[AddressCard alloc] init];
  AddressCard *card4 = [[AddressCard alloc] init];

  AddressBook  *myBook = [AddressBook alloc];
  AddressCard  *myCard

  // First set up four address cards

  [card1 setName: aName andEmail: aEmail];
  [card2 setName: bName andEmail: bEmail];
  [card3 setName: cName andEmail: cEmail];
  [card4 setName: dName andEmail: dEmail];

  myBook = [myBook initWithName: @"Linda's Address Book"];

  // Add some cards to the address book

  [myBook addCard: card1];
  [myBook addCard: card2];
  [myBook addCard: card3];
  [myBook addCard: card4];

  // Look up a person by name

  printf ("Lookup: Stephen Kochan ");
  myCard = [myBook lookup: @"Stephen Kochan"];

  if (myCard != nil)
         [myCard print];
  else
         printf ("Not found! ");

  // Now remove the entry from the phone book

  [myBook removeCard: myCard];
  [myBook list];    // verify it's gone

  [card1 release];
  [card2 release];
  [card3 release];
  [card4 release];
  [myBook release];
  [pool release];

  return 0;
}


Program 15.14. Output


Lookup: Stephen Kochan
====================================
|                                  |
| Stephen Kochan                   |
| [email protected]            |
|                                  |
|                                  |
|                                  |
|       O           O              |
====================================

======== Contents of: Linda's Address Book =========
Julia Kochan      [email protected]
Tony Iannino      [email protected]
Jamie Baker       [email protected]
====================================================


After looking up Stephen Kochan in the address book and verifying he's there, you pass the resulting AddressCard to your new removeCard: method to be removed. The resulting listing of the address book verifies the removal.

Sorting Arrays

If you end up with a lot of entries in your address book, alphabetizing it might be convenient. You can easily do this by adding a sort method to your AddressBook class and by taking advantage of an NSMutableArray method called sortUsingSelector:. This method takes as its argument a selector that is used by the sortUsingSelector: method to compare two elements. Arrays can contain any type of objects in them, so the only way to implement a generic sorting method is to have you decide whether elements in the array are in order. To do this, you have to add a method that will be used to compare two elements in the array.8 The result returned from that method is to be of type NSComparisonResult, and it should return NSOrderedAscending if you want the sorting method to place the first element before the second in the array, return NSOrderedSame if the two elements are considered equal, and return NSOrderedDescending if the first element should come after the second element in the sorted array.

First, here's the new sort method from your AddressBook class:

-(void) sort
{
    [book sortUsingSelector: @selector(compareNames:)];
}

As you learned in Chapter 9, “Polymorphism, Dynamic Typing, and Dynamic Binding,” the expression

@selector (compareNames:)

creates a selector, which is of type SEL, from a specified method name; this is the method used by sortUsingSelector: to compare two elements in the array. When it needs to make such a comparison, it invokes the specified method, sending the message to the first element in the array (the receiver) to be compared against its argument. The returned value should be of type NSComparisonResult, as previously described.

Because the elements of your address book are AddressCard objects, the comparison method must be added to the AddressCard class. So, you have to go back to your AddressCard class and add a compareNames: method to it. This is shown here:

// Compare the two names from the specified address cards
-(NSComparisonResult) compareNames: (id) element
{
       return [name compare: [element name]];
}

Because you are doing a string comparison of the two names from the address book, you can use the NSString compare: method to do the work for you.

If you add the sort method to the AddressBook class and the compareNames: method to the AddressCard class, you can write a test program to test it (see Program 15.15).

Program 15.15. Test Program


#import "AddressBook.h"
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  NSString  *aName = @"Julia Kochan";
  NSString  *aEmail = @"[email protected]";
  NSString  *bName = @"Tony Iannino";
  NSString  *bEmail = @"[email protected]";
  NSString  *cName = @"Stephen Kochan";
  NSString  *cEmail = @"[email protected]";
  NSString  *dName = @"Jamie Baker";
  NSString  *dEmail = @"[email protected]";

  AddressCard  *card1 = [[AddressCard alloc] init];
  AddressCard  *card2 = [[AddressCard alloc] init];
  AddressCard  *card3 = [[AddressCard alloc] init];
  AddressCard  *card4 = [[AddressCard alloc] init];

  AddressBook   *myBook = [AddressBook alloc];

  // First set up four address cards

  [card1 setName: aName andEmail: aEmail];
  [card2 setName: bName andEmail: bEmail];
  [card3 setName: cName andEmail: cEmail];
  [card4 setName: dName andEmail: dEmail];

  myBook = [myBook initWithName: @"Linda's Address Book"];

  // Add some cards to the address book

  [myBook addCard: card1];
  [myBook addCard: card2];
  [myBook addCard: card3];
  [myBook addCard: card4];

  // List the unsorted book

  [myBook list];

  // Sort it and list it again

  [myBook sort];
  [myBook list];

  [card1 release];
  [card2 release];
  [card3 release];
  [card4 release];
  [myBook release];
  [pool release];
  return 0;
}


Program 15.15. Output


======== Contents of: Linda's Address Book =========
Julia Kochan      [email protected]
Tony Iannino      [email protected]
Stephen Kochan    [email protected]
Jamie Baker       [email protected]
====================================================


======== Contents of: Linda's Address Book =========
Jamie Baker       [email protected]
Julia Kochan      [email protected]
Stephen Kochan    [email protected]
Tony Iannino      [email protected]
====================================================


You should note that the sort is an ascending one. However, you can easily perform a descending sort by modifying the compareNames: method in the AddressCard class to reverse the sense of the values that are returned.

More than 60 methods are available for working with array objects. Tables 15.4 and 15.5 list some commonly used methods for working with immutable and mutable arrays, respectively. Because NSMutableArray is a subclass of NSArray, the former inherits the methods of the latter.

In Tables 15.4 and 15.5, obj, obj1, and obj2 are any objects; i is an integer representing a valid index number into the array; selector is a selector object of type SEL; and size is an unsigned integer.

Table 15.4. Common NSArray Methods

image

Table 15.5. Common NSMutableArray Methods

image

Dictionary Objects

A dictionary is a collection of data consisting of key-object pairs. Just as you would look up the definition of a word in a dictionary, you obtain the value (object) from an Objective-C dictionary by its key. The keys in a dictionary must be unique, and they can be of any object type, although they are typically strings. The value associated with the key can also be of any object type, but it cannot be nil.

Dictionaries can be mutable or immutable; mutable ones can have entries dynamically added and removed. Dictionaries can be searched based on a particular key, and their contents can be enumerated. Program 15.16 sets up a mutable dictionary to be used as a glossary of Objective-C terms and fills in the first three entries.

To use dictionaries in your programs, include the following line:

#import <Foundation/NSDictionary.h>

Program 15.16.


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool  *pool = [[NSAutoreleasePool alloc] init];

  NSMutableDictionary *glossary = [NSMutableDictionary dictionary];
  // Store three entries in the glossary

  [glossary setObject:
       @"A class defined so other classes can inherit from it"
        forKey: @"abstract class" ];
  [glossary setObject:
       @"To implement all the methods defined in a protocol"
        forKey: @"adopt"];
  [glossary setObject:
       @"Storing an object for later use"
        forKey: @"archiving"];

  // Retrieve and display them

  printf ("abstract class: %s ",
    [[glossary objectForKey: @"abstract class"] cString]);
  printf ("adopt: %s ",
    [[glossary objectForKey: @"adopt"] cString]);
  printf ("archiving: %s ",
    [[glossary objectForKey: @"archiving"] cString]);

  [pool release];
  return 0;
}


Program 15.16. Output


abstract class: A class defined so other classes can inherit from it

adopt: To implement all the methods defined in a protocol

archiving: Storing an object for later use


The expression

[NSMutableDictionary dictionary]

creates an empty mutable dictionary. Key-value pairs can subsequently be added to the dictionary using the setObject:forKey: method. After the dictionary has been constructed, you can retrieve the value for a given key using the objectForKey: method. Program 15.17 shows how the three entries in the glossary were retrieved and displayed. In a more practical application, the user would type in the word he wanted to define and the program would search the glossary for its definition.

Enumerating a Dictionary

Program 15.17 illustrates how a dictionary can be defined with initial key-value pairs using the dictionaryWithObjectsAndKeys: method. An immutable dictionary is created, and the program also shows how the keyEnumerator method can be used to retrieve each element from a dictionary one key at a time. This process is known as enumeration. Unlike array objects, dictionary objects are not ordered. So, the first key-object pair placed in a dictionary might not be the first key extracted when the dictionary is enumerated.

Program 15.17.


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool  *pool = [[NSAutoreleasePool alloc] init];
  NSEnumerator *keyEnum;
  NSString     *key;

  NSDictionary *glossary =
   [NSDictionary dictionaryWithObjectsAndKeys:
     @"A class defined so other classes can inherit from it",
     @"abstract class",
     @"To implement all the methods defined in a protocol",
     @"adopt",
     @"Storing an object for later use",
     @"archiving",
       nil
   ];

  // Print all key-value pairs from the dictionary

  keyEnum = [glossary keyEnumerator];

  while ( (key = [keyEnum nextObject]) != nil ) {
    printf ("%s: %s ", [key cString],
    [[glossary objectForKey: key] cString]);
  }

  [pool release];
  return 0;
}


Program 15.17. Output


abstract class: A class defined so other classes can inherit from it

adopt: To implement all the methods defined in a protocol

archiving: Storing an object for later use


The argument to dictionaryWithObjectsAndKeys: is a list of object-key pairs (yes, in that order!), each separated by a comma. The list must be terminated with the special nil object.

After the program creates the dictionary, it sets up a loop to enumerate its contents. To use the special enumeration features provided by the Foundation framework, you need to add this line to your program:

#import <Foundation/NSEnumerator.h>

The loop you set up can be used for a dictionary of any size. The statement

keyEnum = [glossary keyEnumerator];

uses the keyEnumerator method to create a list of all the keys in glossary. The returned value is an NSEnumerator object, which is stored in the variable keyEnum. This object is used inside the loop as the receiver of the nextObject message. This method retrieves the next key from the dictionary (keys are not stored in any particular order, as noted) and returns it. When no more keys are left in the dictionary, it returns nil. The value associated with each enumerated key is obtained using the objectForKey: method and is then displayed.

Enumerations can be performed on arrays and sets (discussed in the next section) as well. The objectEnumerator method is used first on the dictionary, array, or set, followed by repeated calls to the nextObject method to retrieve successive elements. After the last element from the dictionary, array, or set is retrieved, nil is returned. In the case of an array, the elements are retrieved in order, whereas in the case of dictionaries and sets, the order is not defined.

If you wanted to display the contents of a dictionary in alphabetical order, you could first retrieve all the keys from the dictionary, sort them, and then retrieve all the values for those sorted keys in order. The method keysSortedByValueUsingSelector: does half of the work for you, returning the sorted keys in an array based on your sorting criteria.

As an example of array enumeration, you could replace the loop that prints all the generated prime numbers from Program 15.8

// Display the results
n = [primes count];
for (i = 0; i < n; ++i)
  printf ("%i ", [[primes objectAtIndex: i] intValue]);

printf (" ");

with the following functionally equivalent code:

#import <Foundation/NSEnumerator.h>
     ...
NSEnumerator *pElems;
NSNumber   *pNum;
     ...
// Display the results

pElems = [primes objectEnumerator];

while ( (pNum = [pElems nextObject]) != nil)
  printf ("%i ", [pNum intValue]);

printf (" ");

We have just shown some basic operations with dictionaries here. Tables 15.6 and 15.7 summarize some of the more commonly used methods for working with immutable and mutable dictionaries, respectively. Because NSMutableDictionary is a subset of NSDictionary, it inherits its methods.

In Tables 15.6 and 15.7, key, key1, key2, obj, obj1, and obj2 are any objects and size is an unsigned integer.

Table 15.6. Common NSDictionary Methods

image

Table 15.7. Common NSMutableDictionary Methods

image

Set Objects

A set is a collection of unique objects, and it can be mutable or immutable. Operations include searching, adding, and removing members (mutable sets); comparing two sets; and finding the intersection and union of two sets.

To work with sets in your program, include the following line:

#import <Foundation/NSSet.h>

Program 15.18 shows some basic operations on sets. Say you wanted to display the contents of your sets several times during execution of the program. You therefore have decided to create a new method called print. You add the print method to the NSSet class by creating a new category called Printing. NSMutableSet is a subclass of NSSet, so mutable sets can use the new print method as well.

Program 15.18.


#import <Foundation/NSObject.h>
#import <Foundation/NSSet.h>
#import <Foundation/NSValue.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>

// Create an integer object
#define INTOBJ(v) [NSNumber numberWithInt: v]

// Add a print method to NSSet with the Printing category
@interface NSSet (Printing);
-(void) print;
@end

@implementation NSSet (Printing);
-(void) print {
    NSEnumerator *setEnum;
    NSNumber   *element;

    setEnum = [self objectEnumerator];

    printf (" {");

    while ((element = [setEnum nextObject]) != nil)
          printf (" %i ", [element intValue]);

    printf ("} ");
}
@end


int main (int argc, char *argv[])
{
    NSAutoreleasePool  *pool = [[NSAutoreleasePool alloc] init];

    NSMutableSet *set1 = [NSMutableSet setWithObjects:
          INTOBJ(1), INTOBJ(3), INTOBJ(5), INTOBJ(10), nil];
    NSSet *set2 = [NSSet setWithObjects:
          INTOBJ(-5), INTOBJ(100), INTOBJ(3), INTOBJ(5), nil];
    NSSet *set3 = [NSSet setWithObjects:
          INTOBJ(12), INTOBJ(200), INTOBJ(3), nil];

    printf ("set1: "); [set1 print];
    printf ("set2: "); [set2 print];

    // Equality test
    if ([set1 isEqualToSet: set2] == NO)
    printf ("set1 equals set2 ");
    else
    printf ("set1 is not equal to set2 ");

    // Membership test

    if ([set1 containsObject: INTOBJ(10)] == YES)
          printf ("set1 contains 10 ");
    else
          printf ("set1 does not contain 10 ");

    if ([set2 containsObject: INTOBJ(10)] == YES)
          printf ("set2 contains 10 ");
    else
          printf ("set2 does not contain 10 ");

    // add and remove objects from mutable set set1

    [set1 addObject: INTOBJ(4)];
    [set1 removeObject: INTOBJ(10)];
          printf ("set1 after adding 4 and removing 10: "); [set1 print];

    // get intersection of two sets

    [set1 intersectSet: set2];
          printf ("set1 intersect set2: "); [set1 print];

    // union of two sets

    [set1 unionSet:set3];
          printf ("set1 union set3: "); [set1 print];

    [pool release];
    return 0;
}


Program 15.18. Output


set1: { 1 10 3 5 }
set2: { 3 100 -5 5 }
set1 is not equal to set2
set1 contains 10
set2 does not contain 10
set1 after adding 4 and removing 10: { 1 3 4 5 }
set1 intersect set2: { 3 5 }
set1 union set3: { 200 3 12 5 }


The print method uses the enumeration technique previously described to retrieve each element from the set. You also defined a macro called INTOBJ to create an integer object from an integer value. This enabled you to make your program more concise and saved some unnecessary typing. Of course, your print method is not that general because it works only with sets that have integer members in them. But it's a good reminder here about how to add methods to a class through a category.9

The setWithObjects: creates a new set from a nil-terminated list of objects. After creating three sets, the program displays the first two using your new print method. The isEqualToSet: method is then used to test whether set1 is equal to set2—it isn't.

The containsObject: method is used to first see whether the integer 10 is in set1 and then whether it is in set2. The Boolean values returned by the method verifies that it is in fact in the first set and not in the second.

The program next uses the addObject: and removeObject: methods to add and remove 4 and 10 from set1, respectively. Displaying the contents of the set verifies that the operations were successful.

The intersect: and union: methods can be used to calculate the intersection and union of two sets. In both cases, the result of the operation replaces the receiver of the message.

The Foundation framework also provides a class called NSCountedSet. These sets can represent more than one occurrence of the same object; however, instead of the object appearing multiple times in the set, a count of the number of times is maintained. So, the first time an object is added to the set, its count is 1. Subsequently, adding the object to the set increments the count, whereas removing the object from the set decrements the count. If it reaches zero, the actual object itself is removed from the set. The countForObject: is used to retrieve the count for a specified object in a set.

One application for a counted set might be a word counter application. Each time a word is found in some text, it can be added to the counted set. When the scan of the text is complete, each word can be retrieved from the set along with its count, which indicates the number of times the word appeared in the text.

We have just shown some basic operations with sets here. Tables 15.8 and 15.9 summarize commonly used methods for working with immutable and mutable sets, respectively. Because NSMutableSet is a subclass of NSSet, it inherits its methods.

In Tables 15.8 and 15.9, obj, obj1, and obj2 are any objects; nsset is an NSSet or NSMutableSet object; and size is an unsigned integer.

Table 15.8. Common NSSet Methods

image

Table 15.9. Common NSMutableSet Methods

image

Exercises

  1. Look up the NSCalendarDate class in your documentation. Then add a new category to NSCalendarDate called ElapsedDays. In that new category, add a method based on the following method declaration:

    -(unsigned long) numberOfElapsedDays: (NSCalendarDate *) theDate;

    Have the new method return the number of elapsed days between the receiver and the argument to the method. Write a test program to test your new method. (Hint: Look at the years:months:days:hours:minutes:seconds: sinceDate: method.)

  2. Modify the lookup: method developed in this chapter for the AddressBook class so that partial matches of a name can be made. The message expression

    [myBook lookup: @"steve"]

    should match an entry that contains the string steve anywhere within the name.

  3. Modify the lookup: method developed in this chapter for the AddressBook class to search the address book for all matches. Have the method return an array of all such matching address cards or nil if no match is made.
  4. Add new fields of your choice to the AddressCard class. Some suggestions are separating the name field into first and last name fields and adding address (perhaps with separate state, city, ZIP, and country fields) and phone number fields. Write appropriate setter and getter methods, and ensure that the fields are displayed properly by the print and list methods.
  5. After completing exercise 3, modify the lookup: method from exercise 2 to perform a search on all the fields of an address card. Can you think of a way to design your AddressCard and AddressBook classes so that the latter does not have to know all the fields stored in the former?
  6. Add the method removeName: to the AddressBook class to remove someone from the address book given this declaration for the method:

    -(BOOL) removeName: (NSString *) theName;

    Use the lookup: method developed in exercise 2. If the name is not found, or multiple entries exist, have the method return NO. If the person is successfully removed, have it return YES.

  7. Using the Fraction class defined in Part I, “The Objective-C Language,” set up an array of fractions with some arbitrary values. Then write some code that finds the sum of all the fractions stored in the array. Make sure you modify the Fraction class as appropriate to run under Foundation.
  8. Using the Fraction class defined in Part I, set up a mutable array of fractions with arbitrary values. Then sort the array using the sortUsingSelector: method from the NSMutableArray class. Add a Comparison category to the Fraction class and implement your comparison method in that category. Make sure you modify the Fraction class as appropriate to run under Foundation.
  9. Define three new classes, called Song, PlayList, and MusicCollection. A Song object will contain information about a particular song, such as its title, artist, album, and playing time. A PlayList object will contain the name of the playlist and a collection of songs, and a MusicCollection object will contain a collection of playlists, including a special master playlist called library that contains every song in the collection. Define these three classes and write methods to do the following:

    • Create a Song object and set its information.

    • Create a Playlist object and add and remove songs to and from a playlist. A new song should be added to the master playlist if it's not already there. Make sure that, if a song is removed from the master playlist, it is removed from all playlists in the music collection as well.

    • Create a MusicCollection object and add and remove playlists to and from the collection.

    • Search and display the information about any song, playlist, or the entire music collection.

    Make sure all your classes do not leak memory!

  10. Write a program that takes an array of integer objects and produces a frequency chart that lists each integer and how many times it occurs in the array. Use an NSCountedSet object for constructing your frequency counts.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.38.43