2. Variables, types and input
Sections
Reading settings
Natural voice recorded for this course, the same in every browser.
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Code: the examples examples/l02_*.cs, the rejected snippets compile_fail/l02_*.cs and the solutions exercises/l02_*.cs. Run an example from the code/csharp-beginner folder with dotnet run examples/l02_variables.cs.
Variables
Section titled “Variables”A variable is a named box that holds a value. In C#, every box also has a type, fixed when you create it: the kind of values it accepts.
// A variable is a named box that holds a value of one typeint strings = 6;string tuning = "E A D G B E";double scaleLength = 64.8; // centimetres, a common guitar scale lengthbool isAcoustic = true;char lowest = 'E';
Console.WriteLine(strings);Console.WriteLine(tuning);Console.WriteLine(scaleLength);Console.WriteLine(isAcoustic);Console.WriteLine(lowest);
// The value can change; the type can'tstrings = 7;Console.WriteLine(strings);
// var: the compiler infers the type from the valuevar frets = 22; // intvar name = "Stratocaster"; // stringConsole.WriteLine(frets.GetType());Console.WriteLine(name.GetType());
// const: a value that never changesconst int SemitonesPerOctave = 12;Console.WriteLine(SemitonesPerOctave * 2);6E A D G B E64.8TrueE7System.Int32System.String24int strings = 6; is a declaration: the type, the name, =, then the first value. It reads “make a box named strings for an int, and put 6 in it”. Later, strings = 7; is an assignment: it replaces the value in the box. The = sign means “put into”, not “is equal to”.
Names follow a few rules: letters, digits and _, not starting with a digit, and not a keyword of the language such as int or if. By convention, a local variable starts with a lower-case letter and each following word with a capital: scaleLength. That style is called camelCase; the C# identifier naming rules list the others.
The basic types
Section titled “The basic types”| Type | Holds | Examples | Range and precision |
|---|---|---|---|
int |
whole numbers | 6, -12, 2_000_000 |
about -2.1 to 2.1 billion |
long |
larger whole numbers | 9_000_000_000 |
about ±9.2 billion billion |
double |
numbers with decimals, approximate | 64.8, 1e-3 |
15 to 17 significant digits |
decimal |
numbers with decimals, exact | 19.99m |
28 to 29 significant digits |
bool |
true or false | true, false |
|
char |
one character | 'E', '#' |
between single quotes |
string |
text | "E A D G B E", "" |
between double quotes |
The integral numeric types and floating-point numeric types pages list the others, such as byte, short or float. You’ll rarely need them as a beginner. The _ in 2_000_000 only makes the number easier to read.
GetType() printed System.Int32 and System.String: int is the C# keyword for the .NET type Int32, a 32-bit integer, and string is the keyword for String. Both names mean exactly the same type.
With var, the compiler reads the value and gives the variable its type: var frets = 22; makes an int. The variable still has one type for its whole life. var saves typing when the type is obvious from the value; write the type when it helps the reader.
A const is a value fixed when you write the program: 12 semitones per octave will never change. Its name starts with a capital letter.
What the compiler refuses
Section titled “What the compiler refuses”Each of these small programs is rejected. The type of a variable can’t change, so a string doesn’t fit in an int box:
var frets = 22;frets = "twenty-two";l02_type_change.cs(2,9): error CS0029: Cannot implicitly convert type 'string' to 'int'A variable must have a value before it’s read:
int strings;Console.WriteLine(strings);l02_unassigned.cs(2,19): error CS0165: Use of unassigned local variable 'strings'var needs a value to find the type:
var tuning;tuning = "E A D G B E";l02_var_without_value.cs(1,5): error CS0818: Implicitly-typed variables must be initializedAnd a constant stays constant:
const int SemitonesPerOctave = 12;SemitonesPerOctave = 13;l02_const_change.cs(2,1): error CS0131: The left-hand side of an assignment must be a variable, property or indexerNumbers and arithmetic
Section titled “Numbers and arithmetic”+, -, * and / work as in mathematics, and % gives the remainder of a division. The type of the numbers changes the result:
// Integers: the division drops the decimalsConsole.WriteLine(7 / 2);Console.WriteLine(7 % 2); // the remainderConsole.WriteLine(7 / 2.0); // one double in the operation: the result is a double
// Each integer type has a range; int goes from about -2.1 billion to 2.1 billionConsole.WriteLine(int.MaxValue);int big = int.MaxValue;big = big + 1; // wraps around without an errorConsole.WriteLine(big);Console.WriteLine(long.MaxValue);
// double is fast but approximate: 0.1 has no exact binary formConsole.WriteLine(0.1 + 0.2);Console.WriteLine(0.1 + 0.2 == 0.3);
// decimal is exact for decimal fractions: use it for moneyConsole.WriteLine(0.1m + 0.2m);Console.WriteLine(0.1m + 0.2m == 0.3m);decimal price = 19.99m;Console.WriteLine(price * 3);313.52147483647-214748364892233720368547758070.30000000000000004False0.3True59.97Three surprises for a beginner:
-
7 / 2is3. When both numbers are integers, the division is an integer division: the decimals are dropped, not rounded.7 % 2gives what’s left,1. If one of the numbers is adouble, such as2.0, the result is adouble:3.5. -
An
intthat goes past its maximum wraps around to the most negative value, without any error. The compiler only catches it when the whole calculation is made of constants:int big = int.MaxValue + 1;l02_overflow_constant.cs(1,11): error CS0220: The operation overflows at compile time in checked modeThe
checkedkeyword makes the program fail instead of wrapping, when the numbers are only known while it runs. -
0.1 + 0.2isn’t exactly0.3withdouble. Adoublestores numbers in binary, where 0.1 has no exact form, just as 1/3 has no exact decimal form. For money, usedecimal, where0.1m + 0.2mis exactly0.3. Themafter the number makes it adecimal; without it, the compiler refuses:decimal price = 19.99;l02_decimal_literal.cs(1,17): error CS0664: Literal of type double cannot be implicitly converted to type 'decimal'; use an 'M' suffix to create a literal of this type
A real calculation: the frequency of a fret
Section titled “A real calculation: the frequency of a fret”Each fret of a guitar raises the note by a semitone, which multiplies the frequency of the string by the twelfth root of 2, about 1.0595. Twelve frets double the frequency: one octave. Math.Pow(x, y) computes x to the power y:
// The open A string vibrates at 110 Hz. Each fret raises the note by a semitone,// which multiplies the frequency by the 12th root of 2.double openA = 110.0;int fret = 7;
double wrong = openA * Math.Pow(2, fret / 12); // 7 / 12 is an integer division: 0double right = openA * Math.Pow(2, fret / 12.0); // 7 / 12.0 is 0.5833...
Console.WriteLine($"Fret {fret}, wrong: {wrong} Hz");Console.WriteLine($"Fret {fret}, right: {right} Hz");Console.WriteLine($"Rounded: {right:F2} Hz");Console.WriteLine($"Fret 12: {openA * Math.Pow(2, 12 / 12.0)} Hz");Fret 7, wrong: 110 HzFret 7, right: 164.81377845643496 HzRounded: 164.81 HzFret 12: 220 Hzfret / 12 is 0, because both are int, and 2 to the power 0 is 1: the “wrong” frequency is the open string. This mistake gives no error and no warning, only a wrong result. Writing 12.0 makes it a double division. Fret 7 of the A string is an E, at about 164.81 Hz.
Conversions
Section titled “Conversions”Some conversions happen by themselves; others must be asked for.
// Implicit conversion: no information can be lostint semitones = 7;double asDouble = semitones;Console.WriteLine(asDouble);
// Explicit conversion (a cast): the decimals are cut, not roundeddouble hertz = 164.81;int truncated = (int)hertz;Console.WriteLine(truncated);
// Rounding: by default, halfway values go to the nearest even numberConsole.WriteLine(Math.Round(2.5));Console.WriteLine(Math.Round(3.5));Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero));
// From text to numberint frets = int.Parse("22");Console.WriteLine(frets + 2);
// TryParse doesn't fail on bad text: it returns falsebool ok = int.TryParse("twenty-two", out int parsed);Console.WriteLine($"{ok} {parsed}");ok = int.TryParse("24", out parsed);Console.WriteLine($"{ok} {parsed}");
// From number to textstring text = frets.ToString();Console.WriteLine(text + "2"); // + on strings joins themConsole.WriteLine(frets + 2); // + on numbers adds them716424324False 0True 2422224Between number types
Section titled “Between number types”Every int fits in a double, so the compiler converts for you: that’s an implicit conversion. The other way, a double can hold decimals that an int can’t, so the compiler refuses to convert silently:
double hertz = 164.81;int rounded = hertz;l02_double_to_int.cs(2,15): error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?)The message suggests a cast: the type in parentheses before the value, (int)hertz. It tells the compiler “I know I may lose something”. A cast to int cuts the decimals: 164.81 becomes 164.
To round instead, use Math.Round. By default it rounds a value exactly halfway to the nearest even number: 2.5 gives 2 and 3.5 gives 4. This is called banker’s rounding, and it avoids pushing every halfway value up. MidpointRounding.AwayFromZero gives the rounding taught at school.
From text to number
Section titled “From text to number”A string that contains digits is still text: "22" can’t go into an int box.
int frets = "22";l02_string_to_int.cs(1,13): error CS0029: Cannot implicitly convert type 'string' to 'int'int.Parse reads the number in a string, and stops the program with an error if the text isn’t a number. int.TryParse never stops the program: it returns true or false, and gives the number through its out parameter. out int parsed declares the variable parsed right there, in the call. When the text isn’t a number, parsed is 0 and the result is false. Use TryParse for anything a person types: people make typing mistakes.
double.Parse, decimal.Parse and their TryParse versions work the same way for the other types.
From number to text
Section titled “From number to text”ToString() turns any value into text. Then + means something else: between two strings, + joins them. "22" + "2" is "222", while 22 + 2 is 24. When one side is a string and the other a number, C# turns the number into text and joins them, from left to right: that’s why "Frets played: " + 3 + 2 + 1 gives Frets played: 321.
Strings and interpolation
Section titled “Strings and interpolation”Joining strings with + quickly gets hard to read. String interpolation puts values right inside the text: a $ before the opening quote, and each expression between braces.
string note = "A";int octave = 4;double frequency = 440;
// Interpolation: $ before the quotes, expressions between bracesConsole.WriteLine($"{note}{octave} vibrates at {frequency} Hz");Console.WriteLine($"One octave higher: {frequency * 2} Hz");
// Format and alignment: {value,width:format}Console.WriteLine($"[{note,-5}] [{octave,5}] [{frequency,8:F1}]");Console.WriteLine($"{1234567.891:N2}");
// A few string methodsstring model = "Les Paul";Console.WriteLine(model.Length);Console.WriteLine(model.ToUpper());Console.WriteLine(model.Contains("Paul"));Console.WriteLine(model.Replace("Paul", "Standard"));Console.WriteLine(model[0]); // the first character: a char
// Special charactersConsole.WriteLine("Tab:\tafter\nNew line, and a quote: \"");Console.WriteLine(@"C:\Users\ada\music"); // verbatim: \ is just a characterConsole.WriteLine(""" A raw string literal keeps "quotes" and line breaks as they are. """);A4 vibrates at 440 HzOne octave higher: 880 Hz[A ] [ 4] [ 440.0]1,234,567.898LES PAULTrueLes StandardLTab: afterNew line, and a quote: "C:\Users\ada\musicA raw string literal keeps "quotes"and line breaks as they are.- Inside the braces, any expression works:
frequency * 2is computed, then inserted. - After a comma, a width:
{octave,5}fills 5 characters, aligned to the right; a negative width,{note,-5}, aligns to the left. After a colon, a format:F1shows one decimal,N2two decimals with thousands separators. The standard numeric format strings list them all. - A string knows its
Length, and has methods such asToUpper,ContainsorReplace. None of them changesmodel: they return a new string.model[0]is the character at position 0, the first one: positions start at 0. - A backslash starts an escape sequence:
\tis a tab,\na new line,\"a quote inside the string. With@before the string, a backslash is just a backslash, handy for Windows paths. Between three quotes, a raw string literal keeps quotes and line breaks as they are, and removes the indentation of the closing""".
The string interpolation tutorial goes further.
Reading what the user types
Section titled “Reading what the user types”Console.ReadLine() waits until the user types a line and presses Enter, then returns that line as a string, without the Enter.
Console.Write("What is your name? ");string? name = Console.ReadLine();
Console.Write("How many years have you played the guitar? ");string? answer = Console.ReadLine();
if (int.TryParse(answer, out int years)){ Console.WriteLine($"Hello {name}, {years} years is {years * 12} months of practice.");}else{ Console.WriteLine($"Hello {name}, \"{answer}\" is not a whole number.");}When I type Ada and 3, the terminal shows:
> dotnet run examples/l02_input.csWhat is your name? AdaHow many years have you played the guitar? 3Hello Ada, 3 years is 36 months of practice.The CI can’t type: check.sh sends the lines of input/l02_input.txt to the program instead, with a redirection of its input:
dotnet run examples/l02_input.cs < input/l02_input.txtThe typed text is then not shown, and the expected output is one line: What is your name? How many years have you played the guitar? Hello Ada, 3 years is 36 months of practice.
Two new things in this program:
- The type is
string?, with a question mark:ReadLinereturns a string, ornullwhen there is nothing left to read. Lesson 4 explainsnull.int.TryParseaccepts anulland returnsfalse. ifandelsechoose between two blocks, depending on theboolthatTryParsereturns. Lesson 3 is about them.
Numbers and the language of the user
Section titled “Numbers and the language of the user”1.5 is one and a half in English, but a French or Spanish speaker writes 1,5. .NET follows the culture of the computer, its language and region settings, when it prints and reads numbers:
using System.Globalization;
// The format of numbers depends on the culture: the language and region of the userdouble frequency = 1234.5;foreach (string name in new[] { "en-US", "fr-FR", "es-ES" }){ CultureInfo.CurrentCulture = new CultureInfo(name); bool ok = double.TryParse("1.5", out double parsed); Console.WriteLine($"{name}: {frequency:F1} \"1.5\" read as {parsed} ({ok})");}
// The invariant culture gives the same result everywhere: use it for files and dataCultureInfo.CurrentCulture = CultureInfo.InvariantCulture;Console.WriteLine(double.Parse("1.5"));en-US: 1234.5 "1.5" read as 1.5 (True)fr-FR: 1234,5 "1.5" read as 0 (False)es-ES: 1234,5 "1.5" read as 15 (True)1.5With the French culture, "1.5" isn’t a number. With the Spanish culture, it’s worse: the dot separates thousands in Spanish, so "1.5" is read as 15, without any error. The outputs of this course were captured with an English culture; on a French or Spanish computer, your decimal numbers may print with a comma. For data that programs exchange, such as files, use CultureInfo.InvariantCulture, which is the same on every computer. The globalization guide has the details.
using System.Globalization; at the top makes the names of that namespace, a group of related types, usable without their long prefix. Console and Math need no using: file-based apps and new projects turn on implicit usings for the most common namespaces.
Key takeaways
Section titled “Key takeaways”- A variable has a name, a type and a value;
=puts a value in it. The type never changes, andvaronly asks the compiler to find it. intandlonghold whole numbers,doubleapproximate decimals,decimalexact decimals (for money),booltrue or false,charone character,stringtext.- Dividing two integers drops the decimals; write
12.0to get adoubledivision. - Widening conversions are automatic; narrowing ones need a cast, such as
(int)hertz, which cuts the decimals. int.TryParseturns text into a number without stopping the program on bad input.- An interpolated string,
$"…{value,width:format}…", builds text from values;Console.ReadLine()reads a line typed by the user. - The culture changes how numbers print and parse: use the invariant culture for data.
Exercises
Section titled “Exercises”- A song lasts 3725 seconds. Using only integers,
/and%, print its length as1 h 02 min 05 s. The formatD2prints an integer with at least two digits.
Solution
// Exercise 1: a song of 3725 seconds, in hours, minutes and secondsint total = 3725;int hours = total / 3600;int minutes = total % 3600 / 60;int seconds = total % 60;Console.WriteLine($"{hours} h {minutes:D2} min {seconds:D2} s");1 h 02 min 05 stotal / 3600 counts the whole hours. total % 3600 is what’s left after them, 125 seconds, and / 60 counts its whole minutes. % and / have the same priority and are computed from left to right, so total % 3600 / 60 is (total % 3600) / 60.
- Ask the user for the price of a set of strings and a number of sets. Print the subtotal, a 15% tax rounded to the cent, and the total, with two decimals. If either answer isn’t valid, print a message instead. Which type do you use for the price?
Solution
decimal, since it’s money. exercises/l02_ex_order.cs:
// Exercise 2: read a price and a quantity, print the total with 15% taxConsole.Write("Price of a set of strings? ");string? priceText = Console.ReadLine();Console.Write("How many sets? ");string? quantityText = Console.ReadLine();
bool priceOk = decimal.TryParse(priceText, out decimal price);bool quantityOk = int.TryParse(quantityText, out int quantity);
if (priceOk && quantityOk){ decimal subtotal = price * quantity; decimal tax = Math.Round(subtotal * 0.15m, 2); Console.WriteLine($"Subtotal: {subtotal:F2}"); Console.WriteLine($"Tax: {tax:F2}"); Console.WriteLine($"Total: {subtotal + tax:F2}");}else{ Console.WriteLine("Please type a price such as 12.49 and a whole number.");}With 12.49 and 3:
Price of a set of strings? 12.49How many sets? 3Subtotal: 37.47Tax: 5.62Total: 43.09&& means “and”: both conversions must succeed. Math.Round(…, 2) rounds to two decimals; 15% of 37.47 is 5.6205, rounded to 5.62. On a computer with a French or Spanish culture, type the price with a comma: 12,49.
- Fret 7 is a fifth above the open string. In the tuning of old instruments, a fifth multiplied the frequency by exactly 3/2. How far is the modern ratio, 2 to the power 7/12, from 3/2? Print both with five decimals, and the difference as a percentage of 3/2.
Solution
// Exercise 3: fret 7 is a fifth above the open string. How close is 2^(7/12) to 3/2?double tempered = Math.Pow(2, 7 / 12.0);double pure = 3.0 / 2.0;Console.WriteLine($"Equal temperament: {tempered:F5}");Console.WriteLine($"Pure fifth: {pure:F5}");Console.WriteLine($"Difference: {(pure - tempered) / pure * 100:F3} %");Equal temperament: 1.49831Pure fifth: 1.50000Difference: 0.113 %Two traps: 7 / 12 would be 0, and 3 / 2 would be 1. Writing 12.0 and 3.0 makes both divisions double divisions. The modern tuning, equal temperament, makes every semitone the same size, so that a guitar plays in tune in every key, at the cost of fifths about 0.1% too narrow.