问题
What is the smartest way to build a query with NSString, when the parameter count is variable?
Let's say I have a web service, which returns a list of countries filtered by a list of parameters. For example:
NSString *continent = @"europe";
NSString *language = @"german";
NSString *timeZone = @"UTC+1";
// to be continued
Thus my query string would be:
[NSString stringWithFormat:@"/getCountries?continent=%@&language=%@&timezone=%@",
continent, language, timeZone];
My response would contain
Austria, Germany and Switzerland
because they match all three parameters.
Now I'm looking for the smartest way (e.g. not a spaghetti code of ifs) to build this query, when 0:n of these parameters could be nil. (e.g. when *timeZone is nil, the URL should not contain timezone=nil) The result should be a NSString.
回答1:
I would make an object that does this one thing and does it right.
One key thing you have to look out for is to make sure that you escape all your names and values properly. For example, if you pass in a value of “fred&wilma”, that shouldn't appear as “members=fred&wilma” in the URL—you'd need the & to be escaped (as %26).
You could simply escape each string as you staple it on, but it would be too easy to forget to do that. Anything that is repeated in the code is too easily missed.
That's the main reason I'd make an object for this. That object would not only staple strings together, but also escape them appropriately.
I'll assume you don't need to look up values for keys later. You just want to construct a URL.
First, the object should hold an NSMutableString, privately (not a public @property—either a private @property or an instance variable). It should create this in initialization and hold onto it for its entire life.
The object's designated initializer should be something like initWithResourceURLString:. This method sends the string a mutableCopy message and assigns the result to the instance variable or private property. init should throw an exception (most easily by asserting false).
The object should also have an instance variable for a single unichar. initWithResourceURLString: should set it to '?'.
The object should respond to a message such as setQueryParameterWithName:toValue:. Each argument should be a string. If either string is nil, the method simply returns.
That method sends each string a stringByAddingPercentEscapesUsingEncoding: message, then sends the mutable string an appendFormat: message with the following format:
%C%@=%@
with the arguments being the character in the unichar instance variable, the escaped parameter name, and the escaped value. It then sets the character variable to &.
The object should respond to a message such as constructedURLString by returning a copy (autoreleased if you're using MRC) of the mutable string.
You would then use that object like so:
builder = [[MyURLBuilder alloc] initWithResourceURLString:@"http://example.com/getCountries"]; //Add autorelease under MRC
[builder setQueryParameterName:@"continent" toValue:continent];
[builder setQueryParameterName:@"language" toValue:language];
[builder setQueryParameterName:@"timeZone" toValue:timeZone];
NSURL *finishedURL = [NSURL URLWithString:[builder constructedURLString]];
Advantages:
- You have exactly one place in which to make sure escaping is handled correctly. All other code should not need to worry about it.
- You have exactly one place in which to make sure
nilis handled (ignored) correctly. All other code should not need to worry about it. - Order is preserved. A dictionary may change the order of the arguments. This shouldn't matter (the server shouldn't care), but if it does, then you need an order-preserving solution.
- It would be easy to write unit tests for this object.
- If you want, you can modify this object in a number of directions. You could, for example, add support for resetting the constructed query URL without modifying or having to separately remember the base URL, enabling you to reuse this object for multiple queries. You could also add support for substituting the empty string for
nilfor any or for only some parameters. Any changes you make to the class would immediately be available to any other URL-building jobs you may have.
回答2:
You could use the ternary operator, then:
[NSString stringWithFormat:@"/getCountries?continent=%@&language=%@&timezone=%@", (continent ? continent : @""), (language ? language : @""), (timezone ? timezone : @"")];
回答3:
Store the params in an NSDictionary instead of individual NSStrings. Then walk the dictionary to build your param string. This implementation will handle 0 to N params. You can generalize it for use in an API by parametrizing the base URL as well.
NSMutableString *paramString = [NSMutableString stringWithString:@"/getCountries"];
NSUInteger paramCount = 0;
[params enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop){
[paramString appendString:(paramCount == 0) ? @"?" : @"&"];
NSString *newParam = [[NSString stringWithFormat:@"%@=%@", key, [params objectForKey:key]] stringByAddingPercentEscapesUsingEncoding:NSStringUTF8Encoding];
[paramString appendString:newParam];
paramCount++;
}];
回答4:
@XJones has a good start, but I think it could be done even more simply:
NSDictionary *params = ...;
NSMutableArray *pairs = [NSMutableArray array];
for (NSString *key in params) {
NSString *value = [params objectForKey:key];
NSString *pair = [NSString stringWithFormat:@"%@=%@", key, value];
[pairs addObject:pair];
}
NSString *queryString = [pairs componentsJoinedByString:@"&"];
NSString *path = [NSString stringWithFormat:@"/getCountries?%@", queryString];
Of course, you'll want to properly URL encode key and value.
回答5:
Simply throw your parameters into an NSDictionary, and build the URL based on that:
NSMutableDictionary * dict = [NSMutableDictionary dictionary];
if (continent) [dict setObject:continent forKey:@"continent"];
if (timeZone) [dict setObject:timeZone forKey:@"timeZone"];
NSMutableString * queryString = [NSMutableString stringWithString:@"/getCountries?"];
NSArray * allKeys = [dict allKeys];
for (NSUInteger i = 0; i < [allKeys count]; i++) {
NSString * key = [allKeys objectAtIndex:i];
NSString * value = [dict objectForKey:key];
NSString * escaped = [value stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
[queryString appendFormat:@"%@=%@", key, escaped];
if (i + 1 < [allKeys count]) {
[queryString appendString:@"&"];
}
}
来源:https://stackoverflow.com/questions/8263001/variable-argument-lists-in-stringwithformat-for-query-string