Remove and foreach
A HashSet has two more everyday operations: taking a value out, and visiting every value.
HashSet<string> colors = new HashSet<string>(new[] {"red", "blue", "green"});
colors.Remove("blue");
colors.Remove("pink"); // not in it, nothing happens
foreach (string color in colors) {
Console.WriteLine(color); // red and green, in any order
}
s.Remove(x) takes x out of the set s. It returns true if x was there. If it wasn't, nothing happens: no error, and the return value is false. This makes Remove safe to call without checking Contains first.
foreach (string x in s) visits each value once. The order isn't fixed, and it may differ from the order you added the values in. For the example above, the loop prints red and green.
Using the answer of Remove
Because Remove reports whether it removed something, you can use it as the test itself:
HashSet<string> left = new HashSet<string>(new[] {"Ava", "Leo"});
if (left.Remove("Leo")) {
Console.WriteLine("Leo removed");
}
if (!left.Remove("Zed")) {
Console.WriteLine("Zed was not there");
}
Both messages print, one after the other. The set ends up holding only Ava.
When looping a set makes sense
Loop over a set when the order doesn't matter. A total, a count and a check are fine, because they come out the same in any order. If you need the values in a particular order, put them in a list and sort it first.
Don't change a set while looping over it
Adding to a set inside a foreach over it throws an InvalidOperationException (the message is "Collection was modified; enumeration operation may not execute."). Lists behave the same way for both Add and Remove. On current .NET, HashSet.Remove inside a loop does not throw, but mixing the two jobs is still a habit worth avoiding: the loop and the changes are easier to follow when you remove first and loop afterwards. If you must remove the values that match a rule, RemoveWhere does it in one call:
HashSet<int> nums = new HashSet<int>(new[] {1, 2, 3, 4, 5, 6});
nums.RemoveWhere(n => n % 2 == 0); // removes 2, 4 and 6
That leaves 1, 3 and 5.
The exercise
In a word game, the set found holds the words a player found: cat, tree, sun, qqq and moon. The words "qqq" and "zzz" aren't allowed, so remove both. The player never found "zzz", so removing it changes nothing. Then add up the points: a word scores one point per letter.
- Call
found.Remove("qqq")andfound.Remove("zzz"), one per line. - Below
int score = 0;, write aforeachloop overfoundwith astring word. - Inside the loop, add
word.Lengthtoscore.
class Solution {
public void MainFunction() {
List<string> words = new List<string>(new[] {"cat", "tree", "sun", "qqq", "moon"});
HashSet<string> found = new HashSet<string>(words);
found.Remove("qqq");
found.Remove("zzz");
Console.WriteLine(found.Count + " words");
int score = 0;
foreach (string word in found) {
score += word.Length;
}
Console.WriteLine(score + " points");
}
}
Output:
4 words
14 points
Four words are left: cat (3), tree (4), sun (3) and moon (4), which add up to 14 in any visiting order.
Common mistakes in the exercise
- Guarding
Remove("zzz")with an error check. No check is needed. The word isn't in the set, andRemovejust returnsfalse. - Forgetting the removal before the first print. The line that prints the number of words comes after the removals, so it shows 4 and not 5.
- Adding 1 per word instead of its length. A word scores one point per letter, so the loop adds
word.Length. Adding 1 would give 4 points, not 14. - Expecting a printed order. This exercise prints only totals, so the order the loop visits the words in never shows.
Key takeaways
Remove(x)returnstrueorfalseand never fails for a missing value.foreachvisits each value once, in no fixed order.- Don't add to a set while looping over it; remove first, then loop.
