Last week we learned about the integral types int
(a signed 32-bit integer), long
(a
signed 64-bit integer) and byte
(an
unsigned 8-bit integer). Here are two more unsigned types:
uint
– an
unsigned 32-bit integer (0 ≤ i < 232)
ulong
– an unsigned 64-bit integer (0 ≤ i < 264)
C# will perform an implicit conversion between two numeric types if every possible value of the source type is valid in the destination type. For example:
byte b; ... int i = b; // implicit conversion
You can use an explicit conversion to force a conversion between any two numeric types. This is accomplished with a type cast:
int i = 300; byte c = (byte) i;
If the destination type cannot hold the source value, it will wrap around or be truncated. In the example above c will be 44, which is 300 mod 256.
C# types such as int
and long
are abbreviations for longer
names such as System.Int32
and
System.Int64
. You may use either the
short or long names in code:
int i = 44; System.Int32 i = 44; // equivalent
Our C#
quick reference is organized using the long names. For example,
the section System.Int32
lists constants and methods related to the int
type. The MinValue
and MaxValue
constants may be useful:
int min = int.MinValue; // smallest possible int int max = int.MaxValue; // largest possible int
Here are two types that represent floating-point numbers:
float
– 32 bits, 7 significant digits (approximate range: 10-45
to 1038)
double
– 64 bits, 15-16 significant digits (approximate range:
10-324
to 10308)
The type of a floating-point constant (e.g. 3.4
)
is double
by default, so this will fail:
float f = 3.4; // error: can't implicitly convert double to float
To create a constant of type float
,
append the character 'f':
float f = 3.4f; // OK
I generally recommend using double
instead of float
, since 64-bit
floating-point operations are inexpensive on modern CPUs. (On the
other hand, if you have a large array of floating-point values, you
may want to save memory by using float
as the element type if you don't care about the extra precision of a
double
.)
Be careful with division. The following code will print 0, not 0.6:
int i = 3, j = 5; double d = i / j; // integer division! Console.WriteLine(d); // prints 0
That's because the division operator /
will perfom an integer division when given two integer values.
To perform a floating-point division, use a type cast:
int i = 3, j = 5; double d = (double) i / j; // floating-point division Console.WriteLine(d); // prints 0.6
Note that a float
or double
may hold the special values NegativeInfinity
or PositiveInfinity
, which are sometimes
useful. For example:
double d = double.PositiveInfinity;
A for
loop in C#
looks like this:
for (int i = 0 ; i < 10 ; i += 1) Console.WriteLine(i);
To be more precise, a for
statement
contains three clauses, separated by semicolons, plus a loop body.
The initializer (e.g. int
i =
0
)
executes once at the beginning of the loop. It contains either a
variable declaration, or one or more statements separated by commas.
The condition (e.g.
i
<
10
)
is evaluated before every loop iteration. If it is false, the loop
terminates.
The iterator (e.g. i
+=
1
)
executes at the end of every loop iteration. It contains one or more
statements, separated by commas.
The code
for (initializer; condition; iterator) body;
is precisely equivalent to
initializer; while (condition) { body; iterator; }
Note that the intializer, condition, and/or iterator may be empty. An empty condition is equivalent to true. They may even all be absent:
for (;;) Console.WriteLine("hi");
This is the same as
while (true) Console.WriteLine("hi");
C# includes two operators for preincrementing or postincrementing a value:
++x (preincrement)
x++ (postincrement)
When you use these operators as statements, they are equivalent:
int x = 4; ++x; // now x is 5 x++; // now x is 6
However when you use them inside an expression they behave differently. The value of the expression (x++) is the value of x before it is incremented. Similarly, the value of (++x) is the value of x after it is incremented:
int x = 4; int y = x++; // now x is 5, and y is 4 int z = ++x; // now x is 6, and z is 6
It's common to use one of these operators as the iterator in a for
loop:
for (int i = 0 ; i < 10 ; ++i) Console.WriteLine(i);
In this situation the decision whether to use "++i
"
or "i++
" is a question of
style, since they are equivalent.
C# includes the break
and continue
statements. You can use
them in any loop, and they work just like in Python:
for (int i = 0 ; i < 5 ; ++i) { if (i == 1) continue; // skip this iteration if (i == 3) break; // exit the loop Console.WriteLine(i); }
The code above will print
0 2
In the last lecture we briefly introduced the char
type, which represents a 16-bit Unicode character. A character
constant is enclosed in single quotes, e.g. 'x'
or 'ř'
.
Let's consider characters in a bit more detail. There are 216 = 65,536 possible 16-bit values. Unfortunately, this is not enough to represent every possible Unicode character, since Unicode code points may be as large as 10FFFF16 = 1,114,11110. So not every Unicode character will fit in a C# char. This is somewhat unfortunate, and is partially due to the fact that Unicode had a smaller range of characters when C# was designed over 20 years ago.
Fortunately, most common
characters in living world languages have Unicode values that will
fit in 16 bits. However, emoji and other graphical characters may
not. For example, the tomato character (🍅) has a Unicode value of
1F34516,
which is larger than the largest possible 16-bit value FFFF16.
So it won't fit in a C# char
:
char c = '🍅'; // error: too many characters in literal
However it can be stored in a C# string:
string t = "🍅";
This is a string of length 2, since it must be stored as two chars:
Console.WriteLine(t.Length); // writes 2
By contrast, Python has 32-bit characters, so this string would have length 1 in Python (which arguably makes more sense).
You can implicitly convert a char
to any numeric type that can hold a 16-bit unsigned value:
char c = 'ř'; int i = c; // now i holds the Unicode value for 'ř', i.e. 345
You can explicitly convert any numeric type to a char
:
int i = 100; char c = (char) i; // now c is 'd', which is ASCII/Unicode character 100
In the last lecture we briefly introduced the
string
type. A string
is an immutable sequence of char
values.
We can access characters in a string using the [] operator:
string w = "watermelon"; char c = w[0]; // now c = 'w' char d = w[2]; // now c = 't'
An index preceded by ^ counts from the end of the string (like a negative index in Python):
char e = w[^1]; // now e = 'n'
It's possible to extract a range of characters from a string:
string w = "watermelon"; string x = w[2..5]; // now x = "ter" string y = w[5..]; // now y = "melon"
Notice that the range above includes all characters from the character w[2] up to but not including the character w[5]. (It's just like "w[2:5]" in Python.)
The + operator concatenates strings:
string s = "one two"; string t = "three four"; string u = s + " " + u; // "one two three four"
It may be suprising that you cannot compare strings using operators such as < and >:
if ("chameleon" < "dog") // ERROR: operator '<' can't be applied to strings Console.WriteLine("less");
That's because the comparison order of strings may be language-dependent. For example, "chameleon" appears before "dog" in an English dictionary, but in a Czech dictionary "chameleon" appears later since in Czech "ch" is considered to be a single letter that comes after "h" in the alphabet.
A string beginning with $
may contain interpolated
values enclosed in braces (just like f-strings in
Python):
int a = 3, b = 4; Console.WriteLine($"{a} plus {b} equals {a + b}");
A interpolated value may be followed by a format specifier (preceded by a colon).
There are many predefined format specifiers. Many format specifiers can be followed by an integer called the precision, whose meaning varies among specifiers. Here are two useful specifiers:
f
– prints a
number in fixed-point decimal (i.e. not using scientific notation).
The precision is the number of the digits after the decimal point.
n
– prints a
number using thousands separators. The precision is the number of
digits after the decimal point.
For example,
const double pi = 3.14159; const int i = 1357; Console.WriteLine($"{pi:f2} {i:n0}");
writes
3.14 1,357
Some more format specifiers are listed in our C# quick reference.
A foreach
statement
iterates over each element of a collection:
foreach (char c in "hello") Console.WriteLine(c);
The only kinds of collections we have learned about so far are strings (but we will soon see others).
This statement is similar to the 'for' statement in Python, which can also loop over various kinds of collections.
A foreach
statement
always declares an iteration variable whose scope is the body of the
statement.
You can allocate a single-dimensional array like this:
int[] a = new int[10];
In this allocation all elements will be initialized to the element
type's default value. The default value for int
and other numeric types is 0. The default value for bool
is false. The default value for string
is null.
Alternatively, you can allocate an array and initialize its elements to specific values:
int[] a = { 3, 4, 5 };
Arrays are indexed from 0, and the ^ operator indexes from the end:
int x = a[1]; // now x is 4 int y = a[^1]; // now y is 5, the last value in the array
You can access ranges of arrays just like for strings:
int[] a = { 2, 4, 6, 8, 10 }; int[] c = a[1..4]; // { 4, 6, 8 } int[] d = a[..3]; // { 2, 4, 6 } int[] e = a[3..]; // { 8, 10 }
The Length
property returns the length
of an array:
int y = a.Length; // now y is 5
An array is a collection, so you can loop over it with foreach
:
foreach (int i in b) Console.WriteLine(i);
Above, I wrote that the default value for the string
type is null. That's a bit strange, because normally a string
in C# cannot hold null:
string s = null; // ERROR: converting null to non-nullable type
Despite this, if I allocate an array of string
values they will all initially be null:
string[] a = new string[5]; string b = a[0]; // now b is null!
This is an inconsistency in the language. It exists for historical
reasons. In older versions of C# a string
could be null, but then the language changed a few years ago to
become stricter, so now the type string?
is required for values that might either be a string
or null. Unfortunately this change introduced a certain
inconsistency, since elements of a string
array are still initialized to null, just as they were before the
change.
There are two kinds of multidimensional arrays: rectangular arrays and jagged arrays.
You can dynamically allocate a rectangular
array using the new
operator:
int
[,] a =
new
int
[
3
,
4
];
Or you can allocate an array and initialize it with a set of values:
int[,] a = { {1, 4, 5}, {2, 3, 6} };
You can access an
element of a rectangular array using an expression such as a[
2
,
5
]
.
An rectangular array may even have 3 or more dimensions, e.g.
int[,,] a = new int[5, 10, 15]; // a 3-dimensional array
The Length property returns the total number of elements in a rectangular array. This is the product of the lengths of all dimensions. The Rank property returns the number of dimensions, and the GetLength() method returns the length of a given dimension (numbered from 0):
int[,,] a = new int[5, 10, 15]; // a 3-dimensional array Console.WriteLine(a.Length); // writes 750 (= 5 * 10 * 15) Console.WriteLine(a.Rank); // writes 3 Console.WriteLine(a.GetLength(1)); // writes 10
A jagged array is an array of arrays. Unlike in a rectangular multidimensional array, each subarray in a jagged array can have a different length.
You can allocate a jagged array like this:
int[][] a = new int[3][]; a[0] = new int[2]; a[1] = new int[4]; a[2] = new int[5];
If you like, you can initialize a jagged array as you allocate it:
int[][] a = { new int[] { 2, 3}, new int[] { 3, 4, 5}, new int[] { 1, 3, 5, 7, 9} };
You can access an element of a jagged array using an expression such
as a[
2
][
5
]
.
A function can take one or more parameters, each with a specific type. It also has a return type. For example:
double abc(double d, int a) { return d / (2 * a); } // Compute the sum of all values in an array of ints. int sum(int[] a) { int s = 0; foreach (int i in a) s += i; return s; }
The return
statement returns a value
from a function, and may be called anywhere from within the function
body (just like in Python). The first function above returns a
double
, and the second returns an int
.
If a function's return type is void
,
then it does not return a value:
void countdown(int i) { while (i > 0) { Console.WriteLine(i); --i; } }
Note that ReCodEx will not accept code with top-level functions, i.e. functions that are outside of any class. If you write code with top-level functions and want to submit it to ReCodEx, you will need to move the functions inside a class by making them be static methods of the class. (As we'll see in a future lecture, a static method is something like a function that belongs to a class.) So you could write e.g.
using System; class Top { static void countdown(int i) { while (i > 0) { Console.WriteLine(i); --i; } } static void Main() { countdown(10); } }