In the last lecture we learned about 5 built-in
types in C#: int
, long
,
bool
, char
,
and string
. Actually each of these names
is an abbreviation for a longer name:
int = System.Int32
long = System.Int64
bool = System.Boolean
char = System.Char
string = System.String
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, and lists some useful constants and methods for these types.
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
The quick reference also lists some helpful methods for converting between strings and integers:
FormatException
if the string does not contain a valid integer.Notice that Parse() is a static method. That means that we invoke it on the int type directly:
int x = int.Parse("345");
ToString() is not marked with static, so it is an instance method. We invoke it on an instance of the int type, i.e. an integer itself:
int i = 345; string s = i.ToString();
Later in this course we'll see how to write instance methods and static methods in our own classes.
Last week we also learned about arithmetic
operators (+
, -
,
*
, /
). Note
that the +
operator concatenates
strings:
string s = "one two"; string t = "three four"; string u = s + " " + u; // "one two three four"
We also learned about comparison operators such as ==, !=, < and >. 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. So C# disallows this comparison. (Many other languages
such as Python are more liberal in this respect and will allow
strings to be compared using < or >
.)
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.
Last week we learned about the types int
(a signed 32-bit integer) and long
(a
signed 64-bit integer). Here are two more integer types:
uint
– an
unsigned 32-bit integer (0 ≤ i < 232)
ulong
– an unsigned 64-bit integer (0 ≤ i < 264)
Because integer types in C# (unlike Python) have a fixed size, an integer calculation may overflow, i.e. go past the range of allowable values. In this case the higher-order bits of the value will be lost, which effectively computes the value mod 2b for a type with b bits. For example:
uint
i
=
2_000_000_000
;
i
*=
3
;
Console.WriteLine(i);
6,000,000,000 is outside the range of a 32-bit unsigned integer, so the calculation will overflow and this program will print
1705032704
This value is 6,000,000,000 mod 232.
Overflow for a signed integer type may even produce a negative value:
int i = 1_000_000_000; WriteLine(i * 3); // writes -1294967296
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:
int i; ... long l = i; // implicit conversion
This sort of conversion is called a widening conversion because, for example, a long has a wider range than an int.
You can use an explicit conversion to force a conversion between any two numeric types. This is accomplished with a type cast:
ulong l = 5_000_000_000; uint i = (uint) l;
If the destination type cannot hold the source value, it will wrap around or be truncated. In the example above c will be 705,032,704, which is 5,000,000,000 mod 232.
The conversion from ulong to uint is a narrowing conversion because uint has a narrower range than ulong. C# requires a type cast for any narrowing conversion.
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
Here are two built-in 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
can 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
Alternatively, you may multiply by 1.0 to convert an integer to a double:
double d = 1.0 * i / j; // floating-point division
Note that a float
or double
may hold the special values NegativeInfinity
or PositiveInfinity
, which are sometimes
useful. For example:
double d = double.PositiveInfinity;
C# includes a special value null
,
which is like None in Python. It represents the absence of a value.
Most types cannot hold a null value:
int x = null; // error: cannot convert null to int
However, you can add the suffix ?
to any
type to make it nullable. For example, int?
is a type that holds either an int, or null:
int? x = null; int? y = 7;
The Console.ReadLine()
method reads a line of standard input. It returns a value of type
string?
,
which is either a string, or is null (in the case that no more input
is available).
So we can write
string? s = Console.ReadLine(); if (s == null) Console.WriteLine("no more input"); else Console.WriteLine(s);
To keep things simple, often we don't want to worry about the null case. Ad so we can use the null-forgiving operator !, like this:
string s = Console.ReadLine()!;
This operator is like an assertion that a value will not be null. If it actually is null at run time, the program will fail with an exception. After applying this operator to a value of type (string?), we can successfully assign it to a variable of type string.
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.
A do
/while
loop is similar to a while
loop, but
checks the loop condition at the bottom of the loop body. For
example:
string? s; do { s = ReadLine(); } while (s != "yes" && s != "no");
Note that the body of an ordinary while
loop might not execute at all, but the body of a do
/while
loop will always execute at least once.
In most programs do
/while
loops are less common than ordinary while
loops, but they are still occasionally useful.
The conditional operator (?
:
) is similar to the if
statement, but it is an expression, not a statement. It takes
a Boolean value plus two extra values. It returns the first of these
values if the boolean is true, or the second if it is false.
For example, the following statement computes the greater of i and j and assigns it to k:
int i, j; ... int k = i > j ? i : j; // set k to the maximum of i and j
This operator is also sometimes called the ternary operator since it takes three operands. We also saw this operator in Python, which uses a completely different syntax for it:
# Python code: set k to the maximum of i and j k = i if i > j else j;
C# includes fixed-size arrays. You can allocate a single-dimensional array like this:
int[] a = new int[10];
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
An array in C# is a reference type, meaning that more than one variable may refer to the same array. For example:
int[] a = { 10, 20, 30, 40 }; int[] b = a; a[0] = 100; WriteLine(b[0]); // writes 100
The assignment "int[] b = a
"
does not copy the array. Instead, it sets b to point to the
same array as a. (We also saw this phenomenon in Python.)
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 }
Note that the array range operator makes a copy of a range of elements (just like slice operators in Python). Of course, it follows that creating a range of N elements will take O(N) time.
The Length
property
returns the length of an array:
int y = a.Length;
An array is a collection, so you can loop over it with foreach
:
foreach (int i in b) Console.WriteLine(i);
Because arrays have a fixed size, you cannot append a value to an existing array. If you want to append values, probably you should use a List, i.e. a dynamic array (we'll discuss these soon).
In C# you can create 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
In a rectangular array a, a.Length is the total number of elements in the array. This is the product of the lengths of all dimensions. a.Rank is 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
Length and Rank are properties, not methods, so when we access them we don't write (). A property looks like a field (i.e. an attribute), but is not exactly the same as a field since code may run when a property is retrieves. We'll study properties in more detail later in this course, and will learn how to implement properties in our own classes.
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 dynamic array is an extremely useful data type. The C# standard library includes a type List<T>, which implements a dynamic array. This is just like a list in Python.
The T in List<T> indicates that this is a generic type. We'll study generics in more detail later in this course, and will learn how to implement our own generic types. For now, we only need to understand that T represents any type. We may construct a List<int>, which holds integers, a List<string> which holds strings, or a list of any other type. Note that (unlike in Python) all elements of a list must have a common type.
The List<T> class is in the System.Collections.Generic namespace. As we saw last week, in a new C# project the implicit usings feature is enabled by default. With implicit usings, this namespace is automatically imported, so you can use List<T> without writing a "using" statement.
However, we also learned last week that implicit usings are not enabled on ReCodEx. So if you want to use List<T> in a program that you submit to ReCodEx, you'll need to import its namespace explicitly:
using System.Collections.Generic;
Let's create an empty list that will hold integers, and add a few integers to it:
List<int> a = new(); a.Add(3); a.Add(5); a.Add(7);
In C# we can use the new() operator to create a new instance of any type. The .Add() method is just like .append() in Python: it appends an element to a list, and will run in O(1) on average.
Once we have a list, we may get or set its elements by index:
a[0] = a[1] + a[2]; WriteLine(a[2]);
The Count property retrieves the number of elements in a List<T>:
WriteLine($"a has {a.Count} elements");
Our C# quick reference lists more useful methods on List objects. If you look at the documentation for System.Collections.Generic in the quick reference, you'll see that List<T> inherits from IList<T>, which inherits from ICollection<T>, which inherits from IEnumerable<T>. So you'll find methods that work on Lists not only in the section about List<T>, but also in the sections about those supertypes as well. For example, the Insert() and RemoveAt() methods are listed in the documentation for IList<T>, and will work on any list. We'll study C#'s collection class hierarchy in more detail in a later lecture.