Booleans and Characters
bool holds only true or false, with no truthiness in C#, and a char is one UTF-16 code unit, not always one visible character.
bool isReady = true; char letter = 'A';Two small types that carry more weight than their size suggests. bool answers a yes-or-no question. char holds a single character of text.
bool
A bool holds exactly one of two values, written true and false. Nothing else converts to it. The number 0 is not false, an empty string is not false, and null is not false. In some languages any value can stand in for a condition, which is called truthiness. C# has none of it. Writing a number where a condition belongs is a build error, not a surprise at run time. What produces a bool is a comparison: count == 0 is an expression whose value is a bool, and you can store it in a variable like any other value. The default value of a bool is false.
char
A char is written in single quotes: 'A'. Double quotes make a string, which is a different type even when it holds one character. A char is 16 bits, and what it holds is one UTF-16 code unit, a number from U+0000 to U+FFFF. You can write the same character three ways, and they are the same value: 'A', the Unicode escape 'A' with its four required hex digits, and the cast (char)65.
What trips people up
One code unit is not always one visible character. Characters outside the range U+0000 to U+FFFF are stored as two code units, called a surrogate pair. A string holding one emoji has a Length of 2. Escapes of the form \U0001F4A1, with eight hex digits, write such a character in a string literal.
Example
// bool holds exactly one of two values.
bool isOpen = true;
bool isLocked = false;
Console.WriteLine($"{isOpen} {isLocked}");
// Comparing two numbers produces a bool. A number on its own is not a
// condition in C#: there is no truthiness here.
int count = 0;
bool isEmpty = count == 0;
Console.WriteLine(isEmpty);
// char is one UTF-16 code unit, written in single quotes.
char letter = 'A';
char sameLetter = 'A'; // the same code unit, written as its hex code
Console.WriteLine($"{letter} {sameLetter} {(int)letter}");
// For most text, Length counts what you see.
Console.WriteLine("cat".Length);
// Not always. This is one printed symbol stored as two code units.
string bulb = "\U0001F4A1";
Console.WriteLine(bulb.Length);
Console.WriteLine(char.IsSurrogatePair(bulb[0], bulb[1]));
// Output:
// True False
// True
// A A 65
// 3
// 2
// TrueWhen to use it
- Store the result of a comparison in a named bool so the reason for a decision reads clearly.
- Read a true/false setting out of text with bool.TryParse instead of comparing strings yourself.
- Check whether a character is a digit or a letter with char.IsDigit or char.IsLetter before converting it.
- Truncate user-supplied text by Rune rather than by char so an emoji is not split in half.
More examples
Inspecting characters, and parsing a bool
The char helper methods all live as static methods on char itself, so you call them as char.IsUpper rather than on the character. The last four lines are the thing to remember about reading a flag out of a config file or a query string: bool.TryParse accepts only the words true and false, ignoring case and surrounding space, so "yes", "1" and "on" all come back false with the parse reported as failed.
// Indexing a string gives you one char at a time.
string word = "Code";
Console.WriteLine(word[0]);
Console.WriteLine(char.IsUpper(word[0]));
Console.WriteLine(char.ToLowerInvariant(word[0]));
// char compares and adds as its numeric code.
Console.WriteLine('b' > 'a');
Console.WriteLine((char)('a' + 1));
// bool has its own parser, and it only accepts "true" or "false".
Console.WriteLine(bool.TryParse("true", out bool flag));
Console.WriteLine(flag);
Console.WriteLine(bool.TryParse("yes", out bool other));
Console.WriteLine(other);
// Output:
// C
// True
// c
// True
// b
// True
// True
// False
// FalseOne symbol, two code units
Length reports 3 for a string a reader would call two characters long. Rune is the type that models a whole character: GetRuneAt takes the index of a code unit and returns the complete scalar value starting there, and Utf16SequenceLength says how many code units it occupied, which is how you advance to the next one. Use Rune whenever you truncate user-supplied text, so you do not cut a surrogate pair in half and produce a broken string.
using System.Text;
// This string holds one light bulb symbol followed by the letter a.
string text = "\U0001F4A1a";
Console.WriteLine(text.Length); // code units, not printed characters
// A Rune is a whole Unicode scalar value, so it spans the surrogate pair.
// Rune lives in System.Text and arrived in .NET Core 3.0.
Rune bulb = Rune.GetRuneAt(text, 0);
Console.WriteLine(bulb.Value);
Console.WriteLine(bulb.Utf16SequenceLength);
Rune letter = Rune.GetRuneAt(text, 2);
Console.WriteLine(letter.Value);
Console.WriteLine(letter.Utf16SequenceLength);
// Output:
// 3
// 128161
// 2
// 97
// 1Default values and stored comparisons
Naming a comparison is the cheapest readability win in the language: isWarm says what the number means, and the name can be reused without recomputing. The default values matter later, because a bool field you never assign is false rather than unset, and a char you never assign holds the null character U+0000 rather than a space.
// A comparison is a value, so you can name it and reuse it.
int temperature = 21;
bool isWarm = temperature > 18;
bool isCold = temperature < 5;
Console.WriteLine($"{isWarm} {isCold}");
// Every type has a default value. bool starts false; char starts at U+0000.
Console.WriteLine(default(bool));
Console.WriteLine((int)default(char));
// Output:
// True False
// False
// 0
Discussion